Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/gotrue/example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Future<void> main() async {
'Authorization': 'Bearer $supabaseKey',
'apikey': supabaseKey,
},
// The pkce flow needs somewhere to keep its code verifiers. Swap this for
// a persistent storage when the code exchange can happen after a restart.
asyncStorage: MemoryGotrueAsyncStorage(),
);

try {
Expand Down
11 changes: 9 additions & 2 deletions packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class _SessionState {
/// [httpClient] custom http client.
///
/// [asyncStorage] local storage to store pkce code verifiers. Required when
/// using the pkce flow.
/// using the pkce flow. Pass a [MemoryGotrueAsyncStorage] when the verifiers
/// do not need to outlive the process.
///
/// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow.
/// {@endtemplate}
Expand Down Expand Up @@ -156,7 +157,13 @@ 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. Pass a '
'MemoryGotrueAsyncStorage when the code verifiers do not need to '
'outlive the process.',
),
Comment thread
spydon marked this conversation as resolved.
Comment thread
spydon marked this conversation as resolved.
_url = url ?? Constants.defaultGotrueUrl,
_headers = {...Constants.defaultHeaders, ...?headers},
_httpClient = httpClient,
_asyncStorage = asyncStorage,
Expand Down
23 changes: 23 additions & 0 deletions packages/gotrue/lib/src/types/gotrue_async_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,26 @@ abstract class GotrueAsyncStorage {
/// Removes an item asynchronously from the storage for the given key.
Future<void> removeItem({required String key});
}

/// A [GotrueAsyncStorage] that keeps the pkce code verifiers in memory only.
///
/// Everything it holds is lost when the process exits, so a pkce flow started
/// before a restart can no longer be completed. Use a persistent
/// implementation when the code exchange happens after the app was closed,
/// which is what `supabase_flutter` does with shared preferences.
class MemoryGotrueAsyncStorage extends GotrueAsyncStorage {
final _items = <String, String>{};

@override
Future<String?> getItem({required String key}) async => _items[key];

@override
Future<void> setItem({required String key, required String value}) async {
_items[key] = value;
}

@override
Future<void> removeItem({required String key}) async {
_items.remove(key);
}
}
3 changes: 3 additions & 0 deletions packages/gotrue/test/admin_delete_user_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:gotrue/gotrue.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';

import 'utils.dart';

class _CapturingHttpClient extends BaseClient {
Request? lastRequest;

Expand All @@ -25,6 +27,7 @@ void main() {
client = GoTrueClient(
url: 'http://localhost:9999',
httpClient: httpClient,
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
3 changes: 3 additions & 0 deletions packages/gotrue/test/admin_list_users_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:gotrue/gotrue.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';

import 'utils.dart';

/// Serves a fixed list-users response with the pagination headers the GoTrue
/// server sends alongside it.
class ListUsersMockClient extends BaseClient {
Expand Down Expand Up @@ -49,6 +51,7 @@ void main() {
GoTrueClient clientWith(ListUsersMockClient mockClient) => GoTrueClient(
url: 'http://localhost:9999',
httpClient: mockClient,
asyncStorage: TestAsyncStorage(),
);

test('listUsers() returns the metadata of a middle page', () async {
Expand Down
1 change: 1 addition & 0 deletions packages/gotrue/test/admin_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ void main() {
'Authorization': 'Bearer ${getServiceRoleToken(env)}',
'apikey': getServiceRoleToken(env),
},
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
42 changes: 40 additions & 2 deletions packages/gotrue/test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ void main() {
final newClient = GoTrueClient(
url: gotrueUrl,
headers: {'apikey': anonToken},
asyncStorage: TestAsyncStorage(),
);

expect(newClient.currentSession?.refreshToken ?? '', isEmpty);
Expand Down Expand Up @@ -314,6 +315,7 @@ void main() {
final newClient = GoTrueClient(
url: gotrueUrl,
headers: {'apikey': anonToken},
asyncStorage: TestAsyncStorage(),
);

expect(newClient.currentSession, isNull);
Expand Down Expand Up @@ -357,6 +359,7 @@ void main() {
final newClient = GoTrueClient(
url: gotrueUrl,
headers: {'apikey': anonToken},
asyncStorage: TestAsyncStorage(),
);

// Should fall back to _callRefreshToken and succeed.
Expand Down Expand Up @@ -408,6 +411,7 @@ void main() {
final newClient = GoTrueClient(
url: gotrueUrl,
headers: {'apikey': anonToken},
asyncStorage: TestAsyncStorage(),
);

expect(newClient.currentSession, isNull);
Expand Down Expand Up @@ -643,7 +647,11 @@ void main() {
late GoTrueClient client;

setUpAll(() {
client = GoTrueClient(url: gotrueUrl, httpClient: CustomHttpClient());
client = GoTrueClient(
url: gotrueUrl,
httpClient: CustomHttpClient(),
asyncStorage: TestAsyncStorage(),
);
});

test('signIn()', () async {
Expand Down Expand Up @@ -673,7 +681,11 @@ void main() {

setUpAll(() {
httpClient = RetryTestHttpClient();
client = GoTrueClient(url: gotrueUrl, httpClient: httpClient);
client = GoTrueClient(
url: gotrueUrl,
httpClient: httpClient,
asyncStorage: TestAsyncStorage(),
);
});

test('Session recovery succeeds after retries', () async {
Expand Down Expand Up @@ -931,6 +943,32 @@ void main() {
},
);
});

group('Constructing a client without an asyncStorage', () {
test('asserts when the pkce flow is used', () {
expect(
() => GoTrueClient(url: gotrueUrl, headers: {'apikey': anonToken}),
throwsA(
isA<AssertionError>().having(
(error) => error.message,
'message',
contains('You need to provide asyncStorage to perform pkce flow.'),
),
),
);
});
Comment thread
spydon marked this conversation as resolved.

test('is allowed when the implicit flow is used', () {
expect(
() => GoTrueClient(
url: gotrueUrl,
headers: {'apikey': anonToken},
flowType: AuthFlowType.implicit,
),
returnsNormally,
);
});
});
}

/// Reads the email-change confirmation link that GoTrue delivered to
Expand Down
3 changes: 3 additions & 0 deletions packages/gotrue/test/header_isolation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:gotrue/gotrue.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';

import 'utils.dart';

/// Records the headers of every request it receives and always answers with a
/// minimal user payload, so we can inspect what the client actually sent.
class _RecordingHttpClient extends BaseClient {
Expand Down Expand Up @@ -46,6 +48,7 @@ void main() {
url: 'http://localhost',
headers: {'apikey': 'anon-key'},
httpClient: http,
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
36 changes: 36 additions & 0 deletions packages/gotrue/test/memory_async_storage_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:gotrue/gotrue.dart';
import 'package:test/test.dart';

void main() {
late MemoryGotrueAsyncStorage storage;

setUp(() {
storage = MemoryGotrueAsyncStorage();
});

test('returns null for a key that was never stored', () async {
expect(await storage.getItem(key: 'code-verifier'), isNull);
});

test('returns the value that was stored last', () async {
await storage.setItem(key: 'code-verifier', value: 'first');
await storage.setItem(key: 'code-verifier', value: 'second');

expect(await storage.getItem(key: 'code-verifier'), 'second');
});

test('forgets a removed key', () async {
await storage.setItem(key: 'code-verifier', value: 'value');
await storage.removeItem(key: 'code-verifier');

expect(await storage.getItem(key: 'code-verifier'), isNull);
});

test('keeps the entries of two instances apart', () async {
await storage.setItem(key: 'code-verifier', value: 'value');

final other = MemoryGotrueAsyncStorage();

expect(await other.getItem(key: 'code-verifier'), isNull);
});
}
3 changes: 3 additions & 0 deletions packages/gotrue/test/mfa_enroll_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:gotrue/gotrue.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';

import 'utils.dart';

/// Records the body of every request it receives and answers with a minimal
/// enroll payload, so we can inspect what the client actually sent without a
/// live server.
Expand Down Expand Up @@ -47,6 +49,7 @@ void main() {
url: 'http://localhost',
headers: {'apikey': 'anon-key'},
httpClient: http,
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ void main() {
'apikey': serviceRoleToken,
'x-forwarded-for': '127.0.0.1',
},
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
1 change: 1 addition & 0 deletions packages/gotrue/test/src/gotrue_admin_mfa_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ void main() {
'apikey': serviceRoleToken,
'x-forwarded-for': '127.0.0.1',
},
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ void main() {
'apikey': serviceRoleToken,
'x-forwarded-for': '127.0.0.1',
},
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
1 change: 1 addition & 0 deletions packages/gotrue/test/src/gotrue_mfa_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ void main() {
'apikey': anonToken,
'x-forwarded-for': '127.0.0.1',
},
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
1 change: 1 addition & 0 deletions packages/gotrue/test/src/gotrue_oauth_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ class GotrueOauthApiFixture {
'apikey': _serviceRoleToken,
'x-forwarded-for': '127.0.0.1',
},
asyncStorage: TestAsyncStorage(),
);
}

Expand Down
18 changes: 1 addition & 17 deletions packages/gotrue/test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,4 @@ const sessionDataUserId = '4d2583da-8de4-49d3-9cd1-37a9a74f55bd';
return (accessToken: accessToken, sessionString: sessionString);
}

class TestAsyncStorage extends GotrueAsyncStorage {
final Map<String, String> _map = {};
@override
Future<String?> getItem({required String key}) async {
return _map[key];
}

@override
Future<void> removeItem({required String key}) async {
_map.remove(key);
}

@override
Future<void> setItem({required String key, required String value}) async {
_map[key] = value;
}
}
class TestAsyncStorage extends MemoryGotrueAsyncStorage {}
12 changes: 8 additions & 4 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ import 'trace_http_client.dart';
/// Pass an instance of `YAJsonIsolate` to [isolate] to use your own persisted
/// isolate instance. A new instance will be created if [isolate] is omitted.
///
/// Pass an instance of `GotrueAsyncStorage` to the `pkceAsyncStorage` field of
/// [authOptions] and set its `authFlowType` field to `AuthFlowType.pkce` in
/// order to perform auth actions with pkce flow.
/// The pkce flow is used by default. It stores its code verifiers in
/// `AuthClientOptions.pkceAsyncStorage`, which falls back to an in-memory
/// storage when none is given.
/// {@endtemplate}
class SupabaseClient {
final String _supabaseKey;
Expand Down Expand Up @@ -331,7 +331,11 @@ class SupabaseClient {
headers: authHeaders,
autoRefreshToken: autoRefreshToken,
httpClient: _gotrueHttpClient,
asyncStorage: gotrueAsyncStorage,
asyncStorage:
gotrueAsyncStorage ??
(authFlowType == AuthFlowType.pkce
? MemoryGotrueAsyncStorage()
: null),
flowType: authFlowType,
);
}
Expand Down
7 changes: 7 additions & 0 deletions packages/supabase/lib/src/supabase_client_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ class PostgrestClientOptions {

class AuthClientOptions {
final bool autoRefreshToken;

/// Storage for the code verifiers of the pkce flow.
///
/// Defaults to a [MemoryGotrueAsyncStorage], which only supports flows that
/// start and complete within the same process. Pass a persistent
/// implementation when the code is exchanged after a restart, which is what
/// `supabase_flutter` does with shared preferences.
final GotrueAsyncStorage? pkceAsyncStorage;
final AuthFlowType authFlowType;

Expand Down
11 changes: 11 additions & 0 deletions packages/supabase/test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ void main() {
});

group('auth', () {
test('the pkce flow works without passing a pkceAsyncStorage', () async {
final supabase = SupabaseClient('http://localhost:1', 'supabaseKey');
addTearDown(supabase.dispose);

final response = await supabase.auth.getOAuthSignInUrl(
provider: OAuthProvider.github,
);

expect(response.url, contains('code_challenge='));
});

test('properly set Authorization header', () async {
final (:sessionString, :accessToken) = getSessionData(
DateTime.now().add(Duration(hours: 1)),
Expand Down
Loading
Loading