From ed56a0b033b9cfde531018506a44d373d772156e Mon Sep 17 00:00:00 2001 From: Marcus Twichel Date: Wed, 18 Dec 2024 10:43:04 -0700 Subject: [PATCH 1/3] feat(dart_frog_auth): add custom unauthenticated responses to all auth middleware --- .../lib/src/dart_frog_auth.dart | 12 +- .../test/src/dart_frog_auth_test.dart | 252 ++++++++++++++++++ 2 files changed, 261 insertions(+), 3 deletions(-) diff --git a/packages/dart_frog_auth/lib/src/dart_frog_auth.dart b/packages/dart_frog_auth/lib/src/dart_frog_auth.dart index c97a62b7c..5e3b5d7c9 100644 --- a/packages/dart_frog_auth/lib/src/dart_frog_auth.dart +++ b/packages/dart_frog_auth/lib/src/dart_frog_auth.dart @@ -72,6 +72,7 @@ Middleware basicAuthentication({ String password, )? authenticator, Applies applies = _defaultApplies, + Handler? unauthenticatedResponse, }) { assert( userFromCredentials != null || authenticator != null, @@ -102,7 +103,8 @@ Middleware basicAuthentication({ } } - return Response(statusCode: HttpStatus.unauthorized); + return unauthenticatedResponse?.call(context) ?? + Response(statusCode: HttpStatus.unauthorized); }; } @@ -134,6 +136,7 @@ Middleware bearerAuthentication({ Future Function(String token)? userFromToken, Future Function(RequestContext context, String token)? authenticator, Applies applies = _defaultApplies, + Handler? unauthenticatedResponse, }) { assert( userFromToken != null || authenticator != null, @@ -162,7 +165,8 @@ Middleware bearerAuthentication({ } } - return Response(statusCode: HttpStatus.unauthorized); + return unauthenticatedResponse?.call(context) ?? + Response(statusCode: HttpStatus.unauthorized); }; } @@ -195,6 +199,7 @@ Middleware cookieAuthentication({ Map cookies, ) authenticator, Applies applies = _defaultApplies, + Handler? unauthenticatedResponse, }) { return (handler) => (context) async { if (!await applies(context)) { @@ -210,6 +215,7 @@ Middleware cookieAuthentication({ } } - return Response(statusCode: HttpStatus.unauthorized); + return unauthenticatedResponse?.call(context) ?? + Response(statusCode: HttpStatus.unauthorized); }; } diff --git a/packages/dart_frog_auth/test/src/dart_frog_auth_test.dart b/packages/dart_frog_auth/test/src/dart_frog_auth_test.dart index 5bb8478a7..e7022b80b 100644 --- a/packages/dart_frog_auth/test/src/dart_frog_auth_test.dart +++ b/packages/dart_frog_auth/test/src/dart_frog_auth_test.dart @@ -11,6 +11,8 @@ class _MockRequestContext extends Mock implements RequestContext {} class _MockRequest extends Mock implements Request {} +class _MockResponse extends Mock implements Response {} + class _User { const _User(this.id); final String id; @@ -85,6 +87,60 @@ void main() { }, ); + group('and custom unauthenticated response is provided', () { + test( + 'returns custom response when Authorization header is not present', + () async { + final response = _MockResponse(); + final middleware = basicAuthentication<_User>( + userFromCredentials: (_, __) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present but ' + 'invalid', + () async { + final response = _MockResponse(); + when(() => request.headers) + .thenReturn({'Authorization': 'not valid'}); + final middleware = basicAuthentication<_User>( + userFromCredentials: (_, __) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present and ' + 'valid but no user is returned', + () async { + final response = _MockResponse(); + when(() => request.headers).thenReturn({ + 'Authorization': 'Bearer 1234', + }); + final middleware = basicAuthentication<_User>( + userFromCredentials: (_, __) async => null, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + }); + test( 'sets the user when everything is valid', () async { @@ -188,6 +244,60 @@ void main() { }, ); + group('and custom unauthenticated response is provided', () { + test( + 'returns custom response when Authorization header is not present', + () async { + final response = _MockResponse(); + final middleware = basicAuthentication<_User>( + authenticator: (_, __, ___) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present but ' + 'invalid', + () async { + final response = _MockResponse(); + when(() => request.headers) + .thenReturn({'Authorization': 'not valid'}); + final middleware = basicAuthentication<_User>( + authenticator: (_, __, ___) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present and ' + 'valid but no user is returned', + () async { + final response = _MockResponse(); + when(() => request.headers).thenReturn({ + 'Authorization': 'Basic dXNlcjpwYXNz', + }); + final middleware = basicAuthentication<_User>( + authenticator: (_, __, ___) async => null, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + }); + test( 'sets the user when everything is valid', () async { @@ -305,6 +415,60 @@ void main() { }, ); + group('and custom unauthenticated response is provided', () { + test( + 'returns custom response when Authorization header is not present', + () async { + final response = _MockResponse(); + final middleware = bearerAuthentication<_User>( + userFromToken: (_) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present but ' + 'invalid', + () async { + final response = _MockResponse(); + when(() => request.headers) + .thenReturn({'Authorization': 'not valid'}); + final middleware = bearerAuthentication<_User>( + userFromToken: (_) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present and ' + 'valid but no user is returned', + () async { + final response = _MockResponse(); + when(() => request.headers).thenReturn({ + 'Authorization': 'Bearer 1234', + }); + final middleware = bearerAuthentication<_User>( + userFromToken: (_) async => null, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + }); + test( 'sets the user when everything is valid', () async { @@ -408,6 +572,60 @@ void main() { }, ); + group('and custom unauthenticated response is provided', () { + test( + 'returns custom response when Authorization header is not present', + () async { + final response = _MockResponse(); + final middleware = bearerAuthentication<_User>( + authenticator: (_, __) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present but ' + 'invalid', + () async { + final response = _MockResponse(); + when(() => request.headers) + .thenReturn({'Authorization': 'not valid'}); + final middleware = bearerAuthentication<_User>( + authenticator: (_, __) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Authorization header is present and ' + 'valid but no user is returned', + () async { + final response = _MockResponse(); + when(() => request.headers).thenReturn({ + 'Authorization': 'Bearer 1234', + }); + final middleware = bearerAuthentication<_User>( + authenticator: (_, __) async => null, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + }); + test( 'sets the user when everything is valid', () async { @@ -502,6 +720,40 @@ void main() { }, ); + group('and custom unauthenticated response is provided', () { + test( + 'returns custom response when Cookie header is not present', + () async { + final response = _MockResponse(); + final middleware = cookieAuthentication<_User>( + authenticator: (_, __) async => user, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + + test( + 'returns custom response when Cookie header is present ' + 'but no user is returned', + () async { + final response = _MockResponse(); + when(() => request.headers).thenReturn({'Cookie': 'session=abc123'}); + final middleware = cookieAuthentication<_User>( + authenticator: (_, __) async => null, + unauthenticatedResponse: (context) => response, + ); + expect( + await middleware((_) async => Response())(context), + equals(response), + ); + }, + ); + }); + test( 'sets the user when everything is valid', () async { From c6be4788573a14510e7793b57432d4dbf7716fc6 Mon Sep 17 00:00:00 2001 From: Marcus Twichel Date: Wed, 18 Dec 2024 10:49:46 -0700 Subject: [PATCH 2/3] add docs --- docs/docs/advanced/authentication.md | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/docs/advanced/authentication.md b/docs/docs/advanced/authentication.md index f69d49a4d..d20a246e7 100644 --- a/docs/docs/advanced/authentication.md +++ b/docs/docs/advanced/authentication.md @@ -247,6 +247,35 @@ Handler middleware(Handler handler) { In the above example, only routes that are not `POST` will have authentication checked. +### Custom Authenticated Responses +In some applications, you'll wish to send a custom response when the request is unauthenticated. +For example, a website will probably send an HTML page explaining to the user they need to log in before accessing the site. + +To accomplish this, simply pass a `Handler` to the `unauthenticatedResponse` parameter to your authentication middleware. + +```dart +Handler middleware(Handler handler) { + final userRepository = UserRepository(); + + return handler + .use(requestLogger()) + .use(provider((_) => userRepository)) + .use( + basicAuthentication( + authenticator: (context, username, password) { + final userRepository = context.read(); + return userRepository.userFromCredentials(username, password); + }, + unauthenticatedResponse : (RequestContext context) async => + Response( + body: 'You are not logged in :(', + statusCode: HttpStatus.unauthorized, + ), + ), + ); +} +``` + ### Authentication vs. Authorization Both Authentication and authorization are related, but are different concepts that are often confused. From f5fdf4870848b0b463f0376bf0dd625c5d3f424c Mon Sep 17 00:00:00 2001 From: Marcus Twichel Date: Thu, 19 Dec 2024 11:51:54 -0700 Subject: [PATCH 3/3] fix doc formatting --- docs/docs/advanced/authentication.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/advanced/authentication.md b/docs/docs/advanced/authentication.md index d20a246e7..b6e157839 100644 --- a/docs/docs/advanced/authentication.md +++ b/docs/docs/advanced/authentication.md @@ -248,6 +248,7 @@ Handler middleware(Handler handler) { In the above example, only routes that are not `POST` will have authentication checked. ### Custom Authenticated Responses + In some applications, you'll wish to send a custom response when the request is unauthenticated. For example, a website will probably send an HTML page explaining to the user they need to log in before accessing the site.