Table of Contents

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}"
}