begin method

Future<({PendingAuthorization pending, Uri url})> begin({
  1. required String serverUrl,
  2. String? wwwAuthenticate,
  3. List<String> keepScopes = const [],
})

The first half: discover, register, and build the authorization URL. keepScopes are scopes already held that a step-up must not lose.

Implementation

Future<({Uri url, PendingAuthorization pending})> begin({
  required String serverUrl,
  String? wwwAuthenticate,
  List<String> keepScopes = const [],
}) async {
  final resourceUrl = canonicalResourceUri(serverUrl);
  final challenge = WwwAuthenticateChallenge.parse(wwwAuthenticate);
  final prm = await _protectedResource(serverUrl, challenge);
  final resource = prm == null ? resourceUrl : canonicalResourceUri(prm.resource);
  final metadata = prm == null
      ? await _legacyAuthorizationServer(serverUrl)
      : await authorizationServerMetadata(_chooseAuthorizationServer(prm));
  _requirePkce(metadata);
  _requireHttps(metadata);

  // A token from a different authorization server is never sent again.
  final held = await options.store.loadGrant(resource);
  if (held != null && held.issuer != metadata.issuer) {
    _logger.warning('The authorization server of $resource changed (${held.issuer} -> ${metadata.issuer})');
    await options.store.deleteGrant(resource);
  }

  final registration = await _registration(metadata);
  final scopes = {
    ...keepScopes,
    ...(challenge?.scopes.isNotEmpty ?? false) ? challenge!.scopes : (prm?.scopesSupported ?? const <String>[]),
  }.toList();
  final verifier = _randomToken(32);
  final state = _randomToken(16);
  final url = Uri.parse(metadata.authorizationEndpoint).replace(queryParameters: {
    ...Uri.parse(metadata.authorizationEndpoint).queryParameters,
    'response_type': 'code',
    'client_id': registration.clientId,
    'redirect_uri': options.redirectUri,
    'code_challenge': base64Url.encode(sha256.convert(utf8.encode(verifier)).bytes).replaceAll('=', ''),
    'code_challenge_method': 'S256',
    'state': state,
    'resource': resource,
    if (scopes.isNotEmpty) 'scope': scopes.join(' '),
  });
  return (
    url: url,
    pending: PendingAuthorization(
      resource: resource,
      issuer: metadata.issuer,
      issParameterSupported: metadata.authorizationResponseIssParameterSupported,
      tokenEndpoint: metadata.tokenEndpoint,
      clientId: registration.clientId,
      redirectUri: options.redirectUri,
      state: state,
      codeVerifier: verifier,
      scopes: scopes,
    ),
  );
}