feat: add support for extracting jwt from http headers (#3109)

This commit is contained in:
Bryan Oltman
2025-05-23 11:20:05 -04:00
committed by GitHub
parent 192fbb94e3
commit 88d0695a1b
2 changed files with 116 additions and 0 deletions
+42
View File
@@ -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<String, String> 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<Jwt> verify(
String encodedJwt, {
+74
View File
@@ -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<JwtExtractionFailure>().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<JwtExtractionFailure>().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<JwtExtractionFailure>().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';