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
8 changes: 5 additions & 3 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ 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 `AuthAsyncStorage` 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 and keeps its code verifiers in the
/// `AuthAsyncStorage` passed to the `pkceAsyncStorage` field of [authOptions].
/// Pass a `MemoryAuthAsyncStorage` when the flow starts and completes within
/// the same process, or a persistent implementation when the code is exchanged
/// after a restart.
/// {@endtemplate}
class SupabaseClient {
final String _supabaseKey;
Expand Down
8 changes: 8 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,14 @@ class PostgrestClientOptions {

class AuthClientOptions {
final bool autoRefreshToken;

/// Storage for the code verifiers of the pkce flow, required when
/// [authFlowType] is [AuthFlowType.pkce].
///
/// Pass a [MemoryAuthAsyncStorage] when the flow starts and completes within
/// the same process. A persistent implementation is needed when the code is
/// exchanged after a restart, which is what `supabase_flutter` does with
/// shared preferences.
final AuthAsyncStorage? pkceAsyncStorage;
final AuthFlowType authFlowType;

Expand Down
58 changes: 57 additions & 1 deletion packages/supabase/test/client_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'dart:async';
import 'dart:io';

import 'package:supabase/supabase.dart';
import 'package:supabase/src/supabase_client.dart' as real;
import 'package:supabase/supabase.dart' hide SupabaseClient;
import 'package:supabase_common/supabase_common.dart';
import 'package:test/test.dart';
import 'package:yet_another_json_isolate/yet_another_json_isolate.dart';
Expand Down Expand Up @@ -183,6 +184,36 @@ void main() {
});

group('auth', () {
test('the pkce flow asserts when no pkceAsyncStorage is given', () {
expect(
() => real.SupabaseClient('http://localhost:1', 'supabaseKey'),
throwsA(
isA<AssertionError>().having(
(error) => error.message,
'message',
contains('You need to provide asyncStorage to perform pkce flow.'),
),
),
);
});

test('the pkce flow works with a MemoryAuthAsyncStorage', () async {
final supabase = real.SupabaseClient(
'http://localhost:1',
'supabaseKey',
authOptions: AuthClientOptions(
pkceAsyncStorage: MemoryAuthAsyncStorage(),
),
);
addTearDown(supabase.dispose);

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

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

test('properly set Authorization header', () async {
final (:sessionString, :accessToken) = getSessionData(
DateTime.now().add(Duration(hours: 1)),
Expand Down Expand Up @@ -469,3 +500,28 @@ void main() {
});
});
}

/// A [real.SupabaseClient] that falls back to an in-memory pkce storage, so the
/// tests below do not have to pass one at every construction site.
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 ?? MemoryAuthAsyncStorage(),
authFlowType: authOptions.authFlowType,
),
Comment thread
spydon marked this conversation as resolved.
);
}
11 changes: 11 additions & 0 deletions packages/supabase/test/mock_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,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;
});
Expand Down Expand Up @@ -848,6 +850,9 @@ 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']);
Expand Down Expand Up @@ -879,6 +884,9 @@ 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
Expand All @@ -897,6 +905,9 @@ 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
Expand Down
3 changes: 3 additions & 0 deletions packages/supabase/test/realtime_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ void main() {
supabase = SupabaseClient(
'http://${mockServer.address.host}:${mockServer.port}',
'supabaseKey',
authOptions: const AuthClientOptions(
authFlowType: AuthFlowType.implicit,
),
);

channel = supabase.channel('realtime');
Expand Down
3 changes: 3 additions & 0 deletions packages/supabase/test/stream_filter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:web_socket_channel/io.dart';

import 'package:supabase_common/testing.dart';

import 'utils.dart';

/// Asserts how the filters of `stream()` are sent to the realtime server and to
/// PostgREST, without needing either of them to be running.
void main() {
Expand Down Expand Up @@ -36,6 +38,7 @@ void main() {
supabase = SupabaseClient(
'http://${InternetAddress.loopbackIPv4.address}:${mockServer.port}',
localStackServiceRoleKey,
authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()),
);
});

