signIn method

  1. @override
Future<AuthSignInResult?> signIn({
  1. Map<String, dynamic>? params,
})
override

Executes the full OAuth 2.0 Authorization Code flow.

Steps:

  1. Generate PKCE verifier / challenge (when config.usePkce is true) and a CSRF state token.
  2. Build the authorization URL and open it in an external browser.
  3. Wait for handleRedirectCallback to deliver the code + state.
  4. Verify the state (CSRF protection).
  5. Exchange the code for tokens at OAuth2Config.tokenEndpoint.
  6. Fetch the user profile from OAuth2Config.userInfoEndpoint.
  7. 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();
  }
}