Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
Remove dependency in package:jwt_decoder
Browse files Browse the repository at this point in the history
  • Loading branch information
ditman committed Feb 14, 2023
1 parent 7087d49 commit 382ed3d
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,41 @@ import 'package:google_identity_services_web/id.dart';

import 'jsify_as.dart';

/// A JWT token with null `credential`.
/// A CredentialResponse with null `credential`.
final CredentialResponse nullCredential =
jsifyAs<CredentialResponse>(<String, Object?>{
'credential': null,
});

/// A JWT token for predefined values.
/// A CredentialResponse wrapping a known good JWT Token as its `credential`.
final CredentialResponse goodCredential =
jsifyAs<CredentialResponse>(<String, Object?>{
'credential': goodJwtToken,
});

/// A JWT token with predefined values.
///
/// 'email': 'adultman@example.com',
/// 'sub': '123456',
/// 'name': 'Vincent Adultman',
/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg',
///
/// Signed with HS256 and the private key: 'symmetric-encryption-is-weak'
final CredentialResponse okCredential =
jsifyAs<CredentialResponse>(<String, Object?>{
'credential':
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFkdWx0bWFuQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwibmFtZSI6IlZpbmNlbnQgQWR1bHRtYW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly90aGlzcGVyc29uZG9lc25vdGV4aXN0LmNvbS9pbWFnZT94PS5qcGcifQ.lqzULA_U3YzEl_-fL7YLU-kFXmdD2ttJLTv-UslaNQ4',
});
const String goodJwtToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$goodPayload.lqzULA_U3YzEl_-fL7YLU-kFXmdD2ttJLTv-UslaNQ4';

/// The payload of a JWT token that contains predefined values.
///
/// 'email': 'adultman@example.com',
/// 'sub': '123456',
/// 'name': 'Vincent Adultman',
/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg',
const String goodPayload =
'eyJlbWFpbCI6ImFkdWx0bWFuQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwibmFtZSI6IlZpbmNlbnQgQWR1bHRtYW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly90aGlzcGVyc29uZG9lc25vdGV4aXN0LmNvbS9pbWFnZT94PS5qcGcifQ';