Expand Down
9 changes: 7 additions & 2 deletions packages/supabase/test/stream_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:test/test.dart';

import 'package:supabase_common/testing.dart';

import 'utils.dart';

late SupabaseClient _supabase;

void main() {
Expand Down Expand Up @@ -541,8 +543,11 @@ void main() {
const _streamTimeout = Duration(seconds: 10);
const _warmUpPrefix = 'warm_up_';

SupabaseClient _createClient() =>
SupabaseClient(localStackUrl, localStackServiceRoleKey);
SupabaseClient _createClient() => SupabaseClient(
localStackUrl,
localStackServiceRoleKey,
authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()),
);

/// Listens to [stream] and asserts that it emits [expectedSnapshots] in order,
/// where every snapshot is the result of [project] applied to the emitted rows.
Expand Down
4 changes: 4 additions & 0 deletions packages/supabase/test/trace_propagation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:supabase/src/trace_http_client.dart';
import 'package:supabase/supabase.dart';
import 'package:test/test.dart';

import 'utils.dart';

const _sampledTraceparent =
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01';
const _unsampledTraceparent =
Expand Down Expand Up @@ -132,6 +134,7 @@ void main() {
_supabaseUrl,
'anon-key',
tracePropagationOptions: optionsWith(() => context),
authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()),
httpClient: MockClient((request) async {
restRequest = request;
return Response(
Expand All @@ -156,6 +159,7 @@ void main() {
final supabase = SupabaseClient(
_supabaseUrl,
'anon-key',
authOptions: AuthClientOptions(pkceAsyncStorage: TestAsyncStorage()),
httpClient: MockClient((request) async {
restRequest = request;
return Response(
Expand Down
4 changes: 4 additions & 0 deletions packages/supabase/test/utils.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,3 +28,5 @@ import 'dart:convert';
'at":"2023-04-01T08:35:05.226938Z"}}';
return (accessToken: accessToken, sessionString: sessionString);
}

class TestAsyncStorage extends MemoryAuthAsyncStorage {}
3 changes: 3 additions & 0 deletions packages/supabase_auth/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: MemoryAuthAsyncStorage(),
);

try {
Expand Down
11 changes: 9 additions & 2 deletions packages/supabase_auth/lib/src/auth_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 [MemoryAuthAsyncStorage] 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 @@ -157,7 +158,13 @@ class AuthClient {
Client? httpClient,
AuthAsyncStorage? asyncStorage,
AuthFlowType flowType = AuthFlowType.pkce,
}) : _url = url ?? AuthConstants.defaultAuthUrl,
}) : assert(
flowType != AuthFlowType.pkce || asyncStorage != null,
'You need to provide asyncStorage to perform pkce flow. Pass a '
'MemoryAuthAsyncStorage 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 ?? AuthConstants.defaultAuthUrl,
_headers = {...AuthConstants.defaultHeaders, ...?headers},
_httpClient = httpClient,
_asyncStorage = asyncStorage,
Expand Down
23 changes: 23 additions & 0 deletions packages/supabase_auth/lib/src/types/auth_async_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,26 @@ abstract class AuthAsyncStorage {
/// Removes an item asynchronously from the storage for the given key.
Future<void> removeItem({required String key});
}

/// A [AuthAsyncStorage] 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 MemoryAuthAsyncStorage extends AuthAsyncStorage {
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/supabase_auth/test/admin_delete_user_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:supabase_auth/supabase_auth.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 = AuthClient(
url: 'http://localhost:9999',
httpClient: httpClient,
asyncStorage: TestAsyncStorage(),
);
});

Expand Down
1 change: 1 addition & 0 deletions packages/supabase_auth/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
Loading
Loading