authorize method

Future<(Uri, OAuthContext)> authorize(
  1. String identity
)

Initiates an OAuth 2.0 authorization request using Pushed Authorization Requests (PAR) with PKCE (Proof Key for Code Exchange) and DPoP (Demonstrating Proof of Possession).

This method implements the following OAuth 2.0 security features:

  • PAR (RFC 9126) for secure authorization request transmission
  • PKCE (RFC 7636) to prevent authorization code interception
  • DPoP (RFC 9449) for proof-of-possession tokens

The flow consists of two steps:

  1. Pushes the authorization request parameters to the authorization server
  2. Returns the authorization URL with the obtained request URI

Example:

final authUrl = await authorize('shinyakato.dev');
// Redirect user to authUrl for authentication

Parameters:

  • identity: The user's identifier (typically email) used as login_hint

Security measures implemented:

  • Generates cryptographically secure random values for PKCE and state
  • Uses SHA-256 for PKCE code challenge
  • Stores DPoP nonce from server response
  • Validates server response status (201 Created)

Throws:

  • OAuthException: If the PAR request fails or returns unexpected status code

Returns:

  • Uri: The authorization URL where the user should be redirected to complete authentication

Implementation

Future<(Uri, OAuthContext)> authorize(final String identity) async {
  final codeVerifier = random(46);
  final codeChallenge = hashS256(codeVerifier);
  final state = random(64);

  final response = await http.post(
    Uri.https(service, '/oauth/par'),
    body: {
      'client_id': metadata.clientId,
      'redirect_uri': metadata.redirectUris.firstOrNull,
      'login_hint': identity,
      'state': state,
      'code_challenge': codeChallenge,
      'code_challenge_method': 'S256',
      'response_type': 'code',
      'scope': metadata.scope,
    },
  );

  if (response.statusCode != 201) {
    throw OAuthException(response.body);
  }

  return (
    Uri.https(
      service,
      '/oauth/authorize',
      {
        'client_id': metadata.clientId,
        'request_uri': jsonDecode(response.body)['request_uri']!,
      },
    ),
    OAuthContext(
      codeVerifier: codeVerifier,
      state: state,
      dpopNonce: response.headers['dpop-nonce']!,
    )
  );
}