verifyAuthWebAuthnBrowserRuntimeConformance function

Future<void> verifyAuthWebAuthnBrowserRuntimeConformance({
  1. required Uri transportOrigin,
  2. required AuthRuntimeConformanceSend send,
  3. required String sessionCookie,
  4. required String sessionCookieName,
  5. required String csrfToken,
  6. required String expectedUserEmail,
})

Verifies a successful browser-shaped passkey ceremony through one host.

transportOrigin is the URL of the host adapter under test. The WebAuthn payload retains the fixture's secure browser RP origin so an ephemeral plain-HTTP IO listener can carry the same deterministic browser JSON.

Implementation

Future<void> verifyAuthWebAuthnBrowserRuntimeConformance({
  required Uri transportOrigin,
  required AuthRuntimeConformanceSend send,
  required String sessionCookie,
  required String sessionCookieName,
  required String csrfToken,
  required String expectedUserEmail,
}) async {
  final registrationOptions = await send(
    AuthRuntimeConformanceRequest(
      method: 'POST',
      path: '/auth/webauthn/register/options',
      headers: _jsonHeaders(
        cookie: sessionCookie,
        origin: transportOrigin.toString(),
      ),
      body: jsonEncode(<String, Object?>{'_csrf': csrfToken}),
    ),
  );
  _expectStatus(registrationOptions, 200, 'webauthn.registration-options');
  final registrationBody = _jsonObject(
    registrationOptions,
    'webauthn.registration-options',
  );
  final relyingParty = registrationBody['rp'];
  final registrationUser = registrationBody['user'];
  _check(
    registrationBody['challenge'] is String &&
        (registrationBody['challenge']! as String).isNotEmpty &&
        relyingParty is Map<String, Object?> &&
        relyingParty['id'] == _relyingPartyId &&
        registrationUser is Map<String, Object?> &&
        registrationUser['id'] is String,
    'webauthn.registration-options',
    'Registration options omitted browser ceremony fields.',
  );
  final registrationUserMap = switch (registrationUser) {
    final Map<String, Object?> value => value,
    _ => throw const AuthRuntimeConformanceFailure(
      caseId: 'webauthn.registration-options',
      message: 'Registration options omitted browser ceremony fields.',
    ),
  };
  final registrationUserId = switch (registrationUserMap['id']) {
    final String value => value,
    _ => throw const AuthRuntimeConformanceFailure(
      caseId: 'webauthn.registration-options',
      message: 'Registration options omitted browser ceremony fields.',
    ),
  };

  final fixture = AuthWebAuthnCeremonyFixture(
    relyingPartyId: _relyingPartyId,
    origin: Uri.parse(_browserOrigin),
  );
  final registered = await send(
    AuthRuntimeConformanceRequest(
      method: 'POST',
      path: '/auth/webauthn/register/verify',
      headers: _jsonHeaders(
        cookie: sessionCookie,
        origin: transportOrigin.toString(),
      ),
      body: jsonEncode(<String, Object?>{
        'credential': fixture.registrationCredential(
          challenge: registrationBody['challenge']! as String,
        ),
        '_csrf': csrfToken,
      }),
    ),
  );
  _expectStatus(registered, 200, 'webauthn.registration-verify');
  final registeredCredential = _jsonObject(
    registered,
    'webauthn.registration-verify',
  )['credential'];
  _check(
    registeredCredential is Map<String, Object?> &&
        registeredCredential['credential_id'] == fixture.credentialId,
    'webauthn.registration-verify',
    'Registration did not persist the browser credential.',
  );

  final authenticationOptions = await send(
    AuthRuntimeConformanceRequest(
      method: 'POST',
      path: '/auth/webauthn/authenticate/options',
      headers: _jsonHeaders(),
      body: '{}',
    ),
  );
  _expectStatus(authenticationOptions, 200, 'webauthn.authentication-options');
  final authenticationBody = _jsonObject(
    authenticationOptions,
    'webauthn.authentication-options',
  );
  _check(
    authenticationBody['challenge'] is String &&
        (authenticationBody['challenge']! as String).isNotEmpty,
    'webauthn.authentication-options',
    'Authentication options omitted the browser challenge.',
  );
  final assertion = fixture.assertionCredential(
    challenge: authenticationBody['challenge']! as String,
    counter: 1,
    userHandle: registrationUserId,
  );
  _check(
    fixture.hasDerEs256Signature(assertion),
    'webauthn.authentication-assertion',
    'Assertion was not encoded as a browser ASN.1 DER signature.',
  );

  final authenticated = await send(
    AuthRuntimeConformanceRequest(
      method: 'POST',
      path: '/auth/webauthn/authenticate/verify',
      headers: _jsonHeaders(),
      body: jsonEncode(<String, Object?>{'credential': assertion}),
    ),
  );
  _expectStatus(authenticated, 200, 'webauthn.authentication-verify');
  final authenticatedBody = _jsonObject(
    authenticated,
    'webauthn.authentication-verify',
  );
  _check(
    _userField(authenticatedBody, 'email') == expectedUserEmail &&
        (authenticatedBody['credential'] as Map?)?['counter'] == 1,
    'webauthn.authentication-verify',
    'Authentication did not resolve the user and advance the counter.',
  );
  _requireSessionCookie(
    authenticated,
    caseId: 'webauthn.authentication-verify',
    cookieName: sessionCookieName,
  );

  final replayedChallenge = await send(
    AuthRuntimeConformanceRequest(
      method: 'POST',
      path: '/auth/webauthn/authenticate/verify',
      headers: _jsonHeaders(),
      body: jsonEncode(<String, Object?>{'credential': assertion}),
    ),
  );
  _expectSanitizedWebAuthnError(
    replayedChallenge,
    'webauthn.challenge-replay',
    expectedError: 'webauthn_challenge_invalid',
    sensitiveValue: fixture.credentialId,
  );

  final counterOptions = await send(
    AuthRuntimeConformanceRequest(
      method: 'POST',
      path: '/auth/webauthn/authenticate/options',
      headers: _jsonHeaders(),
      body: '{}',
    ),
  );
  _expectStatus(counterOptions, 200, 'webauthn.counter-options');
  final staleCounterAssertion = fixture.assertionCredential(
    challenge:
        _jsonObject(counterOptions, 'webauthn.counter-options')['challenge']!
            as String,
    counter: 1,
    userHandle: registrationUserId,
  );
  final replayedCounter = await send(
    AuthRuntimeConformanceRequest(
      method: 'POST',
      path: '/auth/webauthn/authenticate/verify',
      headers: _jsonHeaders(),
      body: jsonEncode(<String, Object?>{'credential': staleCounterAssertion}),
    ),
  );
  _expectSanitizedWebAuthnError(
    replayedCounter,
    'webauthn.counter-replay',
    expectedError: 'webauthn_counter_replay',
    sensitiveValue: fixture.credentialId,
  );
}