Skip to content

Commit

Permalink
fix(gotrue, supabase_flutter): Throw error when parsing auth URL that…
Browse files Browse the repository at this point in the history
… contains an error description. (#839)

* auth exception should be thrown with pkce flow

* Add tests for error url parsing

* correct the auth flow type within tests

* trigger getSessionFromUrl when the URL contains error description

* fix: admin client initialization
  • Loading branch information
dshukertjr committed Feb 16, 2024
1 parent 0e1d891 commit afc4ce5
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 18 deletions.
17 changes: 9 additions & 8 deletions packages/gotrue/lib/src/gotrue_client.dart
Expand Up @@ -719,14 +719,6 @@ class GoTrueClient {
Uri originUrl, {
bool storeSession = true,
}) async {
if (_flowType == AuthFlowType.pkce) {
final authCode = originUrl.queryParameters['code'];
if (authCode == null) {
throw AuthPKCEGrantCodeExchangeError(
'No code detected in query parameters.');
}
return await exchangeCodeForSession(authCode);
}
var url = originUrl;
if (originUrl.hasQuery) {
final decoded = originUrl.toString().replaceAll('#', '&');
Expand All @@ -741,6 +733,15 @@ class GoTrueClient {
throw AuthException(errorDescription);
}

if (_flowType == AuthFlowType.pkce) {
final authCode = originUrl.queryParameters['code'];
if (authCode == null) {
throw AuthPKCEGrantCodeExchangeError(
'No code detected in query parameters.');
}
return await exchangeCodeForSession(authCode);
}

final accessToken = url.queryParameters['access_token'];
final expiresIn = url.queryParameters['expires_in'];
final refreshToken = url.queryParameters['refresh_token'];
Expand Down
49 changes: 40 additions & 9 deletions packages/gotrue/test/client_test.dart
Expand Up @@ -43,9 +43,10 @@ void main() {
'apikey': anonToken,
},
asyncStorage: asyncStorage,
flowType: AuthFlowType.implicit,
);

adminClient = client = GoTrueClient(
adminClient = GoTrueClient(
url: gotrueUrl,
headers: {
'Authorization': 'Bearer ${getServiceRoleToken(env)}',
Expand All @@ -62,6 +63,7 @@ void main() {
'apikey': anonToken,
},
asyncStorage: asyncStorage,
flowType: AuthFlowType.implicit,
);
});

Expand Down Expand Up @@ -106,6 +108,23 @@ void main() {
} catch (_) {}
});

test('Parsing an error URL should throw', () async {
const errorMessage =
'Unverified email with spotify. A confirmation email has been sent to your spotify email';

final urlWithoutAccessToken = Uri.parse(
'http://my-callback-url.com/#error=unauthorized_client&error_code=401&error_description=${Uri.encodeComponent(errorMessage)}');
try {
await client.getSessionFromUrl(urlWithoutAccessToken);
fail('getSessionFromUrl did not throw exception');
} on AuthException catch (error) {
expect(error.message, errorMessage);
} catch (error) {
fail(
'getSessionFromUrl threw ${error.runtimeType} instead of AuthException');
}
});

test('Subscribe a listener', () async {
final stream = client.onAuthStateChange;

Expand Down Expand Up @@ -302,14 +321,8 @@ void main() {
test('signIn() with Provider with redirectTo', () async {
final res = await client.getOAuthSignInUrl(
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.url,
'$gotrueUrl/authorize?provider=google&redirect_to=https%3A%2F%2Fsupabase.com');
expect(res.provider, OAuthProvider.google);
});

Expand Down Expand Up @@ -489,5 +502,23 @@ void main() {
expect(queryParameters['code_challenge_method'], 's256');
expect(queryParameters['code_challenge'], isA<String>());
});

test('Parsing an error URL should throw', () async {
const errorMessage =
'Unverified email with spotify. A confirmation email has been sent to your spotify email';

// Supabase Auth returns a URL with `#` even when using pkce flow.
final urlWithoutAccessToken = Uri.parse(
'http://my-callback-url.com/#error=unauthorized_client&error_code=401&error_description=${Uri.encodeComponent(errorMessage)}');
try {
await client.getSessionFromUrl(urlWithoutAccessToken);
fail('getSessionFromUrl did not throw exception');
} on AuthException catch (error) {
expect(error.message, errorMessage);
} catch (error) {
fail(
'getSessionFromUrl threw ${error.runtimeType} instead of AuthException');
}
});
});
}
3 changes: 2 additions & 1 deletion packages/supabase_flutter/lib/src/supabase_auth.dart
Expand Up @@ -147,7 +147,8 @@ class SupabaseAuth with WidgetsBindingObserver {
return (uri.fragment.contains('access_token') &&
_authFlowType == AuthFlowType.implicit) ||
(uri.queryParameters.containsKey('code') &&
_authFlowType == AuthFlowType.pkce);
_authFlowType == AuthFlowType.pkce) ||
(uri.fragment.contains('error_description'));
}

/// Enable deep link observer to handle deep links
Expand Down

0 comments on commit afc4ce5

Please sign in to comment.