---
title: "Check every step of single sign-on · Agent Wranglers"
url: https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/single-sign-on
updated: 2026-10-20
licence: "All rights reserved"
---

# Check every step of single sign-on

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Access and accounts](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts)
- Rule: For login through another provider with OAuth or OpenID Connect, use the authorization code flow with PKCE and check `state`, `nonce` and the ID token.
- How: Register exact redirect addresses with the provider, never wildcards. Send a random `state` and `nonce` and compare them on return. Verify the ID token's signature, issuer, audience and expiry as in [verify every token](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/token-checks). Link a login to an existing account only by the provider's stable user ID. Never link by an email the provider has not verified. Follow RFC 9700 for the details. For SAML, use a maintained library and reject unsigned assertions. See also [connect single sign-on per organisation](https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/organisation-sso).
- Check: Return to the callback with a changed `state`, a reused code and an ID token for another client. Each login fails. Change the redirect address in the login request to `https://evil.example.net`. The provider refuses it.

Example:

```js
// Node with openid-client: PKCE, state and nonce are checked on return
const tokens = await client.authorizationCodeGrant(config, callbackUrl, {
  pkceCodeVerifier: session.codeVerifier, expectedState: session.state, expectedNonce: session.nonce,
});
const user = await findUserByProviderId(tokens.claims().iss, tokens.claims().sub);
```

```python
# Python with Authlib: the state and the ID token are checked here
token = oauth.example.authorize_access_token()
claims = token["userinfo"]
user = find_user_by_provider_id(claims["iss"], claims["sub"])
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers security testing playbook: https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/single-sign-on`.

- Previous: [Ask again before changing how someone logs in](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/reauthenticate-sensitive-changes)
- [Access and accounts](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts)
