Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gotrue, supabase_flutter): Throw error when parsing auth URL that contains an error description. #839

Merged
merged 5 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ void main() {
'apikey': anonToken,
},
asyncStorage: asyncStorage,
flowType: AuthFlowType.implicit,
Vinzent03 marked this conversation as resolved.
Show resolved Hide resolved
);

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
Original file line number Diff line number Diff line change
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
Loading