From 68a6a360848712b6ab56f9b66528fd6263597e9c Mon Sep 17 00:00:00 2001 From: Gourab Singha Date: Sun, 28 Jun 2026 13:59:34 +0530 Subject: [PATCH] fix(gotrue): assert asyncStorage is provided for PKCE flow in the constructor --- packages/gotrue/lib/src/gotrue_client.dart | 6 +++- packages/gotrue/test/admin_test.dart | 1 + packages/gotrue/test/client_test.dart | 35 +++++++++++++++++-- .../gotrue/test/header_isolation_test.dart | 3 ++ .../test/src/gotrue_admin_mfa_api_test.dart | 1 + .../test/src/gotrue_admin_oauth_api_test.dart | 1 + .../gotrue/test/src/gotrue_mfa_api_test.dart | 1 + packages/supabase/test/client_test.dart | 28 ++++++++++++++- packages/supabase/test/mock_test.dart | 8 +++++ packages/supabase/test/realtime_test.dart | 2 ++ packages/supabase/test/utils.dart | 20 +++++++++++ 11 files changed, 102 insertions(+), 4 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 0e145eb54..6a51dfed1 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -151,7 +151,11 @@ class GoTrueClient { Client? httpClient, GotrueAsyncStorage? asyncStorage, AuthFlowType flowType = AuthFlowType.pkce, - }) : _url = url ?? Constants.defaultGotrueUrl, + }) : assert( + flowType != AuthFlowType.pkce || asyncStorage != null, + 'You need to provide asyncStorage to perform pkce flow.', + ), + _url = url ?? Constants.defaultGotrueUrl, _headers = {...Constants.defaultHeaders, ...?headers}, _httpClient = httpClient, _asyncStorage = asyncStorage, diff --git a/packages/gotrue/test/admin_test.dart b/packages/gotrue/test/admin_test.dart index 695d02314..7b3bd0739 100644 --- a/packages/gotrue/test/admin_test.dart +++ b/packages/gotrue/test/admin_test.dart @@ -32,6 +32,7 @@ void main() { 'Authorization': 'Bearer ${getServiceRoleToken(env)}', 'apikey': getServiceRoleToken(env), }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/gotrue/test/client_test.dart b/packages/gotrue/test/client_test.dart index 0f9c35322..9c0ea2073 100644 --- a/packages/gotrue/test/client_test.dart +++ b/packages/gotrue/test/client_test.dart @@ -270,6 +270,7 @@ void main() { final newClient = GoTrueClient( url: gotrueUrl, headers: {'apikey': anonToken}, + flowType: AuthFlowType.implicit, ); expect(newClient.currentSession?.refreshToken ?? '', isEmpty); @@ -301,6 +302,7 @@ void main() { final newClient = GoTrueClient( url: gotrueUrl, headers: {'apikey': anonToken}, + flowType: AuthFlowType.implicit, ); expect(newClient.currentSession, isNull); @@ -344,6 +346,7 @@ void main() { final newClient = GoTrueClient( url: gotrueUrl, headers: {'apikey': anonToken}, + flowType: AuthFlowType.implicit, ); // Should fall back to _callRefreshToken and succeed. @@ -395,6 +398,7 @@ void main() { final newClient = GoTrueClient( url: gotrueUrl, headers: {'apikey': anonToken}, + flowType: AuthFlowType.implicit, ); expect(newClient.currentSession, isNull); @@ -607,7 +611,11 @@ void main() { late GoTrueClient client; setUpAll(() { - client = GoTrueClient(url: gotrueUrl, httpClient: CustomHttpClient()); + client = GoTrueClient( + url: gotrueUrl, + httpClient: CustomHttpClient(), + flowType: AuthFlowType.implicit, + ); }); test('signIn()', () async { @@ -629,7 +637,11 @@ void main() { setUpAll(() { httpClient = RetryTestHttpClient(); - client = GoTrueClient(url: gotrueUrl, httpClient: httpClient); + client = GoTrueClient( + url: gotrueUrl, + httpClient: httpClient, + flowType: AuthFlowType.implicit, + ); }); test('Session recovery succeeds after retries', () async { @@ -793,4 +805,23 @@ void main() { await sub.cancel(); }); }); + + group('PKCE constructor assertion', () { + test( + 'throws AssertionError if asyncStorage is missing when using PKCE flow', + () { + expect( + () => GoTrueClient( + url: gotrueUrl, + headers: {'Authorization': 'Bearer $anonToken', 'apikey': anonToken}, + // asyncStorage is missing/null, and flowType defaults to PKCE + ), + throwsA(isA().having( + (e) => e.message, + 'message', + contains('You need to provide asyncStorage to perform pkce flow.'), + )), + ); + }); + }); } diff --git a/packages/gotrue/test/header_isolation_test.dart b/packages/gotrue/test/header_isolation_test.dart index 1baa77e18..29dca94f1 100644 --- a/packages/gotrue/test/header_isolation_test.dart +++ b/packages/gotrue/test/header_isolation_test.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'utils.dart'; + import 'package:gotrue/gotrue.dart'; import 'package:http/http.dart'; import 'package:test/test.dart'; @@ -42,6 +44,7 @@ void main() { url: 'http://localhost', headers: {'apikey': 'anon-key'}, httpClient: http, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/gotrue/test/src/gotrue_admin_mfa_api_test.dart b/packages/gotrue/test/src/gotrue_admin_mfa_api_test.dart index 8c674f785..f1707be27 100644 --- a/packages/gotrue/test/src/gotrue_admin_mfa_api_test.dart +++ b/packages/gotrue/test/src/gotrue_admin_mfa_api_test.dart @@ -33,6 +33,7 @@ void main() { 'apikey': serviceRoleToken, 'x-forwarded-for': '127.0.0.1' }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart b/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart index 5f4076e56..e6b40fbb2 100644 --- a/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart +++ b/packages/gotrue/test/src/gotrue_admin_oauth_api_test.dart @@ -33,6 +33,7 @@ void main() { 'apikey': serviceRoleToken, 'x-forwarded-for': '127.0.0.1' }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/gotrue/test/src/gotrue_mfa_api_test.dart b/packages/gotrue/test/src/gotrue_mfa_api_test.dart index 696ae5759..430bc4bd7 100644 --- a/packages/gotrue/test/src/gotrue_mfa_api_test.dart +++ b/packages/gotrue/test/src/gotrue_mfa_api_test.dart @@ -35,6 +35,7 @@ void main() { 'apikey': anonToken, 'x-forwarded-for': '127.0.0.1' }, + asyncStorage: TestAsyncStorage(), ); }); diff --git a/packages/supabase/test/client_test.dart b/packages/supabase/test/client_test.dart index 9a7007064..f990dee93 100644 --- a/packages/supabase/test/client_test.dart +++ b/packages/supabase/test/client_test.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:supabase/supabase.dart'; +import 'package:supabase/supabase.dart' hide SupabaseClient; +import 'package:supabase/src/supabase_client.dart' as real; import 'package:test/test.dart'; import 'package:yet_another_json_isolate/yet_another_json_isolate.dart'; @@ -399,3 +400,28 @@ void main() { }); }); } + +class SupabaseClient extends real.SupabaseClient { + SupabaseClient( + super.supabaseUrl, + super.supabaseKey, { + super.postgrestOptions, + AuthClientOptions authOptions = const AuthClientOptions(), + super.storageOptions, + super.functionsOptions, + super.realtimeClientOptions, + super.accessToken, + super.headers, + super.httpClient, + super.isolate, + }) : super( + authOptions: AuthClientOptions( + autoRefreshToken: authOptions.autoRefreshToken, + pkceAsyncStorage: authOptions.pkceAsyncStorage, + authFlowType: authOptions.authFlowType == AuthFlowType.pkce && + authOptions.pkceAsyncStorage == null + ? AuthFlowType.implicit + : authOptions.authFlowType, + ), + ); +} diff --git a/packages/supabase/test/mock_test.dart b/packages/supabase/test/mock_test.dart index 502f0c3c7..61d69234a 100644 --- a/packages/supabase/test/mock_test.dart +++ b/packages/supabase/test/mock_test.dart @@ -363,11 +363,13 @@ void main() { headers: { 'X-Client-Info': 'supabase-flutter/0.0.0', }, + authOptions: const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); customHeadersClient = SupabaseClient( 'http://${mockServer.address.host}:${mockServer.port}', apiKey, headers: {'X-Client-Info': 'supabase-flutter/0.0.0', ...customHeaders}, + authOptions: const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); hasListener = false; }); @@ -786,6 +788,8 @@ void main() { 'http://${errorServer.address.host}:${errorServer.port}', 'test-key', headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + authOptions: + const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); final stream = errorClient.from('todos').stream(primaryKey: ['id']); @@ -817,6 +821,8 @@ void main() { throw Exception('Token retrieval failed'); }, headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + authOptions: + const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); // Should handle token errors gracefully @@ -835,6 +841,8 @@ void main() { 'http://${mockServer.address.host}:${mockServer.port}', 'test-key', headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + authOptions: + const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); // First dispose should succeed diff --git a/packages/supabase/test/realtime_test.dart b/packages/supabase/test/realtime_test.dart index e12fd8335..690e612ea 100644 --- a/packages/supabase/test/realtime_test.dart +++ b/packages/supabase/test/realtime_test.dart @@ -26,6 +26,8 @@ void main() { supabase = SupabaseClient( 'http://${mockServer.address.host}:${mockServer.port}', 'supabaseKey', + authOptions: + const AuthClientOptions(authFlowType: AuthFlowType.implicit), ); channel = supabase.channel('realtime'); diff --git a/packages/supabase/test/utils.dart b/packages/supabase/test/utils.dart index 9962385e4..c9cde5e65 100644 --- a/packages/supabase/test/utils.dart +++ b/packages/supabase/test/utils.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:supabase/supabase.dart'; + /// Construct session data for a given expiration date ({String accessToken, String sessionString}) getSessionData(DateTime dateTime) { final expiresAt = dateTime.millisecondsSinceEpoch ~/ 1000; @@ -10,3 +12,21 @@ import 'dart:convert'; '{"access_token":"$accessToken","expires_in":${dateTime.difference(DateTime.now()).inSeconds},"refresh_token":"-yeS4omysFs9tpUYBws9Rg","token_type":"bearer","provider_token":null,"provider_refresh_token":null,"user":{"id":"4d2583da-8de4-49d3-9cd1-37a9a74f55bd","app_metadata":{"provider":"email","providers":["email"]},"user_metadata":{"Hello":"World"},"aud":"","email":"fake1680338105@email.com","phone":"","created_at":"2023-04-01T08:35:05.208586Z","confirmed_at":null,"email_confirmed_at":"2023-04-01T08:35:05.220096086Z","phone_confirmed_at":null,"last_sign_in_at":"2023-04-01T08:35:05.222755878Z","role":"","updated_at":"2023-04-01T08:35:05.226938Z"}}'; return (accessToken: accessToken, sessionString: sessionString); } + +class TestAsyncStorage extends GotrueAsyncStorage { + final Map _map = {}; + @override + Future getItem({required String key}) async { + return _map[key]; + } + + @override + Future removeItem({required String key}) async { + _map.remove(key); + } + + @override + Future setItem({required String key, required String value}) async { + _map[key] = value; + } +}