Skip to content

Commit

Permalink
refactor!: many auth breaking changes (#636)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vinzent03 committed Sep 20, 2023
1 parent db5ec82 commit 7782a58
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 183 deletions.
2 changes: 1 addition & 1 deletion packages/gotrue/lib/gotrue.dart
Expand Up @@ -9,8 +9,8 @@ export 'src/types/auth_response.dart' hide ToSnakeCase;
export 'src/types/auth_state.dart';
export 'src/types/gotrue_async_storage.dart';
export 'src/types/mfa.dart';
export 'src/types/o_auth_provider.dart';
export 'src/types/oauth_flow_type.dart';
export 'src/types/provider.dart';
export 'src/types/session.dart';
export 'src/types/user.dart';
export 'src/types/user_attributes.dart';
1 change: 1 addition & 0 deletions packages/gotrue/lib/src/constants.dart
Expand Up @@ -17,6 +17,7 @@ class Constants {
}

enum AuthChangeEvent {
initialSession,
passwordRecovery,
signedIn,
signedOut,
Expand Down
64 changes: 34 additions & 30 deletions packages/gotrue/lib/src/gotrue_client.dart
Expand Up @@ -28,7 +28,7 @@ part 'gotrue_mfa_api.dart';
///
/// [asyncStorage] local storage to store pkce code verifiers. Required when using the pkce flow.
///
/// Set [flowType] to `AuthFlowType.pkce` to perform pkce auth flow.
/// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow.
/// /// {@endtemplate}
class GoTrueClient {
/// Namespace for the GoTrue API methods.
Expand Down Expand Up @@ -93,7 +93,7 @@ class GoTrueClient {
bool? autoRefreshToken,
Client? httpClient,
GotrueAsyncStorage? asyncStorage,
AuthFlowType flowType = AuthFlowType.implicit,
AuthFlowType flowType = AuthFlowType.pkce,
}) : _url = url ?? Constants.defaultGotrueUrl,
_headers = headers ?? {},
_httpClient = httpClient,
Expand Down Expand Up @@ -205,7 +205,7 @@ class GoTrueClient {
final session = authResponse.session;
if (session != null) {
_saveSession(session);
_notifyAllSubscribers(AuthChangeEvent.signedIn);
notifyAllSubscribers(AuthChangeEvent.signedIn);
}

return authResponse;
Expand Down Expand Up @@ -260,14 +260,14 @@ class GoTrueClient {

if (authResponse.session?.accessToken != null) {
_saveSession(authResponse.session!);
_notifyAllSubscribers(AuthChangeEvent.signedIn);
notifyAllSubscribers(AuthChangeEvent.signedIn);
}
return authResponse;
}

/// Generates a link to log in an user via a third-party provider.
Future<OAuthResponse> getOAuthSignInUrl({
required Provider provider,
required OAuthProvider provider,
String? redirectTo,
String? scopes,
Map<String, String>? queryParams,
Expand Down Expand Up @@ -312,15 +312,15 @@ class GoTrueClient {
final session = authResponse.session;
if (session != null) {
_saveSession(session);
_notifyAllSubscribers(AuthChangeEvent.signedIn);
notifyAllSubscribers(AuthChangeEvent.signedIn);
}

return authResponse;
}

/// Allows signing in with an ID token issued by certain supported providers.
/// The [idToken] is verified for validity and a new session is established.
/// This method of signing in only supports [Provider.google] or [Provider.apple].
/// This method of signing in only supports [OAuthProvider.google] or [OAuthProvider.apple].
///
/// If the ID token contains an `at_hash` claim, then [accessToken] must be
/// provided to compare its hash with the value in the ID token.
Expand All @@ -334,17 +334,17 @@ class GoTrueClient {
/// This method is experimental.
@experimental
Future<AuthResponse> signInWithIdToken({
required Provider provider,
required OAuthProvider provider,
required String idToken,
String? accessToken,
String? nonce,
String? captchaToken,
}) async {
_removeSession();

if (provider != Provider.google && provider != Provider.apple) {
if (provider != OAuthProvider.google && provider != OAuthProvider.apple) {
throw AuthException('Provider must either be '
'${Provider.google.name} or ${Provider.apple.name}.');
'${OAuthProvider.google.name} or ${OAuthProvider.apple.name}.');
}

final response = await _fetch.request(
Expand Down Expand Up @@ -372,7 +372,7 @@ class GoTrueClient {
}

_saveSession(authResponse.session!);
_notifyAllSubscribers(AuthChangeEvent.signedIn);
notifyAllSubscribers(AuthChangeEvent.signedIn);

return authResponse;
}
Expand Down Expand Up @@ -493,7 +493,7 @@ class GoTrueClient {
}

_saveSession(authResponse.session!);
_notifyAllSubscribers(AuthChangeEvent.signedIn);
notifyAllSubscribers(AuthChangeEvent.signedIn);

return authResponse;
}
Expand Down Expand Up @@ -603,7 +603,7 @@ class GoTrueClient {

_currentUser = userResponse.user;
_currentSession = currentSession?.copyWith(user: userResponse.user);
_notifyAllSubscribers(AuthChangeEvent.userUpdated);
notifyAllSubscribers(AuthChangeEvent.userUpdated);

return userResponse;
}
Expand Down Expand Up @@ -636,7 +636,7 @@ class GoTrueClient {

if (storeSession == true) {
_saveSession(session);
_notifyAllSubscribers(AuthChangeEvent.signedIn);
notifyAllSubscribers(AuthChangeEvent.signedIn);
}

return AuthSessionUrlResponse(session: session, redirectType: null);
Expand All @@ -652,7 +652,7 @@ class GoTrueClient {

final errorDescription = url.queryParameters['error_description'];
if (errorDescription != null) {
throw _notifyException(AuthException(errorDescription));
throw AuthException(errorDescription);
}

final accessToken = url.queryParameters['access_token'];
Expand All @@ -663,16 +663,16 @@ class GoTrueClient {
final providerRefreshToken = url.queryParameters['provider_refresh_token'];

if (accessToken == null) {
throw _notifyException(AuthException('No access_token detected.'));
throw AuthException('No access_token detected.');
}
if (expiresIn == null) {
throw _notifyException(AuthException('No expires_in detected.'));
throw AuthException('No expires_in detected.');
}
if (refreshToken == null) {
throw _notifyException(AuthException('No refresh_token detected.'));
throw AuthException('No refresh_token detected.');
}
if (tokenType == null) {
throw _notifyException(AuthException('No token_type detected.'));
throw AuthException('No token_type detected.');
}

final headers = {..._headers};
Expand All @@ -682,7 +682,7 @@ class GoTrueClient {
options: options);
final user = UserResponse.fromJson(response).user;
if (user == null) {
throw _notifyException(AuthException('No user found.'));
throw AuthException('No user found.');
}

final session = Session(
Expand All @@ -700,9 +700,9 @@ class GoTrueClient {
if (storeSession == true) {
_saveSession(session);
if (redirectType == 'recovery') {
_notifyAllSubscribers(AuthChangeEvent.passwordRecovery);
notifyAllSubscribers(AuthChangeEvent.passwordRecovery);
} else {
_notifyAllSubscribers(AuthChangeEvent.signedIn);
notifyAllSubscribers(AuthChangeEvent.signedIn);
}
}

Expand All @@ -721,7 +721,7 @@ class GoTrueClient {
_removeSession();
await _asyncStorage?.removeItem(
key: '${Constants.defaultStorageKey}-code-verifier');
_notifyAllSubscribers(AuthChangeEvent.signedOut);
notifyAllSubscribers(AuthChangeEvent.signedOut);
}

if (accessToken != null) {
Expand Down Expand Up @@ -777,7 +777,7 @@ class GoTrueClient {
Future<AuthResponse> recoverSession(String jsonStr) async {
final session = Session.fromJson(json.decode(jsonStr));
if (session == null) {
throw _notifyException(AuthException('Current session is missing data.'));
throw notifyException(AuthException('Current session is missing data.'));
}

if (session.isExpired) {
Expand All @@ -787,22 +787,22 @@ class GoTrueClient {
accessToken: session.accessToken,
);
} else {
throw _notifyException(AuthException('Session expired.'));
throw notifyException(AuthException('Session expired.'));
}
} else {
final shouldEmitEvent = _currentSession == null ||
_currentSession?.user.id != session.user.id;
_saveSession(session);

if (shouldEmitEvent) _notifyAllSubscribers(AuthChangeEvent.signedIn);
if (shouldEmitEvent) notifyAllSubscribers(AuthChangeEvent.signedIn);

return AuthResponse(session: session);
}
}

/// return provider url only
Future<OAuthResponse> _handleProviderSignIn(
Provider provider, {
OAuthProvider provider, {
required String? scopes,
required String? redirectTo,
required Map<String, String>? queryParams,
Expand Down Expand Up @@ -937,7 +937,7 @@ class GoTrueClient {

_saveSession(authResponse.session!);

_notifyAllSubscribers(AuthChangeEvent.tokenRefreshed);
notifyAllSubscribers(AuthChangeEvent.tokenRefreshed);
_refreshTokenCompleter!.complete(authResponse);
return authResponse;
} on SocketException {
Expand All @@ -958,13 +958,17 @@ class GoTrueClient {
}
}

void _notifyAllSubscribers(AuthChangeEvent event) {
/// For internal use only.
@internal
void notifyAllSubscribers(AuthChangeEvent event) {
final state = AuthState(event, currentSession);
_onAuthStateChangeController.add(state);
_onAuthStateChangeControllerSync.add(state);
}

Exception _notifyException(Exception exception, [StackTrace? stackTrace]) {
/// For internal use only.
@internal
Exception notifyException(Exception exception, [StackTrace? stackTrace]) {
_onAuthStateChangeController.addError(
exception,
stackTrace ?? StackTrace.current,
Expand Down
2 changes: 1 addition & 1 deletion packages/gotrue/lib/src/gotrue_mfa_api.dart
Expand Up @@ -99,7 +99,7 @@ class GoTrueMFAApi {
refreshToken: response.refreshToken,
),
);
_client._notifyAllSubscribers(AuthChangeEvent.mfaChallengeVerified);
_client.notifyAllSubscribers(AuthChangeEvent.mfaChallengeVerified);
return response;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/gotrue/lib/src/types/auth_response.dart
Expand Up @@ -19,7 +19,7 @@ class AuthResponse {

/// Response of OAuth signin
class OAuthResponse {
final Provider provider;
final OAuthProvider provider;
final String? url;

/// Instanciates an `OAuthResponse` object from json response.
Expand Down
@@ -1,4 +1,4 @@
enum Provider {
enum OAuthProvider {
apple,
azure,
bitbucket,
Expand All @@ -17,9 +17,3 @@ enum Provider {
twitter,
workos,
}

extension ProviderName on Provider {
String get name {
return toString().split('.').last;
}
}
39 changes: 22 additions & 17 deletions packages/gotrue/test/client_test.dart
Expand Up @@ -60,6 +60,7 @@ void main() {
'Authorization': 'Bearer $anonToken',
'apikey': anonToken,
},
asyncStorage: asyncStorage,
);
});

Expand Down Expand Up @@ -90,10 +91,7 @@ void main() {
expect(data?.user.userMetadata, {'Hello': 'World'});
});

test('Parsing invalid URL should emit Exception on onAuthStateChange',
() async {
expect(client.onAuthStateChange, emitsError(isA<AuthException>()));

test('Parsing invalid URL should throw', () async {
const expiresIn = 12345;
const refreshToken = 'my_refresh_token';
const tokenType = 'my_token_type';
Expand Down Expand Up @@ -294,41 +292,48 @@ void main() {

group('The auth client can signin with third-party oAuth providers', () {
test('signIn() with Provider', () async {
final res = await client.getOAuthSignInUrl(provider: Provider.google);
final res =
await client.getOAuthSignInUrl(provider: OAuthProvider.google);
expect(res.url, isA<String>());
expect(res.provider, Provider.google);
expect(res.provider, OAuthProvider.google);
});

test('signIn() with Provider with redirectTo', () async {
final res = await client.getOAuthSignInUrl(
provider: Provider.google, redirectTo: 'https://supabase.com');
expect(res.url,
'$gotrueUrl/authorize?provider=google&redirect_to=https%3A%2F%2Fsupabase.com');
expect(res.provider, Provider.google);
provider: OAuthProvider.google, redirectTo: 'https://supabase.com');
final expectedOutput =
'$gotrueUrl/authorize?provider=google&redirect_to=https%3A%2F%2Fsupabase.com';
final queryParameters = Uri.parse(res.url!).queryParameters;

expect(res.url, startsWith(expectedOutput));
expect(queryParameters, containsPair('flow_type', 'pkce'));
expect(queryParameters, containsPair('code_challenge', isNotNull));
expect(queryParameters, containsPair('code_challenge_method', 's256'));
expect(res.provider, OAuthProvider.google);
});

test('signIn() with Provider can append a redirectUrl', () async {
final res = await client.getOAuthSignInUrl(
provider: Provider.google,
provider: OAuthProvider.google,
redirectTo: 'https://localhost:9000/welcome');
expect(res.url, isA<String>());
expect(res.provider, Provider.google);
expect(res.provider, OAuthProvider.google);
});

test('signIn() with Provider can append scopes', () async {
final res = await client.getOAuthSignInUrl(
provider: Provider.google, scopes: 'repo');
provider: OAuthProvider.google, scopes: 'repo');
expect(res.url, isA<String>());
expect(res.provider, Provider.google);
expect(res.provider, OAuthProvider.google);
});

test('signIn() with Provider can append options', () async {
final res = await client.getOAuthSignInUrl(
provider: Provider.google,
provider: OAuthProvider.google,
redirectTo: 'https://localhost:9000/welcome',
scopes: 'repo');
expect(res.url, isA<String>());
expect(res.provider, Provider.google);
expect(res.provider, OAuthProvider.google);
});
});

Expand Down Expand Up @@ -419,7 +424,7 @@ void main() {
test('getOAuthSignInUrl with PKCE flow has the correct query parameters',
() async {
final response = await client.getOAuthSignInUrl(
provider: Provider.google,
provider: OAuthProvider.google,
);
final url = Uri.parse(response.url!);
final queryParameters = url.queryParameters;
Expand Down

0 comments on commit 7782a58

Please sign in to comment.