diff --git a/docs/docs/advanced/authentication.md b/docs/docs/advanced/authentication.md index f69d49a4d..b6e157839 100644 --- a/docs/docs/advanced/authentication.md +++ b/docs/docs/advanced/authentication.md @@ -247,6 +247,36 @@ 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. 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 4245a43b4..907436e04 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 @@ -13,6 +13,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; @@ -87,6 +89,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 { @@ -190,6 +246,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 { @@ -307,6 +417,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 { @@ -410,6 +574,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 { @@ -504,6 +722,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 {