Skip to content

Commit c68d44d

Browse files
authored
feat: Add third-party auth support (#999)
* feat: add accessToken option * delete empty line * fix failing test * properly return the access token in _getAccessToken * make sure auth isn't called internally when accessToken is used * add access token parameter to Supabase.initialize()
1 parent 7da6856 commit c68d44d

File tree

5 files changed

+80
-35
lines changed

5 files changed

+80
-35
lines changed

packages/supabase/lib/src/auth_http_client.dart

+5-21
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,16 @@
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;
7-
final String _supabaseKey;
85

9-
AuthHttpClient(this._supabaseKey, this._inner, this._auth);
6+
final String _supabaseKey;
7+
final Future<String?> Function() _getAccessToken;
8+
AuthHttpClient(this._supabaseKey, this._inner, this._getAccessToken);
109

1110
@override
1211
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;
12+
final accessToken = await _getAccessToken();
13+
final authBearer = accessToken ?? _supabaseKey;
3014

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

packages/supabase/lib/src/supabase_client.dart

+60-13
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;
@@ -52,8 +59,9 @@ class SupabaseClient {
5259
late final SupabaseStorageClient storage;
5360
late final RealtimeClient realtime;
5461
late final PostgrestClient rest;
55-
late StreamSubscription<AuthState> _authStateSubscription;
62+
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();
@@ -83,13 +91,15 @@ class SupabaseClient {
8391
..clear()
8492
..addAll(_headers);
8593

86-
auth.headers
87-
..clear()
88-
..addAll({
89-
...Constants.defaultHeaders,
90-
..._getAuthHeaders(),
91-
...headers,
92-
});
94+
if (accessToken == null) {
95+
auth.headers
96+
..clear()
97+
..addAll({
98+
...Constants.defaultHeaders,
99+
..._getAuthHeaders(),
100+
...headers,
101+
});
102+
}
93103

94104
// To apply the new headers in the realtime client,
95105
// manually unsubscribe and resubscribe to all channels.
@@ -106,6 +116,7 @@ class SupabaseClient {
106116
AuthClientOptions authOptions = const AuthClientOptions(),
107117
StorageClientOptions storageOptions = const StorageClientOptions(),
108118
RealtimeClientOptions realtimeClientOptions = const RealtimeClientOptions(),
119+
this.accessToken,
109120
Map<String, String>? headers,
110121
Client? httpClient,
111122
YAJsonIsolate? isolate,
@@ -122,18 +133,30 @@ class SupabaseClient {
122133
},
123134
_httpClient = httpClient,
124135
_isolate = isolate ?? (YAJsonIsolate()..initialize()) {
125-
auth = _initSupabaseAuthClient(
136+
_authInstance = _initSupabaseAuthClient(
126137
autoRefreshToken: authOptions.autoRefreshToken,
127138
gotrueAsyncStorage: authOptions.pkceAsyncStorage,
128139
authFlowType: authOptions.authFlowType,
129140
);
130141
_authHttpClient =
131-
AuthHttpClient(_supabaseKey, httpClient ?? Client(), auth);
142+
AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken);
132143
rest = _initRestClient();
133144
functions = _initFunctionsClient();
134145
storage = _initStorageClient(storageOptions.retryAttempts);
135146
realtime = _initRealtimeClient(options: realtimeClientOptions);
136-
_listenForAuthEvents();
147+
if (accessToken == null) {
148+
_listenForAuthEvents();
149+
}
150+
}
151+
152+
GoTrueClient get auth {
153+
if (accessToken == null) {
154+
return _authInstance;
155+
} else {
156+
throw AuthException(
157+
'Supabase Client is configured with the accessToken option, accessing supabase.auth is not possible.',
158+
);
159+
}
137160
}
138161

139162
/// Perform a table operation.
@@ -200,8 +223,32 @@ class SupabaseClient {
200223
return realtime.removeAllChannels();
201224
}
202225

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

packages/supabase/test/client_test.dart

+10
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/lib/src/supabase.dart

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class Supabase {
7575
PostgrestClientOptions postgrestOptions = const PostgrestClientOptions(),
7676
StorageClientOptions storageOptions = const StorageClientOptions(),
7777
FlutterAuthClientOptions authOptions = const FlutterAuthClientOptions(),
78+
Future<String> Function()? accessToken,
7879
bool? debug,
7980
}) async {
8081
assert(
@@ -103,6 +104,7 @@ class Supabase {
103104
authOptions: authOptions,
104105
postgrestOptions: postgrestOptions,
105106
storageOptions: storageOptions,
107+
accessToken: accessToken,
106108
);
107109
_instance._debugEnable = debug ?? kDebugMode;
108110
_instance.log('***** Supabase init completed $_instance');
@@ -154,6 +156,7 @@ class Supabase {
154156
required PostgrestClientOptions postgrestOptions,
155157
required StorageClientOptions storageOptions,
156158
required AuthClientOptions authOptions,
159+
required Future<String> Function()? accessToken,
157160
}) {
158161
final headers = {
159162
...Constants.defaultHeaders,
@@ -168,6 +171,7 @@ class Supabase {
168171
postgrestOptions: postgrestOptions,
169172
storageOptions: storageOptions,
170173
authOptions: authOptions,
174+
accessToken: accessToken,
171175
);
172176
_initialized = true;
173177
}

packages/supabase_flutter/test/supabase_flutter_test.dart

+1-1
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)