verifyAuthExternalProviderRuntimeConformance function

Future<void> verifyAuthExternalProviderRuntimeConformance({
  1. required Uri origin,
  2. required AuthRuntimeConformanceSend send,
  3. required bool expectJwt,
})

Verifies deterministic external-provider behavior through one host adapter.

send must target an engine created by createAuthExternalProviderRuntimeConformanceEngine with the strategy represented by expectJwt. This helper has no dependency on package:test or browser interoperability libraries.

Implementation

Future<void> verifyAuthExternalProviderRuntimeConformance({
  required Uri origin,
  required AuthRuntimeConformanceSend send,
  required bool expectJwt,
}) async {
  final existingUserId = await _seedExistingUser(send, origin.toString());
  final primary = await _startOidc(send);
  final sessionOnlyCookie = _cookieNamed(
    primary.cookieHeader,
    authExternalProviderRuntimeCookieName,
  );
  _check(
    sessionOnlyCookie != null,
    'oidc.start.browser-cookie',
    'OIDC start did not issue the host session cookie.',
  );

  // A missing auxiliary state cookie is intentionally supported by the
  // framework-session fallback. To test browser binding, use the framework
  // cookie from a genuinely different browser session instead.
  final otherBrowser = await _startOidc(send);
  final otherSessionCookie = _cookieNamed(
    otherBrowser.cookieHeader,
    authExternalProviderRuntimeCookieName,
  );
  _check(
    otherSessionCookie != null,
    'oidc.start.other-browser-cookie',
    'The second OIDC browser did not issue the host session cookie.',
  );

  final crossBrowser = await _finishOidc(
    send,
    start: primary,
    cookie: otherSessionCookie!,
    state: primary.state,
    variant: 'verified',
  );
  _expectProviderError(
    crossBrowser,
    caseId: 'oidc.callback.browser-binding',
    error: 'invalid_state',
  );

  final tampered = await _finishOidc(
    send,
    start: primary,
    cookie: primary.cookieHeader,
    state: '${primary.state}x',
    variant: 'verified',
  );
  _expectProviderError(
    tampered,
    caseId: 'oidc.callback.state-tampering',
    error: 'invalid_state',
  );

  final linked = await _finishOidc(
    send,
    start: primary,
    cookie: primary.cookieHeader,
    state: primary.state,
    variant: 'verified',
  );
  _expectStatus(linked, 200, 'oidc.callback.verified-email');
  final linkedBody = _jsonObject(linked, 'oidc.callback.verified-email');
  _check(
    _userField(linkedBody, 'id') == existingUserId,
    'oidc.callback.verified-email',
    'A verified provider email did not reuse the existing account.',
  );
  _check(
    _userField(linkedBody, 'email') == _email,
    'oidc.callback.verified-email',
    'The linked provider user did not retain its verified email.',
  );
  _expectStrategyAndCookie(
    linked,
    linkedBody,
    caseId: 'oidc.callback.host-session',
    expectJwt: expectJwt,
  );

  final replay = await _finishOidc(
    send,
    start: primary,
    cookie: primary.cookieHeader,
    state: primary.state,
    variant: 'verified',
  );
  _expectProviderError(
    replay,
    caseId: 'oidc.callback.replay',
    error: 'invalid_state',
  );

  final unverifiedStart = await _startOidc(send);
  final unverified = await _finishOidc(
    send,
    start: unverifiedStart,
    cookie: unverifiedStart.cookieHeader,
    state: unverifiedStart.state,
    variant: 'unverified',
  );
  _expectStatus(unverified, 200, 'oidc.callback.unverified-email');
  final unverifiedBody = _jsonObject(
    unverified,
    'oidc.callback.unverified-email',
  );
  _check(
    _userField(unverifiedBody, 'id') != existingUserId,
    'oidc.callback.unverified-email',
    'An unverified provider email silently linked an existing account.',
  );
  _check(
    _userField(unverifiedBody, 'email') == null,
    'oidc.callback.unverified-email',
    'An unverified provider email was persisted as trusted identity data.',
  );

  final nonceStart = await _startOidc(send);
  final badNonce = await _finishOidc(
    send,
    start: nonceStart,
    cookie: nonceStart.cookieHeader,
    state: nonceStart.state,
    variant: 'bad-nonce',
  );
  _expectProviderError(
    badNonce,
    caseId: 'oidc.callback.nonce-tampering',
    error: 'oidc_nonce_mismatch',
  );

  final failureStart = await _startOidc(send);
  final providerFailure = await send(
    AuthRuntimeConformanceRequest(
      method: 'GET',
      path: Uri(
        path: '/auth/callback/$authExternalProviderRuntimeOidcProviderId',
        queryParameters: <String, String>{
          'code': 'provider-failure',
          'state': failureStart.state,
        },
      ).toString(),
      headers: <String, List<String>>{
        'cookie': <String>[failureStart.cookieHeader],
      },
    ),
  );
  _expectProviderError(
    providerFailure,
    caseId: 'oidc.callback.provider-failure',
    error: 'token_exchange_failed',
  );

  final safeStart = await _startOidc(send, callbackUrl: '/after-oauth');
  final safeRedirect = await _finishOidc(
    send,
    start: safeStart,
    cookie: safeStart.cookieHeader,
    state: safeStart.state,
    variant: 'verified',
  );
  _expectStatus(safeRedirect, 302, 'oidc.redirect.safe');
  _check(
    _location(safeRedirect) == '/after-oauth',
    'oidc.redirect.safe',
    'A rooted relative provider redirect was not preserved.',
  );
  _expectIssuedAuthCookie(
    safeRedirect,
    caseId: 'oidc.redirect.safe',
    expectJwt: expectJwt,
  );

  for (final unsafe in const <String>[
    'https://attacker.example/steal',
    '//attacker.example/steal',
    'javascript:alert(1)',
    '/safe\r\nlocation:https://attacker.example',
  ]) {
    final start = await _startOidc(send, callbackUrl: unsafe);
    final response = await _finishOidc(
      send,
      start: start,
      cookie: start.cookieHeader,
      state: start.state,
      variant: 'unsafe',
    );
    _expectStatus(response, 200, 'oidc.redirect.unsafe');
    _check(
      _location(response) == null &&
          !response.body.contains('attacker.example') &&
          !response.body.toLowerCase().contains('javascript:'),
      'oidc.redirect.unsafe',
      'An unsafe provider redirect reached the public response.',
    );
  }

  await _verifyCustomCallbackProvider(send, expectJwt: expectJwt);
}