Skip to content

Commit f6407d8

Browse files
grdsdevclaude
andcommitted
test(gotrue): fix expiresIn tests by using raw base64url JWT crafting
dart_jsonwebtoken auto-injects an iat claim when signing, which broke: - 'expiresIn equals exp minus iat' (iat value was overridden) - 'expiresIn is null when iat absent' (iat was auto-added) Replace dart_jsonwebtoken usage with _makeRawJwt(), which base64url-encodes the payload map directly (no auto-claims, no signature verification needed). Also removes the dart_jsonwebtoken import from the test file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 42ce6fa commit f6407d8

1 file changed

Lines changed: 27 additions & 21 deletions

File tree

packages/gotrue/test/src/set_session_test.dart

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'dart:async';
22
import 'dart:convert';
33

4-
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
54
import 'package:gotrue/gotrue.dart';
65
import 'package:http/http.dart';
76
import 'package:test/test.dart';
@@ -50,8 +49,8 @@ class _SetSessionMockClient extends BaseClient {
5049
// Refresh-token fallback response with a freshly minted access token.
5150
final exp = DateTime.now().millisecondsSinceEpoch ~/ 1000 + 3600;
5251
final iat = exp - 3600;
53-
final freshAt = JWT({'exp': exp, 'iat': iat}, subject: 'mock-user-id')
54-
.sign(SecretKey('test-secret'));
52+
final freshAt =
53+
_makeRawJwt({'exp': exp, 'iat': iat, 'sub': 'mock-user-id'});
5554
return StreamedResponse(
5655
Stream.value(utf8.encode(jsonEncode({
5756
'access_token': freshAt,
@@ -68,12 +67,17 @@ class _SetSessionMockClient extends BaseClient {
6867
}
6968
}
7069

71-
/// Returns a signed JWT with the given [exp] and optional [iat] claims.
72-
/// If [iat] is null, the claim is omitted entirely.
73-
String _makeJwt({required int exp, int? iat}) {
74-
final payload = <String, dynamic>{'exp': exp};
75-
if (iat != null) payload['iat'] = iat;
76-
return JWT(payload, subject: 'mock-user-id').sign(SecretKey('test-secret'));
70+
/// Crafts a JWT by base64url-encoding [payload] directly.
71+
///
72+
/// Unlike using dart_jsonwebtoken, this gives exact control over every claim —
73+
/// no auto-injected `iat`, no claim overrides. The signature is a stub;
74+
/// [decodeJwt] does not verify signatures.
75+
String _makeRawJwt(Map<String, dynamic> payload) {
76+
final header =
77+
base64Url.encode(utf8.encode(jsonEncode({'alg': 'HS256', 'typ': 'JWT'})));
78+
final body = base64Url.encode(utf8.encode(jsonEncode(payload)));
79+
const sig = 'AAAA';
80+
return '$header.$body.$sig';
7781
}
7882

7983
void main() {
@@ -94,7 +98,8 @@ void main() {
9498
'empty refresh token with a non-null access token throws before '
9599
'inspecting the access token', () async {
96100
final exp = DateTime.now().millisecondsSinceEpoch ~/ 1000 + 3600;
97-
final at = _makeJwt(exp: exp, iat: exp - 3600);
101+
final at =
102+
_makeRawJwt({'exp': exp, 'iat': exp - 3600, 'sub': 'mock-user-id'});
98103

99104
await expectLater(
100105
() => client.setSession('', accessToken: at),
@@ -109,7 +114,8 @@ void main() {
109114
'as expired and falls back to the refresh-token path', () async {
110115
final timeNow = DateTime.now().millisecondsSinceEpoch ~/ 1000;
111116
// exp is 20 s in the future, inside the 30 s Constants.expiryMargin.
112-
final at = _makeJwt(exp: timeNow + 20, iat: timeNow - 3580);
117+
final at = _makeRawJwt(
118+
{'exp': timeNow + 20, 'iat': timeNow - 3580, 'sub': 'mock-user-id'});
113119

114120
final response =
115121
await client.setSession('some-refresh-token', accessToken: at);
@@ -124,8 +130,7 @@ void main() {
124130
'access token with no exp claim is treated as expired and falls back '
125131
'to the refresh-token path', () async {
126132
// JWT without an exp claim: decodeJwt succeeds but exp == null.
127-
final at = JWT({'role': 'authenticated'}, subject: 'mock-user-id')
128-
.sign(SecretKey('test-secret'));
133+
final at = _makeRawJwt({'role': 'authenticated', 'sub': 'mock-user-id'});
129134

130135
final response =
131136
await client.setSession('some-refresh-token', accessToken: at);
@@ -141,7 +146,7 @@ void main() {
141146
() async {
142147
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000 - 60;
143148
final exp = iat + 3600;
144-
final at = _makeJwt(exp: exp, iat: iat);
149+
final at = _makeRawJwt({'exp': exp, 'iat': iat, 'sub': 'mock-user-id'});
145150

146151
final response =
147152
await client.setSession('some-refresh-token', accessToken: at);
@@ -153,7 +158,7 @@ void main() {
153158
test('expiresIn is null when iat claim is absent', () async {
154159
final exp = DateTime.now().millisecondsSinceEpoch ~/ 1000 + 3600;
155160
// JWT without iat.
156-
final at = _makeJwt(exp: exp);
161+
final at = _makeRawJwt({'exp': exp, 'sub': 'mock-user-id'});
157162

158163
final response =
159164
await client.setSession('some-refresh-token', accessToken: at);
@@ -164,7 +169,7 @@ void main() {
164169
test('expiresAt matches the exp claim in the JWT', () async {
165170
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000 - 60;
166171
final exp = iat + 3600;
167-
final at = _makeJwt(exp: exp, iat: iat);
172+
final at = _makeRawJwt({'exp': exp, 'iat': iat, 'sub': 'mock-user-id'});
168173

169174
final response =
170175
await client.setSession('some-refresh-token', accessToken: at);
@@ -178,7 +183,7 @@ void main() {
178183
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000 - 60;
179184
final exp = iat + 3600;
180185
const refreshToken = 'my-refresh-token';
181-
final at = _makeJwt(exp: exp, iat: iat);
186+
final at = _makeRawJwt({'exp': exp, 'iat': iat, 'sub': 'mock-user-id'});
182187

183188
final response = await client.setSession(refreshToken, accessToken: at);
184189

@@ -192,7 +197,7 @@ void main() {
192197
test('fast path emits signedIn (not tokenRefreshed)', () async {
193198
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000 - 60;
194199
final exp = iat + 3600;
195-
final at = _makeJwt(exp: exp, iat: iat);
200+
final at = _makeRawJwt({'exp': exp, 'iat': iat, 'sub': 'mock-user-id'});
196201

197202
expect(
198203
client.onAuthStateChange,
@@ -205,7 +210,8 @@ void main() {
205210
test('expired-fallback path emits tokenRefreshed (not signedIn)', () async {
206211
final timeNow = DateTime.now().millisecondsSinceEpoch ~/ 1000;
207212
// Clearly expired token (exp well in the past).
208-
final at = _makeJwt(exp: timeNow - 100, iat: timeNow - 3700);
213+
final at = _makeRawJwt(
214+
{'exp': timeNow - 100, 'iat': timeNow - 3700, 'sub': 'mock-user-id'});
209215

210216
expect(
211217
client.onAuthStateChange,
@@ -223,7 +229,7 @@ void main() {
223229
'and both receive the same response', () async {
224230
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000 - 60;
225231
final exp = iat + 3600;
226-
final at = _makeJwt(exp: exp, iat: iat);
232+
final at = _makeRawJwt({'exp': exp, 'iat': iat, 'sub': 'mock-user-id'});
227233

228234
// Pause the mock /user response so both calls are in-flight together.
229235
final pause = Completer<void>();
@@ -245,7 +251,7 @@ void main() {
245251
test('deduplicated call emits signedIn exactly once', () async {
246252
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000 - 60;
247253
final exp = iat + 3600;
248-
final at = _makeJwt(exp: exp, iat: iat);
254+
final at = _makeRawJwt({'exp': exp, 'iat': iat, 'sub': 'mock-user-id'});
249255

250256
final pause = Completer<void>();
251257
mockClient.userCallPause = pause;

0 commit comments

Comments
 (0)