authorize method
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:
- Pushes the authorization request parameters to the authorization server
- Returns the authorization URL with the obtained request URI
Example:
// With login hint
final authUrl = await authorize('shinyakato.dev');
// Without login hint
final authUrl = await authorize();
// Redirect user to authUrl for authentication
Parameters:
identity: Optional user identifier (typically handle or email) used as login_hint. If not provided, the login_hint parameter will not be included in the request.
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 bodyParams = <String, String>{
'client_id': metadata.clientId,
'redirect_uri': metadata.redirectUris.firstOrNull ?? '',
'state': state,
'code_challenge': codeChallenge,
'code_challenge_method': 'S256',
'response_type': 'code',
'scope': metadata.scope,
};
// Only include login_hint if identity is provided
if (identity != null && identity.isNotEmpty) {
bodyParams['login_hint'] = identity;
}
final response = await http.post(
Uri.https(service, '/oauth/par'),
body: bodyParams,
);
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']!,
),
);
}