Skip to content

Commit bcef6eb

Browse files
authored
feat(dart_frog_auth): add cookie authentication (#1600)
1 parent 1402bd6 commit bcef6eb

File tree

3 files changed

+194
-3
lines changed

3 files changed

+194
-3
lines changed

docs/docs/advanced/authentication.md

+45-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ layering the foundation for more advanced authentication. See below for more det
1414

1515
## Dart Frog Auth
1616

17-
The authentication methods provided in `dart_frog_auth` are based on `Authorization` specification,
18-
as defined in [`General HTTP`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). Here you will find support
19-
for `Basic` and `Bearer` authentications, which are common authentication methods used by many developers.
17+
The authentication methods provided in `dart_frog_auth` use different HTTP headers depending on the method. Basic and Bearer authentication use the `Authorization` header, as defined in [`General HTTP`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication), while Cookie-based authentication uses the `Cookie` header, as defined in [`HTTP Cookies`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). The package provides support for Basic, Bearer, and Cookie-based authentications, which are common authentication methods used by many developers.
2018

2119
## Basic Authentication
2220

@@ -153,6 +151,50 @@ Response onRequest(RequestContext context) {
153151

154152
In the case of `null` being returned (unauthenticated), the middleware will automatically send an unauthorized `401` in the response.
155153

154+
### Cookie-based Authentication
155+
156+
To implement cookie-based authentication, you can use the `cookieAuthentication` middleware:
157+
158+
```dart
159+
// routes/admin/_middleware.dart
160+
import 'package:dart_frog/dart_frog.dart';
161+
import 'package:dart_frog_auth/dart_frog_auth.dart';
162+
import 'package:blog/user.dart';
163+
164+
Handler middleware(Handler handler) {
165+
final userRepository = ...;
166+
return handler
167+
.use(requestLogger())
168+
.use(
169+
cookieAuthentication<User>(
170+
authenticator: (context, cookies) {
171+
final userRepository = context.read<UserRepository>();
172+
return userRepository.fetchFromAccessCookies(cookies);
173+
}
174+
),
175+
);
176+
}
177+
```
178+
179+
The `authenticator` parameter must be a function that receives two positional argument the
180+
context and the cookies set in the cookie header and returns a user if any is found
181+
for that token.
182+
183+
Just like in the basic and bearer methods, if a user is returned, it will be set in the request
184+
context and can be read on request handlers, for example:
185+
186+
```dart
187+
import 'package:dart_frog/dart_frog.dart';
188+
import 'package:blog/user.dart';
189+
190+
Response onRequest(RequestContext context) {
191+
final user = context.read<User>();
192+
return Response.json(body: {'user': user.id});
193+
}
194+
```
195+
196+
In the case of `null` being returned (unauthenticated), the middleware will automatically send an unauthorized `401` in the response.
197+
156198
### Filtering Routes
157199

158200
In many instances, developers will want to apply authentication to some routes, while not to others.

packages/dart_frog_auth/lib/src/dart_frog_auth.dart

+59
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ extension on Map<String, String> {
1616

1717
String? bearer() => authorization('Bearer');
1818
String? basic() => authorization('Basic');
19+
Map<String, String>? cookies() {
20+
final cookieString = this['Cookie'];
21+
if (cookieString == null) return null;
22+
23+
final cookiesEntries = cookieString.split('; ').map((cookie) {
24+
final [key, value] = cookie.split('=');
25+
return MapEntry(key, value);
26+
});
27+
28+
return Map.fromEntries(cookiesEntries);
29+
}
1930
}
2031

2132
/// Function definition for the predicate function used by Dart Frog Auth
@@ -154,3 +165,51 @@ Middleware bearerAuthentication<T extends Object>({
154165
return Response(statusCode: HttpStatus.unauthorized);
155166
};
156167
}
168+
169+
/// Authentication that uses the `Cookie` header.
170+
///
171+
/// Cookie authentication expects cookies to be sent in the format:
172+
/// ```markdown
173+
/// Cookie: key1=value1; key2=value2;
174+
/// ```
175+
///
176+
/// This is typically done in web applications where the cookie is set with the
177+
/// `Set-Cookie` header, then sent with every request by a browser.
178+
///
179+
/// The cookie format and contents are up to the user. Typically they will
180+
/// identify a logged in session.
181+
///
182+
/// In order to use this middleware, you must provide a function that will
183+
/// return a user object from the cookies and request context.
184+
///
185+
/// If the given function returns null for the given cookies,
186+
/// the middleware will return a `401 Unauthorized` response.
187+
///
188+
/// By default, this middleware will apply to all routes. You can change this
189+
/// behavior by providing a function that returns a boolean value based on the
190+
/// [RequestContext]. If the function returns false, the middleware will not
191+
/// apply to the route and the call will have no authentication validation.
192+
Middleware cookieAuthentication<T extends Object>({
193+
required Future<T?> Function(
194+
RequestContext context,
195+
Map<String, String> cookies,
196+
) authenticator,
197+
Applies applies = _defaultApplies,
198+
}) {
199+
return (handler) => (context) async {
200+
if (!await applies(context)) {
201+
return handler(context);
202+
}
203+
204+
final cookies = context.request.headers.cookies();
205+
206+
if (cookies != null) {
207+
final user = await authenticator(context, cookies);
208+
if (user != null) {
209+
return handler(context.provide(() => user));
210+
}
211+
}
212+
213+
return Response(statusCode: HttpStatus.unauthorized);
214+
};
215+
}

packages/dart_frog_auth/test/src/dart_frog_auth_test.dart

+90
Original file line numberDiff line numberDiff line change
@@ -456,4 +456,94 @@ void main() {
456456
});
457457
});
458458
});
459+
460+
group('$cookieAuthentication', () {
461+
late RequestContext context;
462+
late Request request;
463+
_User? user;
464+
465+
setUp(() {
466+
context = _MockRequestContext();
467+
request = _MockRequest();
468+
when(() => context.provide<_User>(any())).thenReturn(context);
469+
when(() => request.headers).thenReturn({});
470+
when(() => context.request).thenReturn(request);
471+
});
472+
473+
test('returns 401 when Cookie header is not present', () async {
474+
final middleware = cookieAuthentication<_User>(
475+
authenticator: (_, __) async => user,
476+
);
477+
expect(
478+
await middleware((_) async => Response())(context),
479+
isA<Response>().having(
480+
(r) => r.statusCode,
481+
'statusCode',
482+
HttpStatus.unauthorized,
483+
),
484+
);
485+
});
486+
487+
test(
488+
'returns 401 when Cookie header is present but no user is returned',
489+
() async {
490+
when(() => request.headers).thenReturn({'Cookie': 'session=abc123'});
491+
final middleware = cookieAuthentication<_User>(
492+
authenticator: (_, __) async => null,
493+
);
494+
expect(
495+
await middleware((_) async => Response())(context),
496+
isA<Response>().having(
497+
(r) => r.statusCode,
498+
'statusCode',
499+
HttpStatus.unauthorized,
500+
),
501+
);
502+
},
503+
);
504+
505+
test(
506+
'sets the user when everything is valid',
507+
() async {
508+
user = _User('');
509+
when(() => request.headers).thenReturn({
510+
'Cookie': 'session=abc123',
511+
});
512+
final middleware = cookieAuthentication<_User>(
513+
authenticator: (_, __) async => user,
514+
);
515+
expect(
516+
await middleware((_) async => Response())(context),
517+
isA<Response>().having(
518+
(r) => r.statusCode,
519+
'statusCode',
520+
HttpStatus.ok,
521+
),
522+
);
523+
final captured =
524+
verify(() => context.provide<_User>(captureAny())).captured.single;
525+
expect(
526+
(captured as _User Function()).call(),
527+
equals(user),
528+
);
529+
},
530+
);
531+
532+
test("skips routes that doesn't match the custom predicate", () async {
533+
var called = false;
534+
535+
final middleware = cookieAuthentication<_User>(
536+
authenticator: (_, __) async {
537+
called = true;
538+
return null;
539+
},
540+
applies: (_) async => false,
541+
);
542+
543+
final response = await middleware((_) async => Response())(context);
544+
545+
expect(called, isFalse);
546+
expect(response.statusCode, equals(HttpStatus.ok));
547+
});
548+
});
459549
}

0 commit comments

Comments
 (0)