signIn method
Executes the full OAuth 2.0 Authorization Code flow.
Steps:
- Generate PKCE verifier / challenge (when
config.usePkceistrue) and a CSRF state token. - Build the authorization URL and open it in an external browser.
- Wait for handleRedirectCallback to deliver the code + state.
- Verify the state (CSRF protection).
- Exchange the code for tokens at OAuth2Config.tokenEndpoint.
- Fetch the user profile from OAuth2Config.userInfoEndpoint.
- Return AuthSignInResult with user + all tokens.
Throws AuthenticationFailedException on protocol or network errors. Throws AuthenticationCancelledException when the user dismisses the browser or OAuth2Config.timeout elapses.
Implementation
@override
Future<AuthSignInResult?> signIn({Map<String, dynamic>? params}) async {
try {
logInfo('Starting OAuth2 flow for ${config.providerName}');
// Step 1: Generate PKCE values (and state for CSRF) if enabled.
if (config.usePkce) {
_generatePkceValues();
logDebug('PKCE enabled — code verifier generated');
} else {
_generateState();
logDebug('PKCE disabled — using state parameter only');
}
// Step 2: Build authorization URL.
final authUrl = _buildAuthorizationUrl();
logDebug('Authorization URL built');
// Step 3: Open browser and wait for the redirect callback.
final callbackParams = await _openAuthorizationUrl(authUrl);
logDebug('Redirect callback received');
// Step 4: Verify state (CSRF protection).
_verifyState(callbackParams);
// Step 5: Check for OAuth error parameters in the callback.
_checkForErrors(callbackParams);
// Step 6: Extract the authorization code.
final code = callbackParams['code'];
if (code == null || code.isEmpty) {
throw AuthenticationFailedException(
'No authorization code in callback from ${config.providerName}',
providerName: config.providerName,
);
}
logDebug('Authorization code received');
// Step 7: Exchange code for tokens.
final tokens = await _exchangeCodeForTokens(code);
logDebug('Tokens received from ${config.providerName}');
// Step 8: Fetch the user profile from the userinfo endpoint.
final userInfo = await _fetchUserInfo(tokens['access_token']!);
logDebug('User info fetched');
// Step 9: Extract user from the provider-specific userInfo map.
final user = config.userExtractor(userInfo);
// Step 10: Parse expiry from the token response.
// expires_in is included as a String in the returned map (token endpoint
// returns it as a JSON integer, which we stringify to stay Map<String,String>).
final expiresInStr = tokens['expires_in'];
final expiresAt = expiresInStr != null
? DateTime.now().add(Duration(seconds: int.parse(expiresInStr)))
: null;
logInfo('OAuth2 sign in successful for $id');
// Return the full result — never discard tokens.
return AuthSignInResult(
user: user,
accessToken: tokens['access_token'],
refreshToken: tokens['refresh_token'],
expiresAt: expiresAt,
);
} on AuthException {
rethrow;
} catch (e, stackTrace) {
logError('OAuth2 sign in failed', e, stackTrace);
throw AuthenticationFailedException(
'Failed to sign in with ${config.providerName}',
providerName: id,
originalError: e,
);
} finally {
_cleanup();
}
}