Skip to content

Commit fb5074a

Browse files
committed
feat: add accessToken option
1 parent 7da6856 commit fb5074a

File tree

4 files changed

+66
-25
lines changed

4 files changed

+66
-25
lines changed

packages/supabase/lib/src/auth_http_client.dart

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,17 @@
11
import 'package:http/http.dart';
2-
import 'package:supabase/supabase.dart';
32

43
class AuthHttpClient extends BaseClient {
54
final Client _inner;
6-
final GoTrueClient _auth;
5+
76
final String _supabaseKey;
7+
final Future<String?> Function() _getAccessToken;
88

9-
AuthHttpClient(this._supabaseKey, this._inner, this._auth);
9+
AuthHttpClient(this._supabaseKey, this._inner, this._getAccessToken);
1010

1111
@override
1212
Future<StreamedResponse> send(BaseRequest request) async {
13-
if (_auth.currentSession?.isExpired ?? false) {
14-
try {
15-
await _auth.refreshSession();
16-
} catch (error) {
17-
final expiresAt = _auth.currentSession?.expiresAt;
18-
if (expiresAt != null) {
19-
// Failed to refresh the token.
20-
final isExpiredWithoutMargin = DateTime.now()
21-
.isAfter(DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000));
22-
if (isExpiredWithoutMargin) {
23-
// Throw the error instead of making an API request with an expired token.
24-
rethrow;
25-
}
26-
}
27-
}
28-
}
29-
final authBearer = _auth.currentSession?.accessToken ?? _supabaseKey;
13+
final accessToken = await _getAccessToken();
14+
final authBearer = accessToken ?? _supabaseKey;
3015

3116
request.headers.putIfAbsent("Authorization", () => 'Bearer $authBearer');
3217
request.headers.putIfAbsent("apikey", () => _supabaseKey);

packages/supabase/lib/src/supabase_client.dart

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import 'counter.dart';
2424
///
2525
/// [realtimeClientOptions] specifies different options you can pass to `RealtimeClient`.
2626
///
27+
/// [accessToken] Optional function for using a third-party authentication system with Supabase.
28+
/// The function should return an access token or ID token (JWT) by obtaining
29+
/// it from the third-party auth client library. Note that this function may be
30+
/// called concurrently and many times. Use memoization and locking techniques
31+
/// if this is not supported by the client libraries. When set, the `auth`
32+
/// namespace of the Supabase client cannot be used.
33+
///
2734
/// Pass an instance of `YAJsonIsolate` to [isolate] to use your own persisted
2835
/// isolate instance. A new instance will be created if [isolate] is omitted.
2936
///
@@ -43,7 +50,7 @@ class SupabaseClient {
4350
final Client? _httpClient;
4451
late final Client _authHttpClient;
4552

46-
late final GoTrueClient auth;
53+
late final GoTrueClient _authInstance;
4754

4855
/// Supabase Functions allows you to deploy and invoke edge functions.
4956
late final FunctionsClient functions;
@@ -54,6 +61,7 @@ class SupabaseClient {
5461
late final PostgrestClient rest;
5562
late StreamSubscription<AuthState> _authStateSubscription;
5663
late final YAJsonIsolate _isolate;
64+
final Future<String> Function()? accessToken;
5765

5866
/// Increment ID of the stream to create different realtime topic for each stream
5967
final _incrementId = Counter();
@@ -106,6 +114,7 @@ class SupabaseClient {
106114
AuthClientOptions authOptions = const AuthClientOptions(),
107115
StorageClientOptions storageOptions = const StorageClientOptions(),
108116
RealtimeClientOptions realtimeClientOptions = const RealtimeClientOptions(),
117+
this.accessToken,
109118
Map<String, String>? headers,
110119
Client? httpClient,
111120
YAJsonIsolate? isolate,
@@ -122,18 +131,30 @@ class SupabaseClient {
122131
},
123132
_httpClient = httpClient,
124133
_isolate = isolate ?? (YAJsonIsolate()..initialize()) {
125-
auth = _initSupabaseAuthClient(
134+
_authInstance = _initSupabaseAuthClient(
126135
autoRefreshToken: authOptions.autoRefreshToken,
127136
gotrueAsyncStorage: authOptions.pkceAsyncStorage,
128137
authFlowType: authOptions.authFlowType,
129138
);
130139
_authHttpClient =
131-
AuthHttpClient(_supabaseKey, httpClient ?? Client(), auth);
140+
AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken);
132141
rest = _initRestClient();
133142
functions = _initFunctionsClient();
134143
storage = _initStorageClient(storageOptions.retryAttempts);
135144
realtime = _initRealtimeClient(options: realtimeClientOptions);
136-
_listenForAuthEvents();
145+
if (accessToken != null) {
146+
_listenForAuthEvents();
147+
}
148+
}
149+
150+
GoTrueClient get auth {
151+
if (accessToken == null) {
152+
return _authInstance;
153+
} else {
154+
throw AuthException(
155+
'Supabase Client is configured with the accessToken option, accessing supabase.auth is not possible.',
156+
);
157+
}
137158
}
138159

139160
/// Perform a table operation.
@@ -200,6 +221,31 @@ class SupabaseClient {
200221
return realtime.removeAllChannels();
201222
}
202223

224+
Future<String?> _getAccessToken() async {
225+
if (accessToken != null) {
226+
return await accessToken!();
227+
}
228+
229+
if (_authInstance.currentSession?.isExpired ?? false) {
230+
try {
231+
await _authInstance.refreshSession();
232+
return _authInstance.currentSession?.accessToken;
233+
} catch (error) {
234+
final expiresAt = _authInstance.currentSession?.expiresAt;
235+
if (expiresAt != null) {
236+
// Failed to refresh the token.
237+
final isExpiredWithoutMargin = DateTime.now()
238+
.isAfter(DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000));
239+
if (isExpiredWithoutMargin) {
240+
// Throw the error instead of making an API request with an expired token.
241+
rethrow;
242+
}
243+
}
244+
}
245+
}
246+
return null;
247+
}
248+
203249
Future<void> dispose() async {
204250
await _authStateSubscription.cancel();
205251
await _isolate.dispose();

packages/supabase/test/client_test.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ void main() {
186186

187187
mockServer.close();
188188
});
189+
190+
test('create a client with third-party auth accessToken', () async {
191+
final supabase = SupabaseClient('URL', 'KEY', accessToken: () async {
192+
return 'jwt';
193+
});
194+
expect(
195+
supabase.auth.currentUser,
196+
throwsA(AuthException(
197+
'Supabase Client is configured with the accessToken option, accessing supabase.auth is not possible.')));
198+
});
189199
});
190200

191201
group('Custom Header', () {

packages/supabase_flutter/test/supabase_flutter_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ void main() {
9999
/// Check if the current version of AppLinks uses an explicit call to get
100100
/// the initial link. This is only the case before version 6.0.0, where we
101101
/// can find the getInitialAppLink function.
102-
///
102+
///
103103
/// CI pipeline is set so that it tests both app_links newer and older than v6.0.0
104104
bool appLinksExposesInitialLinkInStream() {
105105
try {

0 commit comments

Comments
 (0)