-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth): Add support for Supabase IDP (#192)
Adds support for using Supabase as an external auth provider.
- Loading branch information
Showing
11 changed files
with
242 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
packages/celest/lib/src/runtime/auth/supabase/supabase_auth_middleware.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import 'dart:async'; | ||
import 'dart:typed_data'; | ||
|
||
import 'package:celest/src/runtime/auth/auth_middleware.dart'; | ||
import 'package:celest/src/runtime/auth/supabase/supabase_token_verifier.dart'; | ||
import 'package:celest_core/celest_core.dart' show User; | ||
import 'package:collection/collection.dart'; | ||
import 'package:shelf/shelf.dart' show Request; | ||
|
||
/// {@template celest.runtime.supabase_auth_middleware} | ||
/// A Celest authentication middleware which authenticates users with Supabase | ||
/// as the external auth provider. | ||
/// {@endtemplate} | ||
final class SupabaseAuthMiddleware extends AuthMiddleware { | ||
/// {@macro celest.runtime.supabase_auth_middleware} | ||
SupabaseAuthMiddleware({ | ||
required String url, | ||
Uint8List? jwtSecret, | ||
this.required = false, | ||
}) : _tokenVerifier = SupabaseTokenVerifier(url: url, jwtSecret: jwtSecret); | ||
|
||
final SupabaseTokenVerifier _tokenVerifier; | ||
|
||
@override | ||
final bool required; | ||
|
||
@override | ||
Future<User?> authenticate(Request request) async { | ||
final token = switch (request.headers['Authorization']?.split(' ')) { | ||
[final type, final token] when equalsIgnoreAsciiCase(type, 'bearer') => | ||
token, | ||
_ => null, | ||
}; | ||
if (token == null) { | ||
return null; | ||
} | ||
return _tokenVerifier.verify(token); | ||
} | ||
} |
112 changes: 112 additions & 0 deletions
112
packages/celest/lib/src/runtime/auth/supabase/supabase_token_verifier.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import 'dart:convert'; | ||
import 'dart:typed_data'; | ||
|
||
import 'package:celest/celest.dart'; | ||
import 'package:celest/src/runtime/auth/jwt/base64_raw_url.dart'; | ||
import 'package:crypto_keys/crypto_keys.dart' | ||
show AlgorithmIdentifier, Signature, SymmetricKey; | ||
|
||
/// {@template celest.runtime.supabase_token_verifier} | ||
/// Verifies access tokens issued by Supabase by issuing a request to the | ||
/// Supabase `/auth/v1/user` endpoint. | ||
/// {@endtemplate} | ||
final class SupabaseTokenVerifier { | ||
/// Creates an instance of [SupabaseTokenVerifier] for the Supabase project | ||
/// at the given [url]. | ||
SupabaseTokenVerifier({ | ||
required String url, | ||
Uint8List? jwtSecret, | ||
}) : _userUri = Uri.parse(url).resolve('./auth/v1/user'), | ||
_jwtSecret = switch (jwtSecret) { | ||
final secret? => SymmetricKey(keyValue: secret), | ||
_ => null, | ||
}; | ||
|
||
final Uri _userUri; | ||
final SymmetricKey? _jwtSecret; | ||
static final _decoder = utf8.decoder.fuse(json.decoder); | ||
|
||
Object? _decodeJwtPart(String part) { | ||
final decoded = base64RawUrl.decode(part); | ||
return _decoder.convert(decoded); | ||
} | ||
|
||
User? _verifyLocally(String token) { | ||
final jwtSecret = _jwtSecret; | ||
if (jwtSecret == null) { | ||
return null; | ||
} | ||
final (header, payload, signature) = switch (token.split('.')) { | ||
[final header, final payload, final signature] => ( | ||
header, | ||
payload, | ||
signature | ||
), | ||
_ => throw const FormatException('Invalid JWT format'), | ||
}; | ||
final alg = switch (_decodeJwtPart(header)) { | ||
{ | ||
'alg': final String alg, | ||
} => | ||
AlgorithmIdentifier.getByJwaName(alg), | ||
_ => throw const FormatException('Invalid JWT header'), | ||
}; | ||
if (alg == null || alg.name != 'sig/HMAC/SHA-256') { | ||
throw FormatException('Invalid JWT algorithm: $alg'); | ||
} | ||
final data = utf8.encode('$header.$payload'); | ||
final verifier = jwtSecret.createVerifier(alg); | ||
final validSignature = verifier.verify( | ||
data, | ||
Signature(base64RawUrl.decode(signature)), | ||
); | ||
if (!validSignature) { | ||
throw const CloudException.unauthorized('Invalid JWT signature'); | ||
} | ||
final claims = switch (_decodeJwtPart(payload)) { | ||
final Map<String, Object?> payload => payload, | ||
_ => throw const FormatException('Invalid JWT payload'), | ||
}; | ||
return User( | ||
userId: claims['sub'] as String, | ||
email: claims['email'] as String?, | ||
); | ||
} | ||
|
||
/// Verifies the given [token] and returns the authenticated user. | ||
Future<User> verify(String token) async { | ||
if (_verifyLocally(token) case final user?) { | ||
return user; | ||
} | ||
context.logger.fine('Local Supabase verification skipped'); | ||
final response = await context.httpClient.get( | ||
_userUri, | ||
headers: { | ||
'Accept': 'application/json', | ||
'Authorization': 'Bearer $token', | ||
}, | ||
); | ||
if (response.statusCode != 200) { | ||
throw CloudException.fromHttpResponse(response); | ||
} | ||
try { | ||
final jsonResp = jsonDecode(response.body) as Map<String, Object?>; | ||
if (jsonResp case {'user': final Map<String, Object?> user}) { | ||
final userId = user['id'] as String; | ||
final email = user['email'] as String?; | ||
final emailVerified = | ||
(user['email_confirmed_at'] ?? user['confirmed_at']) != null; | ||
return User( | ||
userId: userId, | ||
email: email, | ||
emailVerified: emailVerified, | ||
); | ||
} | ||
// Shouldn't ever happened for a well-formed response. | ||
throw FormatException('Bad user JSON: $jsonResp'); | ||
} on Object catch (e, st) { | ||
context.logger.severe('Failed to parse Supabase response', e, st); | ||
rethrow; | ||
} | ||
} | ||
} |
Oops, something went wrong.