From 88d0695a1bad155bfbecdeea7f2442791d31f794 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Fri, 23 May 2025 11:20:05 -0400 Subject: [PATCH] feat: add support for extracting jwt from http headers (#3109) --- packages/jwt/lib/src/jwt.dart | 42 ++++++++++++++++ packages/jwt/test/src/jwt_test.dart | 74 +++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/packages/jwt/lib/src/jwt.dart b/packages/jwt/lib/src/jwt.dart index 24e1f5a3..4ad8d056 100644 --- a/packages/jwt/lib/src/jwt.dart +++ b/packages/jwt/lib/src/jwt.dart @@ -59,6 +59,48 @@ class JwtVerificationFailure implements Exception { String toString() => 'JwtVerificationFailure: $reason'; } +/// {@template jwt_extraction_failure} +/// An exception thrown during JWT extraction. +/// {@endtemplate} +class JwtExtractionFailure implements Exception { + /// {@macro jwt_extraction_failure} + const JwtExtractionFailure(this.reason); + + /// The reason for the extraction failure. + final String reason; + + @override + String toString() => 'JwtExtractionFailure: $reason'; +} + +/// Extracts a JWT from the Authorization header of the request. Throws a +/// [JwtExtractionFailure] if the header is missing or the token is malformed. +/// Does NOT verify the JWT. +Jwt extractFromRequestHeaders(Map headers) { + final caseInsensitiveHeaders = headers.map( + (key, value) => MapEntry(key.toLowerCase(), value), + ); + final authorization = caseInsensitiveHeaders[HttpHeaders.authorizationHeader]; + if (authorization == null) { + throw const JwtExtractionFailure('Missing authorization header'); + } + + final tokenRegExp = RegExp('^Bearer (.*)'); + final token = tokenRegExp.firstMatch(authorization)?.group(1); + if (token == null) { + throw const JwtExtractionFailure('Malformed authorization header'); + } + + final Jwt jwt; + try { + jwt = Jwt.parse(token); + } on Exception { + throw const JwtExtractionFailure('Malformed JWT'); + } + + return jwt; +} + /// Verify the encoded [encodedJwt]. Future verify( String encodedJwt, { diff --git a/packages/jwt/test/src/jwt_test.dart b/packages/jwt/test/src/jwt_test.dart index b8cc8fb5..a96592fe 100644 --- a/packages/jwt/test/src/jwt_test.dart +++ b/packages/jwt/test/src/jwt_test.dart @@ -43,6 +43,80 @@ void main() { }; }); + group(JwtExtractionFailure, () { + group('toString', () { + test('returns the reason', () { + const reason = 'reason'; + const failure = JwtExtractionFailure(reason); + expect(failure.toString(), equals('JwtExtractionFailure: $reason')); + }); + }); + }); + + group('extractFromRequestHeaders', () { + group('when no authorization header is provided', () { + test('throws JwtExtractionFailure', () { + expect( + () => extractFromRequestHeaders({}), + throwsA( + isA().having( + (e) => e.reason, + 'reason', + 'Missing authorization header', + ), + ), + ); + }); + }); + + group('when authorization header is malformed', () { + test('throws JwtExtractionFailure', () { + expect( + () => extractFromRequestHeaders({ + HttpHeaders.authorizationHeader: 'not-a-jwt', + }), + throwsA( + isA().having( + (e) => e.reason, + 'reason', + 'Malformed authorization header', + ), + ), + ); + }); + }); + + group('when authorization header cannot be parsed into jwt', () { + test('throws JwtExtractionFailure', () { + expect( + () => extractFromRequestHeaders({ + HttpHeaders.authorizationHeader: 'Bearer not-a-jwt', + }), + throwsA( + isA().having( + (e) => e.reason, + 'reason', + 'Malformed JWT', + ), + ), + ); + }); + }); + + group('when authorization header contains a valid jwt', () { + test('returns a jwt', () { + final jwt = extractFromRequestHeaders({ + HttpHeaders.authorizationHeader: 'Bearer $token', + }); + expect(jwt.payload.aud, equals('my-app')); + expect( + jwt.payload.iss, + equals('https://securetoken.google.com/my-app'), + ); + }); + }); + }); + group('verify', () { group('when key store is key-value', () { const issuer = 'https://securetoken.google.com/my-app';