Skip to content

Commit 207ed5f

Browse files
grdsdevclaude
andauthored
fix(auth): fix getClaims() crash with asymmetric JWTs on first call (#1300)
Fixed a crash in getClaims() that occurred when verifying JWTs signed with asymmetric algorithms (RS256, ES256) on the first call. The issue was that _jwks was force-unwrapped (_jwks!) before it was initialized. On the first call to getClaims() with an asymmetric JWT containing a 'kid' header, _jwks would be null, causing a null check operator error. The fix passes an empty JWKSet when the cache is null: _jwks ?? JWKSet(keys: []). This allows _fetchJwk() to handle the first call gracefully by fetching from the server and populating the cache. Changes: - Updated getClaims() to use null-coalescing operator instead of force-unwrap - Added test case to reproduce and verify the fix for SDK-627 - Enhanced documentation to clarify JWKS caching behavior for asymmetric JWTs Linear: SDK-627 Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 0719e55 commit 207ed5f

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

packages/gotrue/lib/src/gotrue_client.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,11 @@ class GoTrueClient {
13971397
///
13981398
/// If the project is not using an asymmetric JWT signing key (like ECC or
13991399
/// RSA) it always sends a request to the Auth server (similar to [getUser]) to verify the JWT.
1400+
///
1401+
/// For JWTs signed with asymmetric algorithms (RS256, ES256, etc.), the JWKS
1402+
/// is fetched from the server on the first call and cached for subsequent calls.
1403+
/// The cache is refreshed automatically after 10 minutes.
1404+
///
14001405
/// [jwt] An optional specific JWT you wish to verify, not the one you
14011406
/// can obtain from [currentSession].
14021407
/// [options] Various additional options that allow you to customize the
@@ -1428,7 +1433,7 @@ class GoTrueClient {
14281433
final signingKey =
14291434
(decoded.header.alg.startsWith('HS') || decoded.header.kid == null)
14301435
? null
1431-
: await _fetchJwk(decoded.header.kid!, _jwks!);
1436+
: await _fetchJwk(decoded.header.kid!, _jwks ?? JWKSet(keys: []));
14321437

14331438
// If symmetric algorithm, fallback to getUser()
14341439
if (signingKey == null) {

packages/gotrue/test/get_claims_test.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,54 @@ void main() {
207207
});
208208
});
209209

210+
group('getClaims with asymmetric JWTs', () {
211+
late GoTrueClient client;
212+
213+
setUp(() {
214+
final asyncStorage = TestAsyncStorage();
215+
216+
client = GoTrueClient(
217+
url: gotrueUrl,
218+
headers: {
219+
'Authorization': 'Bearer $anonToken',
220+
'apikey': anonToken,
221+
},
222+
asyncStorage: asyncStorage,
223+
flowType: AuthFlowType.implicit,
224+
);
225+
});
226+
227+
test('getClaims() with RS256 JWT on first call should not crash (SDK-627)',
228+
() async {
229+
// This test reproduces the bug reported in SDK-627
230+
// A JWT with RS256 algorithm and kid in header
231+
// Header: {"alg":"RS256","typ":"JWT","kid":"test-key-id"}
232+
// Payload: {"sub":"1234567890","aud":"authenticated","exp":9999999999,"iat":1516239022,"email":"test@example.com","role":"authenticated"}
233+
// Signature: dummy base64url encoded signature (not cryptographically valid, but structurally valid)
234+
const rs256Jwt =
235+
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LWlkIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiYXV0aGVudGljYXRlZCIsImV4cCI6OTk5OTk5OTk5OSwiaWF0IjoxNTE2MjM5MDIyLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20iLCJyb2xlIjoiYXV0aGVudGljYXRlZCJ9.SW52YWxpZFNpZ25hdHVyZURhdGFIZXJlVGhhdElzTm90UmVhbEJ1dFZhbGlkQmFzZTY0VXJs';
236+
237+
// Before the fix, this would crash with:
238+
// "Null check operator used on a null value"
239+
// because _jwks is null on first call and the code does _jwks!
240+
//
241+
// After the fix, this should attempt to fetch JWKS from the server
242+
// and fail gracefully (either with network error or invalid signature)
243+
// but NOT crash with null error
244+
try {
245+
await client.getClaims(rs256Jwt);
246+
// If we get here, the server responded successfully (unlikely in test env)
247+
} catch (e) {
248+
// The important part is that it should NOT crash with null error
249+
// It may fail with network error, invalid signature, etc.
250+
// but the error message should not contain null-related errors
251+
expect(e.toString(), isNot(contains('Unexpected null value')));
252+
expect(e.toString(), isNot(contains('Null check operator')));
253+
}
254+
// Test passes if we get here without null error
255+
});
256+
});
257+
210258
group('JWT helper functions', () {
211259
test('decodeJwt() successfully decodes valid JWT', () {
212260
// A sample JWT with known values

0 commit comments

Comments
 (0)