OAuth 2.1 posture
This server implements the OAuth 2.1 subset of OAuth 2.0 (RFC 6749, with PKCE
from RFC 7636) rather than all of it: authorization code with PKCE, client
credentials, refresh token and device code, plus backchannel authentication,
token exchange and grants of a host's own. The code on this page is included
from samples/Protocols.Resources,
which a test boots and reads the discovery document of: no implicit or password grant, and code as the only
response type.
What OAuth 2.1 requires, and where
Each row was checked against a call site, not inferred from a type name.
| OAuth 2.1 requirement | Here |
|---|---|
| Implicit grant removed | Not implemented, and not configurable: GrantTypes.Implicit no longer exists |
| Resource-owner password (ROPC) removed | Not implemented, and not configurable: GrantTypes.ResourceOwnerPassword no longer exists |
| PKCE required for the authorization code flow | RequirePkce defaults to true and is enforced by the authorize validator |
| Redirect URIs compared by exact string match | No wildcard or prefix matching anywhere (see which redirect URIs a client may use) |
| Bearer tokens never in the query string | UserInfo reads the Authorization header only |
| Refresh tokens rotated / single use | RefreshTokenUsage defaults to OneTimeOnly |
| An artifact is redeemable only by the client it was issued to | All five: authorization code, refresh token, device code, auth_req_id, and — as of 0.37.0 — a pushed request_uri |
A native client, with each of those defaults written out:
// A native application. Each setting shown is the default, written out: PKCE required and with S256 only,
// and a refresh token that is good for one use. The loopback address is registered without a port.
new Client
{
ClientId = "field-notes",
RequireClientSecret = false,
AllowedGrantTypes = { OidcConstants.GrantTypes.AuthorizationCode },
RedirectUris = { "http://127.0.0.1" },
AllowedScopes = { "openid", "profile", "ledger.read", "orders.read" },
AllowOfflineAccess = true,
RequirePkce = true,
AllowPlainTextPkce = false,
RefreshTokenUsage = IdentityServerConstants.TokenUsage.OneTimeOnly,
},
One deliberate opt-out. AllowPlainTextPkce lets a single client fall back
to the plain code challenge method, which OAuth 2.1 disallows. It defaults to false, so it costs a host an explicit
per-client decision; it exists for legacy clients that cannot compute S256. Leave it off.
Why the removed grants are gone rather than refused at runtime. Both were once settable and neither was ever
serviceable: a client registered for one validated cleanly and could then never obtain a token. Keeping the names while
refusing the requests would keep that false promise. The constants are deleted, so the failure is a compile error at
configuration time rather than a puzzle at runtime. A client that asks for either anyway is answered
unsupported_grant_type (see a grant this server does not have).
Corrected in 0.130.0: the
fragmentandform_postresponse modes were deleted with those grants, as the modes only implicit and hybrid need — and they are not: both apply toresponse_type=code, and ASP.NET Core's OpenID Connect handler asks forform_postby default. They are back; see Response modes.
The flows, as the server runs them
Every flow this server serves is recorded from the demo host, request by request, with a sequence diagram drawn from the same exchanges — see Protocol flows:
| Flow | Recorded |
|---|---|
| Discovery and the key set | Discovery and JWKS |
| Authorization code with PKCE — interactive web, SPA and native applications | Authorization code with PKCE |
| Refresh, with rotation | Refresh token |
| Client credentials — machine to machine | Client credentials |
| A token for one API | Resource indicators |
| A service's token for a user | Token exchange |
| An API asking about a token | Introspection, revocation |
| Pushed authorization requests | PAR |
| A device with no browser | Device authorization |
| A user who is somewhere else | CIBA (poll mode) |
| A token bound to the client's key | DPoP |
| Signing out | RP-initiated logout |
| SAML 2.0, this server as the identity provider | SAML single sign-on |
SAML 2.0 (OASIS SAML Core) is native in both directions, over the HTTP-Redirect and HTTP-POST bindings, and every response and assertion is signed with the framework's XML-DSig. Two SAML shapes are not recorded:
- IdP-initiated sign-on. The user follows a link at this server (
/saml/sso/initiate?sp=…) and there is noAuthnRequestat all; the rest is the recorded flow from the signed response on. - This server as the service provider, federating the user out to an upstream identity provider — statically registered (see External providers) or stored as a row (see stored SAML providers):
sequenceDiagram
autonumber
participant U as User (Browser)
participant SP as Fdyay.IdentityServer (SP)
participant IdP as Business IdP
U->>SP: Access a protected resource (challenge the SAML scheme)
SP->>SP: Create AuthnRequest, protect state into RelayState
SP-->>U: 302 to IdP SSO?SAMLRequest=…&RelayState=…
U->>IdP: Authenticate at the business IdP
IdP-->>U: 200 auto-POST form → SP assertion consumer service
U->>SP: POST SAMLResponse (+ RelayState)
SP->>SP: Verify signature / issuer / audience / recipient / InResponseTo
SP->>SP: Map NameID + attributes → ClaimsPrincipal, sign into external cookie
SP-->>U: 302 to the original return URL