Federated sign-in (OpenID Connect upstream)
A client signs its user in at this server, and this server sends the user on to the identity provider they belong to: the client trusts one server, which trusts the provider. Both hops are authorization code flows with PKCE. The client is the demo host's partner-web, which is held to the provider partner-external (IdentityProviderRestrictions) and names it in acr_values; the provider is a second instance of the demo host at http://idp.partner.example, which knows this server as its client gateway. For this flow the test registers that provider on the first instance with AddExternalOpenIdConnect — ASP.NET Core's OpenID Connect handler — with MapInboundClaims and the handler's telemetry parameters off and, because both run over plain http in-process, its correlation and nonce cookies following the request's scheme. The first time the user arrives the external sign-in page provisions a local user for the provider's alice, under a subject of this server's own. The full setup is the federation gateway guide.
Follows: OpenID Connect Core 1.0 §3.1, OAuth 2.0 Form Post Response Mode, RFC 7636 (PKCE), OpenID Connect Discovery 1.0.
Recorded from the demo host (src/Fdyay.IdentityServer.Host) by the FederatedSignInFlow test on every CI run. A value in braces, such as {access_token_1}, is one that differs on every run; the same placeholder is the same value wherever it appears. See how these pages are made.
sequenceDiagram
participant Browser
participant Client
participant AuthorizationServer as Authorization Server
participant UpstreamProvider as Upstream Provider
Browser->>AuthorizationServer: GET /connect/authorize
AuthorizationServer-->>Browser: 302 Found to /account/login
Browser->>AuthorizationServer: GET /account/login
AuthorizationServer-->>Browser: 302 Found to /account/externallogin
Browser->>AuthorizationServer: GET /account/externallogin
AuthorizationServer->>UpstreamProvider: GET http://idp.partner.example/.well-known/openid-configuration
UpstreamProvider-->>AuthorizationServer: 200 OK
AuthorizationServer->>UpstreamProvider: GET http://idp.partner.example/.well-known/openid-configuration/jwks
UpstreamProvider-->>AuthorizationServer: 200 OK
AuthorizationServer->>UpstreamProvider: POST http://idp.partner.example/connect/par
UpstreamProvider-->>AuthorizationServer: 201 Created
AuthorizationServer-->>Browser: 302 Found to http://idp.partner.example/connect/authorize
Browser->>UpstreamProvider: GET http://idp.partner.example/connect/authorize
UpstreamProvider-->>Browser: 302 Found to /account/login
Note over Browser,UpstreamProvider: The user signs in at the provider's sign-in page,<br/>as in the authorization code flow.
Browser->>UpstreamProvider: GET http://idp.partner.example/connect/authorize
UpstreamProvider-->>Browser: 200 OK
Browser->>AuthorizationServer: POST /signin-partner
AuthorizationServer->>UpstreamProvider: POST http://idp.partner.example/connect/token
UpstreamProvider-->>AuthorizationServer: 200 OK
AuthorizationServer-->>Browser: 302 Found to /Account/ExternalLogin
Browser->>AuthorizationServer: GET /Account/ExternalLogin
AuthorizationServer-->>Browser: 302 Found to /connect/authorize
Browser->>AuthorizationServer: GET /connect/authorize
AuthorizationServer-->>Browser: 302 Found to https://client.example.com/callback
Client->>AuthorizationServer: POST /connect/token
AuthorizationServer-->>Client: 200 OK
1. The client sends the browser to the authorize endpoint
acr_values=idp:partner-external names the provider the user signs in at — a home-realm hint. Nobody is signed in here, so the server sends the browser to its sign-in page.
Browser → Authorization Server
GET /connect/authorize?client_id=partner-web
&response_type=code
&scope=openid%20profile
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4
&code_challenge_method=S256
&acr_values=idp%3Apartner-external HTTP/1.1
Authorization Server → Browser
HTTP/1.1 302 Found
Location: /account/login?returnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dpartner-web%26response_type%3Dcode%26scope%3Dopenid%2520profile%26redirect_uri%3Dhttps%253A%252F%252Fclient.example.com%252Fcallback%26state%3Daf0ifjsldkj%26nonce%3Dn-0S6_WzA2Mj%26code_challenge%3D_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4%26code_challenge_method%3DS256%26acr_values%3Didp%253Apartner-external
2. The sign-in page sends the user straight to that provider
It does not offer its form or the list of providers: the hint names a provider the client accepts.
Browser → Authorization Server
GET /account/login?returnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dpartner-web%26response_type%3Dcode%26scope%3Dopenid%2520profile%26redirect_uri%3Dhttps%253A%252F%252Fclient.example.com%252Fcallback%26state%3Daf0ifjsldkj%26nonce%3Dn-0S6_WzA2Mj%26code_challenge%3D_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4%26code_challenge_method%3DS256%26acr_values%3Didp%253Apartner-external HTTP/1.1
Authorization Server → Browser
HTTP/1.1 302 Found
Location: /account/externallogin?scheme=partner-external
&returnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dpartner-web%26response_type%3Dcode%26scope%3Dopenid%2520profile%26redirect_uri%3Dhttps%253A%252F%252Fclient.example.com%252Fcallback%26state%3Daf0ifjsldkj%26nonce%3Dn-0S6_WzA2Mj%26code_challenge%3D_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4%26code_challenge_method%3DS256%26acr_values%3Didp%253Apartner-external
3. The external sign-in page challenges the provider
The OpenID Connect handler reads the provider's discovery document and keys the first time it needs them. The document advertises pushed authorization requests, so the handler pushes the request — with its client secret, a PKCE challenge, a nonce and a state that protects where the sign-in returns to — and sends the browser with only the request_uri it got back. Two cookies tie the response to this browser.
Browser → Authorization Server
GET /account/externallogin?scheme=partner-external
&returnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dpartner-web%26response_type%3Dcode%26scope%3Dopenid%2520profile%26redirect_uri%3Dhttps%253A%252F%252Fclient.example.com%252Fcallback%26state%3Daf0ifjsldkj%26nonce%3Dn-0S6_WzA2Mj%26code_challenge%3D_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4%26code_challenge_method%3DS256%26acr_values%3Didp%253Apartner-external HTTP/1.1
Authorization Server → Upstream Provider
GET http://idp.partner.example/.well-known/openid-configuration HTTP/1.1
Upstream Provider → Authorization Server
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"issuer": "http://idp.partner.example",
"jwks_uri": "http://idp.partner.example/.well-known/openid-configuration/jwks",
"authorization_endpoint": "http://idp.partner.example/connect/authorize",
"token_endpoint": "http://idp.partner.example/connect/token",
"userinfo_endpoint": "http://idp.partner.example/connect/userinfo",
"end_session_endpoint": "http://idp.partner.example/connect/endsession",
"request_parameter_supported": true,
"request_uri_parameter_supported": false,
"prompt_values_supported": [
"none",
"login",
"consent",
"select_account"
],
"check_session_iframe": "http://idp.partner.example/connect/checksession",
"revocation_endpoint": "http://idp.partner.example/connect/revocation",
"introspection_endpoint": "http://idp.partner.example/connect/introspect",
"device_authorization_endpoint": "http://idp.partner.example/connect/deviceauthorization",
"backchannel_authentication_endpoint": "http://idp.partner.example/connect/ciba",
"backchannel_token_delivery_modes_supported": [
"poll",
"ping",
"push"
],
"backchannel_user_code_parameter_supported": true,
"backchannel_authentication_request_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512"
],
"pushed_authorization_request_endpoint": "http://idp.partner.example/connect/par",
"scopes_supported": [
"openid",
"profile",
"email",
"api",
"api.admin",
"api.metrics",
"reports",
"IdentityServerApi",
"ledger.read",
"orders.write",
"offline_access"
],
"claims_supported": [
"sub",
"name",
"family_name",
"given_name",
"middle_name",
"nickname",
"preferred_username",
"profile",
"picture",
"website",
"gender",
"birthdate",
"zoneinfo",
"locale",
"updated_at",
"email",
"email_verified"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"dpop_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512"
],
"request_object_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512"
],
"response_types_supported": [
"code"
],
"response_modes_supported": [
"form_post",
"query",
"fragment"
],
"authorization_response_iss_parameter_supported": true,
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:openid:params:grant-type:ciba",
"urn:fdyay:params:oauth:grant-type:demo-delegation",
"urn:ietf:params:oauth:grant-type:token-exchange"
],
"code_challenge_methods_supported": [
"S256"
],
"token_endpoint_auth_methods_supported": [
"private_key_jwt",
"tls_client_auth",
"self_signed_tls_client_auth",
"client_secret_basic",
"client_secret_post"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512"
],
"revocation_endpoint_auth_methods_supported": [
"private_key_jwt",
"tls_client_auth",
"self_signed_tls_client_auth",
"client_secret_basic",
"client_secret_post"
],
"revocation_endpoint_auth_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512"
],
"introspection_endpoint_auth_methods_supported": [
"private_key_jwt",
"tls_client_auth",
"self_signed_tls_client_auth",
"client_secret_basic",
"client_secret_post"
],
"introspection_endpoint_auth_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512"
]
}
Authorization Server → Upstream Provider
GET http://idp.partner.example/.well-known/openid-configuration/jwks HTTP/1.1
Upstream Provider → Authorization Server
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "{key_id_1}",
"alg": "RS256",
"n": "{rsa_modulus_1}",
"e": "AQAB"
}
]
}
Authorization Server → Upstream Provider
POST http://idp.partner.example/connect/par HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=gateway
&redirect_uri=http%3A%2F%2Flocalhost%2Fsignin-partner
&response_type=code
&scope=openid+profile+email
&code_challenge={upstream_code_challenge_1}
&code_challenge_method=S256
&response_mode=form_post
&nonce={upstream_nonce_1}
&state={upstream_state_1}
&client_secret=gateway-secret
Upstream Provider → Authorization Server
HTTP/1.1 201 Created
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"request_uri": "{request_uri_1}",
"expires_in": 600
}
Authorization Server → Browser
HTTP/1.1 302 Found
Location: http://idp.partner.example/connect/authorize?client_id=gateway
&request_uri={request_uri_1}
Set-Cookie: .AspNetCore.OpenIdConnect.Nonce.{nonce_cookie_1}=N; expires={timestamp}; path=/signin-partner; samesite=none; httponly
Set-Cookie: .AspNetCore.Correlation.{correlation_id_1}=N; expires={timestamp}; path=/signin-partner; samesite=none; httponly
4. The provider asks the user to sign in
Nobody is signed in at the provider either.
Browser → Upstream Provider
GET http://idp.partner.example/connect/authorize?client_id=gateway
&request_uri={request_uri_1} HTTP/1.1
Upstream Provider → Browser
HTTP/1.1 302 Found
Location: /account/login?returnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dgateway%26request_uri%3D{request_uri_1}
The user signs in at the provider's sign-in page, as in the authorization code flow.
5. The provider answers with a code
By form_post, the handler's default: a page whose form the browser posts to this server's callback, so the code never appears in a URL.
Browser → Upstream Provider
GET http://idp.partner.example/connect/authorize?client_id=gateway
&request_uri={request_uri_1} HTTP/1.1
Cookie: .AspNetCore.Antiforgery.{suffix}={antiforgery_cookie_1}; idsrv={authentication_cookie_1}
Upstream Provider → Browser
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
Set-Cookie: idsrv.session={session_id_1}; path=/; secure; samesite=none
Set-Cookie: idsrv={authentication_cookie_2}; path=/; samesite=none; httponly
(an HTML page, not shown, that posts this form to http://localhost/signin-partner as it loads:)
code={authorization_code_1}
state={upstream_state_1}
session_state={session_state_1}
iss=http://idp.partner.example
6. The browser posts the code to this server's callback
The handler redeems it at the provider's token endpoint with its client secret and the PKCE verifier, validates the identity token it gets — signature, issuer, audience, nonce — and keeps the user in the external cookie, idsrv.external, for the external sign-in page.
Browser → Authorization Server
POST /signin-partner HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: .AspNetCore.OpenIdConnect.Nonce.{nonce_cookie_1}=N; .AspNetCore.Correlation.{correlation_id_1}=N
code={authorization_code_1}
&state={upstream_state_1}
&session_state={session_state_1}
&iss=http%3A%2F%2Fidp.partner.example
Authorization Server → Upstream Provider
POST http://idp.partner.example/connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
client_id=gateway
&client_secret=gateway-secret
&code={authorization_code_1}
&grant_type=authorization_code
&redirect_uri=http%3A%2F%2Flocalhost%2Fsignin-partner
&code_verifier={upstream_code_verifier_1}
Upstream Provider → Authorization Server
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"access_token": "{access_token_1}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email",
"id_token": "{id_token_1}"
}
{access_token_1} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "at+jwt"
}
{
"iss": "http://idp.partner.example",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 3600},
"jti": "{jti_1}",
"client_id": "gateway",
"scope": "email openid profile",
"sub": "alice",
"auth_time": {auth_time},
"idp": "local",
"amr": [
"pwd"
],
"sid": "{session_id_1}"
}
{id_token_1} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "JWT"
}
{
"aud": [
"gateway"
],
"iss": "http://idp.partner.example",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 300},
"jti": "{jti_2}",
"azp": "gateway",
"sub": "alice",
"auth_time": {auth_time},
"idp": "local",
"amr": [
"pwd"
],
"name": "Alice Smith",
"email": "alice@example.com",
"nonce": "{upstream_nonce_1}",
"sid": "{session_id_1}",
"at_hash": "{at_hash_1}"
}
Authorization Server → Browser
HTTP/1.1 302 Found
Location: /Account/ExternalLogin?handler=Callback
Cache-Control: no-cache,no-store
Pragma: no-cache
Set-Cookie: .AspNetCore.Correlation.{correlation_id_1}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/signin-partner; samesite=none; httponly
Set-Cookie: .AspNetCore.OpenIdConnect.Nonce.{nonce_cookie_1}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/signin-partner; samesite=none; httponly
Set-Cookie: idsrv.external={cookie_1}; path=/; samesite=none; httponly
7. The external sign-in page signs the user in here
It finds the local user linked to the provider's subject — partner-external and alice — or provisions one, signs them in to this server's session with idp=partner-external, drops the external cookie and resumes the authorize request.
Browser → Authorization Server
GET /Account/ExternalLogin?handler=Callback HTTP/1.1
Cookie: idsrv.external={cookie_1}
Authorization Server → Browser
HTTP/1.1 302 Found
Location: /connect/authorize?client_id=partner-web
&response_type=code
&scope=openid%20profile
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4
&code_challenge_method=S256
&acr_values=idp%3Apartner-external
Cache-Control: no-cache,no-store
Pragma: no-cache
Set-Cookie: idsrv={authentication_cookie_3}; path=/; samesite=none; httponly
Set-Cookie: idsrv.external=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=none; httponly
8. The authorize endpoint issues this server's code
The client sees one sign-in, at the server it trusts.
Browser → Authorization Server
GET /connect/authorize?client_id=partner-web
&response_type=code
&scope=openid%20profile
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4
&code_challenge_method=S256
&acr_values=idp%3Apartner-external HTTP/1.1
Cookie: idsrv={authentication_cookie_3}
Authorization Server → Browser
HTTP/1.1 302 Found
Location: https://client.example.com/callback?code={authorization_code_2}
&state=af0ifjsldkj
&session_state={session_state_2}
&iss=http%3A%2F%2Flocalhost
Cache-Control: no-cache,no-store
Pragma: no-cache
Set-Cookie: idsrv.session={session_id_2}; path=/; secure; samesite=none
Set-Cookie: idsrv={authentication_cookie_4}; path=/; samesite=none; httponly
9. The client redeems the code
The identity token names the local user — a subject of this server's own, not the provider's — and says where they signed in: idp is the provider's scheme and amr is external.
Client → Authorization Server
POST /connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code={authorization_code_2}
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&client_id=partner-web
&code_verifier=documented-flow-code-verifier-0123456789-abcdefghij
Authorization Server → Client
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"access_token": "{access_token_2}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile",
"id_token": "{id_token_2}"
}
{access_token_2} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_2}",
"typ": "at+jwt"
}
{
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 3600},
"jti": "{jti_3}",
"client_id": "partner-web",
"scope": "openid profile",
"sub": "{local_subject_1}",
"auth_time": {auth_time},
"idp": "partner-external",
"amr": [
"external"
],
"sid": "{session_id_2}"
}
{id_token_2} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_2}",
"typ": "JWT"
}
{
"aud": [
"partner-web"
],
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 300},
"jti": "{jti_4}",
"azp": "partner-web",
"sub": "{local_subject_1}",
"auth_time": {auth_time},
"idp": "partner-external",
"amr": [
"external"
],
"name": "Alice Smith",
"nonce": "n-0S6_WzA2Mj",
"sid": "{session_id_2}",
"at_hash": "{at_hash_2}"
}