From 1981f220763b983d8bbfb8ed112840ed7eb691da Mon Sep 17 00:00:00 2001 From: jtdLab <72231111+jtdLab@users.noreply.github.com> Date: Sun, 27 Oct 2024 02:23:19 +0100 Subject: [PATCH 1/6] update token_verifier to match node admin-sdk --- .../lib/src/auth/token_verifier.dart | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/auth/token_verifier.dart b/packages/dart_firebase_admin/lib/src/auth/token_verifier.dart index e65f78a..562ed30 100644 --- a/packages/dart_firebase_admin/lib/src/auth/token_verifier.dart +++ b/packages/dart_firebase_admin/lib/src/auth/token_verifier.dart @@ -79,9 +79,7 @@ class FirebaseTokenVerifier { isEmulator: isEmulator, ); - final decodedIdToken = DecodedIdToken.fromMap(decoded.payload); - decodedIdToken.uid = decodedIdToken.sub; - return decodedIdToken; + return DecodedIdToken.fromMap(decoded.payload); } Future _decodeAndVerify( @@ -249,6 +247,17 @@ class TokenProvider { required this.tenant, }); + @internal + factory TokenProvider.fromMap(Map map) { + return TokenProvider( + identities: map['identities']! as Map, + signInProvider: map['sign_in_provider']! as String, + signInSecondFactor: map['sign_in_second_factor'] as String?, + secondFactorIdentifier: map['second_factor_identifier'] as String?, + tenant: map['tenant'] as String?, + ); + } + /// Provider-specific identity details corresponding /// to the provider used to sign in the user. Map identities; @@ -313,19 +322,13 @@ class DecodedIdToken { email: map['email'] as String?, emailVerified: map['email_verified'] as bool?, exp: map['exp']! as int, - firebase: TokenProvider( - identities: Map.from(map['firebase']! as Map), - signInProvider: map['sign_in_provider']! as String, - signInSecondFactor: map['sign_in_second_factor'] as String?, - secondFactorIdentifier: map['second_factor_identifier'] as String?, - tenant: map['tenant'] as String?, - ), + firebase: TokenProvider.fromMap(map['firebase']! as Map), iat: map['iat']! as int, iss: map['iss']! as String, phoneNumber: map['phone_number'] as String?, picture: map['picture'] as String?, sub: map['sub']! as String, - uid: map['uid']! as String, + uid: map['sub']! as String, ); } From f2d9a301b012a651522bba84edc6ea4a33b8f23f Mon Sep 17 00:00:00 2001 From: jtdLab <72231111+jtdLab@users.noreply.github.com> Date: Sun, 27 Oct 2024 02:33:18 +0100 Subject: [PATCH 2/6] fix token verification for emulator --- .../lib/src/utils/jwt.dart | 87 +++++++++---------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/utils/jwt.dart b/packages/dart_firebase_admin/lib/src/utils/jwt.dart index ca064ac..8d0b3c5 100644 --- a/packages/dart_firebase_admin/lib/src/utils/jwt.dart +++ b/packages/dart_firebase_admin/lib/src/utils/jwt.dart @@ -9,14 +9,28 @@ class EmulatorSignatureVerifier implements SignatureVerifier { @override Future verify(String token) async { // Signature checks skipped for emulator; no need to fetch public keys. + try { - verifyJwtSignature( + JWT.verify( token, SecretKey(''), ); + } on JWTExpiredException catch (e, stackTrace) { + Error.throwWithStackTrace( + JwtError( + JwtErrorCode.tokenExpired, + 'The provided token has expired. Get a fresh token from your ' + 'client app and try again.', + ), + stackTrace, + ); } on JWTInvalidException catch (e) { + // Emulator tokens have "alg": "none" + if (e.message == 'unknown algorithm') return; if (e.message == 'invalid signature') return; rethrow; + } catch (_) { + // Invalid signature. Can be ignored when using emulator. } } } @@ -122,11 +136,32 @@ class PublicKeySignatureVerifier implements SignatureVerifier { 'no-matching-kid-error', ); } - verifyJwtSignature( - token, - RSAPublicKey.cert(publicKey), - issueAt: Duration.zero, // Any past date should be valid - ); + + try { + JWT.verify( + token, + RSAPublicKey.cert(publicKey), + issueAt: Duration.zero, // Any past date should be valid + ); + } on JWTExpiredException catch (e, stackTrace) { + Error.throwWithStackTrace( + JwtError( + JwtErrorCode.tokenExpired, + 'The provided token has expired. Get a fresh token from your ' + 'client app and try again.', + ), + stackTrace, + ); + } catch (e, stackTrace) { + Error.throwWithStackTrace( + JwtError( + JwtErrorCode.invalidSignature, + 'Error while verifying signature of Firebase ID token: $e', + ), + stackTrace, + ); + } + // At this point most JWTException's should have been caught in // verifyJwtSignature, but we could still get some from JWT.decode above } on JWTException catch (e) { @@ -140,46 +175,6 @@ class PublicKeySignatureVerifier implements SignatureVerifier { sealed class SecretOrPublicKey {} -@internal -void verifyJwtSignature( - String token, - JWTKey key, { - Duration? issueAt, - Audience? audience, - String? subject, - String? issuer, - String? jwtId, -}) { - try { - JWT.verify( - token, - key, - issueAt: issueAt, - audience: audience, - subject: subject, - issuer: issuer, - jwtId: jwtId, - ); - } on JWTExpiredException catch (e, stackTrace) { - Error.throwWithStackTrace( - JwtError( - JwtErrorCode.tokenExpired, - 'The provided token has expired. Get a fresh token from your ' - 'client app and try again.', - ), - stackTrace, - ); - } catch (e, stackTrace) { - Error.throwWithStackTrace( - JwtError( - JwtErrorCode.invalidSignature, - 'Error while verifying signature of Firebase ID token: $e', - ), - stackTrace, - ); - } -} - /// Jwt error code structure. class JwtError extends Error { JwtError(this.code, this.message); From b0c844c6b4b1dc40276cf930af6a3ef39024eed7 Mon Sep 17 00:00:00 2001 From: jtdLab <72231111+jtdLab@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:13:58 +0100 Subject: [PATCH 3/6] test: add decode id token test --- .../test/auth/token_verifier_test.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/dart_firebase_admin/test/auth/token_verifier_test.dart diff --git a/packages/dart_firebase_admin/test/auth/token_verifier_test.dart b/packages/dart_firebase_admin/test/auth/token_verifier_test.dart new file mode 100644 index 0000000..0c064fb --- /dev/null +++ b/packages/dart_firebase_admin/test/auth/token_verifier_test.dart @@ -0,0 +1,52 @@ +import 'package:dart_firebase_admin/src/auth.dart'; +import 'package:test/test.dart'; + +void main() { + group('DecodedIdToken', () { + test('.fromMap', () async { + final idToken = DecodedIdToken.fromMap( + { + 'aud': 'mock-aud', + 'auth_time': 1, + 'email': 'mock-email', + 'email_verified': true, + 'exp': 1, + 'firebase': { + 'identities': { + 'email': 'mock-email', + }, + 'sign_in_provider': 'mock-sign-in-provider', + 'sign_in_second_factor': 'mock-sign-in-second-factor', + 'second_factor_identifier': 'mock-second-factor-identifier', + 'tenant': 'mock-tenant', + }, + 'iat': 1, + 'iss': 'mock-iss', + 'phone_number': 'mock-phone-number', + 'picture': 'mock-picture', + 'sub': 'mock-sub', + 'uid': 'mock-sub', + }, + ); + expect(idToken.aud, 'mock-aud'); + expect(idToken.authTime, DateTime.fromMillisecondsSinceEpoch(1000)); + expect(idToken.email, 'mock-email'); + expect(idToken.emailVerified, true); + expect(idToken.exp, 1); + expect(idToken.firebase.identities, {'email': 'mock-email'}); + expect(idToken.firebase.signInProvider, 'mock-sign-in-provider'); + expect(idToken.firebase.signInSecondFactor, 'mock-sign-in-second-factor'); + expect( + idToken.firebase.secondFactorIdentifier, + 'mock-second-factor-identifier', + ); + expect(idToken.firebase.tenant, 'mock-tenant'); + expect(idToken.iat, 1); + expect(idToken.iss, 'mock-iss'); + expect(idToken.phoneNumber, 'mock-phone-number'); + expect(idToken.picture, 'mock-picture'); + expect(idToken.sub, 'mock-sub'); + expect(idToken.uid, 'mock-sub'); + }); + }); +} From 8cab3de209dd554641e9fb77404171792d7375e9 Mon Sep 17 00:00:00 2001 From: jtdLab <72231111+jtdLab@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:52:38 +0100 Subject: [PATCH 4/6] refactor: simplify by readding verifyJwtSignature --- .../lib/src/utils/jwt.dart | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/utils/jwt.dart b/packages/dart_firebase_admin/lib/src/utils/jwt.dart index 8d0b3c5..9950db9 100644 --- a/packages/dart_firebase_admin/lib/src/utils/jwt.dart +++ b/packages/dart_firebase_admin/lib/src/utils/jwt.dart @@ -11,19 +11,10 @@ class EmulatorSignatureVerifier implements SignatureVerifier { // Signature checks skipped for emulator; no need to fetch public keys. try { - JWT.verify( + verifyJwtSignature( token, SecretKey(''), ); - } on JWTExpiredException catch (e, stackTrace) { - Error.throwWithStackTrace( - JwtError( - JwtErrorCode.tokenExpired, - 'The provided token has expired. Get a fresh token from your ' - 'client app and try again.', - ), - stackTrace, - ); } on JWTInvalidException catch (e) { // Emulator tokens have "alg": "none" if (e.message == 'unknown algorithm') return; @@ -138,20 +129,11 @@ class PublicKeySignatureVerifier implements SignatureVerifier { } try { - JWT.verify( + verifyJwtSignature( token, RSAPublicKey.cert(publicKey), issueAt: Duration.zero, // Any past date should be valid ); - } on JWTExpiredException catch (e, stackTrace) { - Error.throwWithStackTrace( - JwtError( - JwtErrorCode.tokenExpired, - 'The provided token has expired. Get a fresh token from your ' - 'client app and try again.', - ), - stackTrace, - ); } catch (e, stackTrace) { Error.throwWithStackTrace( JwtError( @@ -175,6 +157,38 @@ class PublicKeySignatureVerifier implements SignatureVerifier { sealed class SecretOrPublicKey {} +@internal +void verifyJwtSignature( + String token, + JWTKey key, { + Duration? issueAt, + Audience? audience, + String? subject, + String? issuer, + String? jwtId, +}) { + try { + JWT.verify( + token, + key, + issueAt: issueAt, + audience: audience, + subject: subject, + issuer: issuer, + jwtId: jwtId, + ); + } on JWTExpiredException catch (e, stackTrace) { + Error.throwWithStackTrace( + JwtError( + JwtErrorCode.tokenExpired, + 'The provided token has expired. Get a fresh token from your ' + 'client app and try again.', + ), + stackTrace, + ); + } +} + /// Jwt error code structure. class JwtError extends Error { JwtError(this.code, this.message); From c37a01f0867cd3d9df04eed8c48fe8a094790bb9 Mon Sep 17 00:00:00 2001 From: jtdLab <72231111+jtdLab@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:30:05 +0100 Subject: [PATCH 5/6] fix: dont catch all errors when using emulator e.g expired should still go through --- packages/dart_firebase_admin/lib/src/utils/jwt.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/utils/jwt.dart b/packages/dart_firebase_admin/lib/src/utils/jwt.dart index 9950db9..b783220 100644 --- a/packages/dart_firebase_admin/lib/src/utils/jwt.dart +++ b/packages/dart_firebase_admin/lib/src/utils/jwt.dart @@ -20,8 +20,6 @@ class EmulatorSignatureVerifier implements SignatureVerifier { if (e.message == 'unknown algorithm') return; if (e.message == 'invalid signature') return; rethrow; - } catch (_) { - // Invalid signature. Can be ignored when using emulator. } } } From a6d14365f02e01670094da390cf23b2d3bb06a06 Mon Sep 17 00:00:00 2001 From: jtdLab <72231111+jtdLab@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:32:01 +0100 Subject: [PATCH 6/6] test: remove uid --- packages/dart_firebase_admin/test/auth/token_verifier_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dart_firebase_admin/test/auth/token_verifier_test.dart b/packages/dart_firebase_admin/test/auth/token_verifier_test.dart index 0c064fb..b2363ab 100644 --- a/packages/dart_firebase_admin/test/auth/token_verifier_test.dart +++ b/packages/dart_firebase_admin/test/auth/token_verifier_test.dart @@ -25,7 +25,6 @@ void main() { 'phone_number': 'mock-phone-number', 'picture': 'mock-picture', 'sub': 'mock-sub', - 'uid': 'mock-sub', }, ); expect(idToken.aud, 'mock-aud');