// More encrypted credential responses may be created on https://jwt.io.
// More encrypted JWT Tokens may be created on https://jwt.io.
//
// First, decode the credential that's listed above, modify to your heart's
// First, decode the `goodJwtToken` above, modify to your heart's
// content, and add a new credential here.
//
// (It can also be done with `package:jose` and `dart:convert`.)
// (New tokens can also be created with `package:jose` and `dart:convert`.)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:google_identity_services_web/id.dart';
import 'package:google_identity_services_web/oauth2.dart';
Expand All @@ -18,6 +20,7 @@ void main() {
group('gisResponsesToTokenData', () {
testWidgets('null objects -> no problem', (_) async {
final GoogleSignInTokenData tokens = gisResponsesToTokenData(null, null);

expect(tokens.accessToken, isNull);
expect(tokens.idToken, isNull);
expect(tokens.serverAuthCode, isNull);
Expand All @@ -36,6 +39,7 @@ void main() {
});
final GoogleSignInTokenData tokens =
gisResponsesToTokenData(credential, token);

expect(tokens.accessToken, expectedAccessToken);
expect(tokens.idToken, expectedIdToken);
expect(tokens.serverAuthCode, isNull);
Expand All @@ -44,22 +48,21 @@ void main() {

group('gisResponsesToUserData', () {
testWidgets('happy case', (_) async {
final GoogleSignInUserData data = gisResponsesToUserData(okCredential)!;
final GoogleSignInUserData data = gisResponsesToUserData(goodCredential)!;

expect(data.displayName, 'Vincent Adultman');
expect(data.id, '123456');
expect(data.email, 'adultman@example.com');
expect(data.photoUrl, 'https://thispersondoesnotexist.com/image?x=.jpg');
expect(data.idToken, okCredential.credential);
expect(data.idToken, goodJwtToken);
});

testWidgets('null response -> null', (_) async {
expect(gisResponsesToUserData(null), isNull);
});

testWidgets('null response.credential -> null', (_) async {
final CredentialResponse response = nullCredential;
expect(gisResponsesToUserData(response), isNull);
expect(gisResponsesToUserData(nullCredential), isNull);
});

testWidgets('invalid payload -> null', (_) async {
Expand All @@ -70,4 +73,101 @@ void main() {
expect(gisResponsesToUserData(response), isNull);
});
});

group('getJwtTokenPayload', () {
testWidgets('happy case -> data', (_) async {
final Map<String, Object?>? data = getJwtTokenPayload(goodJwtToken);

expect(data, isNotNull);
expect(data, containsPair('name', 'Vincent Adultman'));
expect(data, containsPair('email', 'adultman@example.com'));
expect(data, containsPair('sub', '123456'));
expect(
data,
containsPair(
'picture',
'https://thispersondoesnotexist.com/image?x=.jpg',
));
});

testWidgets('null Token -> null', (_) async {
final Map<String, Object?>? data = getJwtTokenPayload(null);

expect(data, isNull);
});

testWidgets('Token not matching the format -> null', (_) async {
final Map<String, Object?>? data = getJwtTokenPayload('1234.4321');

expect(data, isNull);
});

testWidgets('Bad token that matches the format -> null', (_) async {
final Map<String, Object?>? data = getJwtTokenPayload('1234.abcd.4321');

expect(data, isNull);
});
});

group('decodeJwtPayload', () {
testWidgets('Good payload -> data', (_) async {
final Map<String, Object?>? data = decodeJwtPayload(goodPayload);

expect(data, isNotNull);
expect(data, containsPair('name', 'Vincent Adultman'));
expect(data, containsPair('email', 'adultman@example.com'));
expect(data, containsPair('sub', '123456'));
expect(
data,
containsPair(
'picture',
'https://thispersondoesnotexist.com/image?x=.jpg',
));
});

testWidgets('Proper JSON payload -> data', (_) async {
final String payload = base64.encode(utf8.encode('{"properJson": true}'));

final Map<String, Object?>? data = decodeJwtPayload(payload);

expect(data, isNotNull);
expect(data, containsPair('properJson', true));
});

testWidgets('Not-normalized base-64 payload -> data', (_) async {
// This is the payload generated by the "Proper JSON payload" test, but
// we remove the leading "=" symbols so it's length is not a multiple of 4
// anymore!
final String payload = 'eyJwcm9wZXJKc29uIjogdHJ1ZX0='.replaceAll('=', '');

final Map<String, Object?>? data = decodeJwtPayload(payload);

expect(data, isNotNull);
expect(data, containsPair('properJson', true));
});

testWidgets('Invalid JSON payload -> null', (_) async {
final String payload = base64.encode(utf8.encode('{properJson: false}'));

final Map<String, Object?>? data = decodeJwtPayload(payload);

expect(data, isNull);
});

testWidgets('Non JSON payload -> null', (_) async {
final String payload = base64.encode(utf8.encode('not-json'));

final Map<String, Object?>? data = decodeJwtPayload(payload);

expect(data, isNull);
});

testWidgets('Non base-64 payload -> null', (_) async {
const String payload = 'not-base-64-at-all';

final Map<String, Object?>? data = decodeJwtPayload(payload);

expect(data, isNull);
});
});
}
51 changes: 49 additions & 2 deletions packages/google_sign_in/google_sign_in_web/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,57 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'package:google_identity_services_web/id.dart';
import 'package:google_identity_services_web/oauth2.dart';
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
import 'package:jwt_decoder/jwt_decoder.dart' as jwt;

/// A codec that can encode/decode JWT payloads.
///
/// See https://www.rfc-editor.org/rfc/rfc7519#section-3
final Codec<Object?, String> jwtCodec = json.fuse(utf8).fuse(base64);

/// A RegExp that can match, and extract parts from a JWT Token.
///
/// A JWT token consists of 3 base-64 encoded parts of data separated by periods:
///
/// header.payload.signature
///
/// More info: https://regexr.com/789qc
final RegExp jwtTokenRegexp = RegExp(
r'^(?<header>[^\.\s]+)\.(?<payload>[^\.\s]+)\.(?<signature>[^\.\s]+)$');

/// Decodes the `claims` of a JWT token and returns them as a Map.
///
/// JWT `claims` are stored as a JSON object in the `payload` part of the token.
///
/// (This method does not validate the signature of the token.)
///
/// See https://www.rfc-editor.org/rfc/rfc7519#section-3
Map<String, Object?>? getJwtTokenPayload(String? token) {
if (token != null) {
final RegExpMatch? match = jwtTokenRegexp.firstMatch(token);
if (match != null) {
return decodeJwtPayload(match.namedGroup('payload'));
}
}

return null;
}

/// Decodes a JWT payload using the [jwtCodec].
Map<String, Object?>? decodeJwtPayload(String? payload) {
try {
// Payload must be normalized before passing it to the codec
return (jwtCodec.decode(base64.normalize(payload!))
as Map<String, Object?>?)
?.cast<String, Object?>();
} catch (_) {
// Do nothing, we always return null for any failure.
}
return null;
}

/// Converts a [CredentialResponse] into a [GoogleSignInUserData].
///
Expand All @@ -18,7 +65,7 @@ GoogleSignInUserData? gisResponsesToUserData(
}

final Map<String, Object?>? payload =
jwt.JwtDecoder.tryDecode(credentialResponse.credential!);
getJwtTokenPayload(credentialResponse.credential);

if (payload == null) {
return null;
Expand Down
1 change: 0 additions & 1 deletion packages/google_sign_in/google_sign_in_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ dependencies:
google_sign_in_platform_interface: ^2.2.0
http: ^0.13.5
js: ^0.6.3
jwt_decoder: ^2.0.1

dev_dependencies:
flutter_test:
Expand Down

0 comments on commit 382ed3d

Please sign in to comment.