verifyAuthExternalProviderRuntimeConformance function
Future<void>
verifyAuthExternalProviderRuntimeConformance({
- required Uri origin,
- required AuthRuntimeConformanceSend send,
- 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);
}