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

Commit 7fb3abf

Browse files
committed
Remove dependency in package:jwt_decoder
1 parent 95f3d9e commit 7fb3abf

File tree

4 files changed

+175
-17
lines changed

4 files changed

+175
-17
lines changed

packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,41 @@ import 'package:google_identity_services_web/id.dart';
66

77
import 'jsify_as.dart';
88

9-
/// A JWT token with null `credential`.
9+
/// A CredentialResponse with null `credential`.
1010
final CredentialResponse nullCredential =
1111
jsifyAs<CredentialResponse>(<String, Object?>{
1212
'credential': null,
1313
});
1414

15-
/// A JWT token for predefined values.
15+
/// A CredentialResponse wrapping a known good JWT Token as its `credential`.
16+
final CredentialResponse goodCredential =
17+
jsifyAs<CredentialResponse>(<String, Object?>{
18+
'credential': goodJwtToken,
19+
});
20+
21+
/// A JWT token with predefined values.
1622
///
1723
/// 'email': '[email protected]',
1824
/// 'sub': '123456',
1925
/// 'name': 'Vincent Adultman',
2026
/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg',
2127
///
2228
/// Signed with HS256 and the private key: 'symmetric-encryption-is-weak'
23-
final CredentialResponse okCredential =
24-
jsifyAs<CredentialResponse>(<String, Object?>{
25-
'credential':
26-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFkdWx0bWFuQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwibmFtZSI6IlZpbmNlbnQgQWR1bHRtYW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly90aGlzcGVyc29uZG9lc25vdGV4aXN0LmNvbS9pbWFnZT94PS5qcGcifQ.lqzULA_U3YzEl_-fL7YLU-kFXmdD2ttJLTv-UslaNQ4',
27-
});
29+
const String goodJwtToken =
30+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$goodPayload.lqzULA_U3YzEl_-fL7YLU-kFXmdD2ttJLTv-UslaNQ4';
31+
32+
/// The payload of a JWT token that contains predefined values.
33+
///
34+
/// 'email': '[email protected]',
35+
/// 'sub': '123456',
36+
/// 'name': 'Vincent Adultman',
37+
/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg',
38+
const String goodPayload =
39+
'eyJlbWFpbCI6ImFkdWx0bWFuQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwibmFtZSI6IlZpbmNlbnQgQWR1bHRtYW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly90aGlzcGVyc29uZG9lc25vdGV4aXN0LmNvbS9pbWFnZT94PS5qcGcifQ';
2840

29-
// More encrypted credential responses may be created on https://jwt.io.
41+
// More encrypted JWT Tokens may be created on https://jwt.io.
3042
//
31-
// First, decode the credential that's listed above, modify to your heart's
43+
// First, decode the `goodJwtToken` above, modify to your heart's
3244
// content, and add a new credential here.
3345
//
34-
// (It can also be done with `package:jose` and `dart:convert`.)
46+
// (New tokens can also be created with `package:jose` and `dart:convert`.)

packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:convert';
6+
57
import 'package:flutter_test/flutter_test.dart';
68
import 'package:google_identity_services_web/id.dart';
79
import 'package:google_identity_services_web/oauth2.dart';
@@ -18,6 +20,7 @@ void main() {
1820
group('gisResponsesToTokenData', () {
1921
testWidgets('null objects -> no problem', (_) async {
2022
final GoogleSignInTokenData tokens = gisResponsesToTokenData(null, null);
23+
2124
expect(tokens.accessToken, isNull);
2225
expect(tokens.idToken, isNull);
2326
expect(tokens.serverAuthCode, isNull);
@@ -36,6 +39,7 @@ void main() {
3639
});
3740
final GoogleSignInTokenData tokens =
3841
gisResponsesToTokenData(credential, token);
42+
3943
expect(tokens.accessToken, expectedAccessToken);
4044
expect(tokens.idToken, expectedIdToken);
4145
expect(tokens.serverAuthCode, isNull);
@@ -44,22 +48,21 @@ void main() {
4448

4549
group('gisResponsesToUserData', () {
4650
testWidgets('happy case', (_) async {
47-
final GoogleSignInUserData data = gisResponsesToUserData(okCredential)!;
51+
final GoogleSignInUserData data = gisResponsesToUserData(goodCredential)!;
4852

4953
expect(data.displayName, 'Vincent Adultman');
5054
expect(data.id, '123456');
5155
expect(data.email, '[email protected]');
5256
expect(data.photoUrl, 'https://thispersondoesnotexist.com/image?x=.jpg');
53-
expect(data.idToken, okCredential.credential);
57+
expect(data.idToken, goodJwtToken);
5458
});
5559

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

6064
testWidgets('null response.credential -> null', (_) async {
61-
final CredentialResponse response = nullCredential;
62-
expect(gisResponsesToUserData(response), isNull);
65+
expect(gisResponsesToUserData(nullCredential), isNull);
6366
});
6467

6568
testWidgets('invalid payload -> null', (_) async {
@@ -70,4 +73,101 @@ void main() {
7073
expect(gisResponsesToUserData(response), isNull);
7174
});
7275
});
76+
77+
group('getJwtTokenPayload', () {
78+
testWidgets('happy case -> data', (_) async {
79+
final Map<String, Object?>? data = getJwtTokenPayload(goodJwtToken);
80+
81+
expect(data, isNotNull);
82+
expect(data, containsPair('name', 'Vincent Adultman'));
83+
expect(data, containsPair('email', '[email protected]'));
84+
expect(data, containsPair('sub', '123456'));
85+
expect(
86+
data,
87+
containsPair(
88+
'picture',
89+
'https://thispersondoesnotexist.com/image?x=.jpg',
90+
));
91+
});
92+
93+
testWidgets('null Token -> null', (_) async {
94+
final Map<String, Object?>? data = getJwtTokenPayload(null);
95+
96+
expect(data, isNull);
97+
});
98+
99+
testWidgets('Token not matching the format -> null', (_) async {
100+
final Map<String, Object?>? data = getJwtTokenPayload('1234.4321');
101+
102+
expect(data, isNull);
103+
});
104+
105+
testWidgets('Bad token that matches the format -> null', (_) async {
106+
final Map<String, Object?>? data = getJwtTokenPayload('1234.abcd.4321');
107+
108+
expect(data, isNull);
109+
});
110+
});
111+
112+
group('decodeJwtPayload', () {
113+
testWidgets('Good payload -> data', (_) async {
114+
final Map<String, Object?>? data = decodeJwtPayload(goodPayload);
115+
116+
expect(data, isNotNull);
117+
expect(data, containsPair('name', 'Vincent Adultman'));
118+
expect(data, containsPair('email', '[email protected]'));
119+
expect(data, containsPair('sub', '123456'));
120+
expect(
121+
data,
122+
containsPair(
123+
'picture',
124+
'https://thispersondoesnotexist.com/image?x=.jpg',
125+
));
126+
});
127+
128+
testWidgets('Proper JSON payload -> data', (_) async {
129+
final String payload = base64.encode(utf8.encode('{"properJson": true}'));
130+
131+
final Map<String, Object?>? data = decodeJwtPayload(payload);
132+
133+
expect(data, isNotNull);
134+
expect(data, containsPair('properJson', true));
135+
});
136+
137+
testWidgets('Not-normalized base-64 payload -> data', (_) async {
138+
// This is the payload generated by the "Proper JSON payload" test, but
139+
// we remove the leading "=" symbols so it's length is not a multiple of 4
140+
// anymore!
141+
final String payload = 'eyJwcm9wZXJKc29uIjogdHJ1ZX0='.replaceAll('=', '');
142+
143+
final Map<String, Object?>? data = decodeJwtPayload(payload);
144+
145+
expect(data, isNotNull);
146+
expect(data, containsPair('properJson', true));
147+
});
148+
149+
testWidgets('Invalid JSON payload -> null', (_) async {
150+
final String payload = base64.encode(utf8.encode('{properJson: false}'));
151+
152+
final Map<String, Object?>? data = decodeJwtPayload(payload);
153+
154+
expect(data, isNull);
155+
});
156+
157+
testWidgets('Non JSON payload -> null', (_) async {
158+
final String payload = base64.encode(utf8.encode('not-json'));
159+
160+
final Map<String, Object?>? data = decodeJwtPayload(payload);
161+
162+
expect(data, isNull);
163+
});
164+
165+
testWidgets('Non base-64 payload -> null', (_) async {
166+
const String payload = 'not-base-64-at-all';
167+
168+
final Map<String, Object?>? data = decodeJwtPayload(payload);
169+
170+
expect(data, isNull);
171+
});
172+
});
73173
}

packages/google_sign_in/google_sign_in_web/lib/src/utils.dart

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,57 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:convert';
6+
57
import 'package:google_identity_services_web/id.dart';
68
import 'package:google_identity_services_web/oauth2.dart';
79
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
8-
import 'package:jwt_decoder/jwt_decoder.dart' as jwt;
10+
11+
/// A codec that can encode/decode JWT payloads.
12+
///
13+
/// See https://www.rfc-editor.org/rfc/rfc7519#section-3
14+
final Codec<Object?, String> jwtCodec = json.fuse(utf8).fuse(base64);
15+
16+
/// A RegExp that can match, and extract parts from a JWT Token.
17+
///
18+
/// A JWT token consists of 3 base-64 encoded parts of data separated by periods:
19+
///
20+
/// header.payload.signature
21+
///
22+
/// More info: https://regexr.com/789qc
23+
final RegExp jwtTokenRegexp = RegExp(
24+
r'^(?<header>[^\.\s]+)\.(?<payload>[^\.\s]+)\.(?<signature>[^\.\s]+)$');
25+
26+
/// Decodes the `claims` of a JWT token and returns them as a Map.
27+
///
28+
/// JWT `claims` are stored as a JSON object in the `payload` part of the token.
29+
///
30+
/// (This method does not validate the signature of the token.)
31+
///
32+
/// See https://www.rfc-editor.org/rfc/rfc7519#section-3
33+
Map<String, Object?>? getJwtTokenPayload(String? token) {
34+
if (token != null) {
35+
final RegExpMatch? match = jwtTokenRegexp.firstMatch(token);
36+
if (match != null) {
37+
return decodeJwtPayload(match.namedGroup('payload'));
38+
}
39+
}
40+
41+
return null;
42+
}
43+
44+
/// Decodes a JWT payload using the [jwtCodec].
45+
Map<String, Object?>? decodeJwtPayload(String? payload) {
46+
try {
47+
// Payload must be normalized before passing it to the codec
48+
return (jwtCodec.decode(base64.normalize(payload!))
49+
as Map<String, Object?>?)
50+
?.cast<String, Object?>();
51+
} catch (_) {
52+
// Do nothing, we always return null for any failure.
53+
}
54+
return null;
55+
}
956

1057
/// Converts a [CredentialResponse] into a [GoogleSignInUserData].
1158
///
@@ -18,7 +65,7 @@ GoogleSignInUserData? gisResponsesToUserData(
1865
}
1966

2067
final Map<String, Object?>? payload =
21-
jwt.JwtDecoder.tryDecode(credentialResponse.credential!);
68+
getJwtTokenPayload(credentialResponse.credential);
2269

2370
if (payload == null) {
2471
return null;

packages/google_sign_in/google_sign_in_web/pubspec.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ dependencies:
2626
google_sign_in_platform_interface: ^2.2.0
2727
http: ^0.13.5
2828
js: ^0.6.3
29-
jwt_decoder: ^2.0.1
3029

3130
dev_dependencies:
3231
flutter_test:

0 commit comments

Comments
 (0)