Pushed Authorization Request (PAR) & Step-up Authentication
Pushed Authorization Request (PAR) is defined in RFC 9126. It allows a client to push its authorization parameters directly to the authorization server — before redirecting the user — receiving back a short-lived request_uri reference. This eliminates sensitive parameters from the browser URL and enables richer payloads such as authorization_details (RFC 9396) to be transmitted securely over a back-channel.
LoginRadius extends PAR to support Step-up Customer Authentication (SCA): a pattern where an already-authenticated user must re-verify their identity before completing a sensitive operation (for example, a high-value payment or account change).
Key Concepts
| Concept | Description |
|---|---|
request_uri | Opaque reference (urn:ietf:params:oauth:request_uri:<token>) returned by the PAR endpoint. Valid for 10 minutes. |
authorization_details | Structured JSON object (RFC 9396) describing the authorization context — e.g. payment amount, payee, currency. Passed by the client at PAR time and propagated through the entire flow into the final JWT. |
linking_id | Server-generated UUID created during PAR. Acts as a stable correlation token that ties the push notification challenge back to the SCA session. Included in the push payload so the mobile app can poll for authorization status. |
challenge | Server-generated HMAC value cryptographically binding the authentication act to the specific transaction (amount + payee). Computed as base64(HMAC-SHA256(tokenSeed, linkingId + "|" + sha256(authorization_details))). Sent in the push notification payload, returned from the GET endpoint, and must be included in the mobile app's RSA signature. |
| Re-auth flow | When linking_id is present in the workflow session AND the workflow was initiated from an OIDC /authorize call, LoginRadius skips creating a new RAAS session after MFA verification — the final token is issued by the OIDC /token endpoint instead. |
End-to-End Flow
Step-by-Step Walkthrough
Step 1 — Push Authorization Request (PAR)
The client makes a back-channel POST to the PAR endpoint with all authorization parameters, including the authorization_details payload.
Endpoint
POST https://{SiteURL}/api/oidc/{OIDCAppName}/par
Request
POST /api/oidc/MyApp/par
Content-Type: application/x-www-form-urlencoded
client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&response_type=code
&scope=openid profile
&redirect_uri=https://your-app.com/callback
&authorization_details=[{"type":"lr_payment","amount":"500","currency":"EUR","payee":"Stripe"}]
Response
{
"request_uri": "urn:ietf:params:oauth:request_uri:abc123xyz",
"expires_in": 600
}
The server also generates a
linking_id(UUID) internally at this step and stores it alongside the PAR state in Redis. It is not returned to the client in this response — it flows through the rest of the pipeline automatically.
PAR Request Parameters
| Parameter | Required | Description |
|---|---|---|
client_id | ✅ Required | Your OIDC application client ID |
client_secret | ✅ Required | Your OIDC application client secret |
response_type | ✅ Required | Must be code |
scope | ✅ Required | Space-delimited scopes. Must include openid. |
redirect_uri | ✅ Required | Callback URL. Must be whitelisted in Admin Console. |
authorization_details | Optional | JSON array (RFC 9396) describing the authorization context. Propagated into the final JWT authorization_details claim. |
nonce | Optional | Replay attack prevention for id_token. |
code_challenge | Optional | PKCE code challenge. |
code_challenge_method | Optional | PKCE method. Must be S256. |
Step 2 — Authorization Request with request_uri
The client redirects the user's browser to /authorize using the request_uri reference instead of inline parameters.
Endpoint
GET https://{SiteURL}/service/oidc/{OIDCAppName}/authorize
Example Request
GET https://your-app.hub.loginradius.com/service/oidc/MyApp/authorize
?request_uri=urn:ietf:params:oauth:request_uri:abc123xyz
&client_id=YOUR_CLIENT_ID
What happens internally:
- LoginRadius resolves the
request_uri— strips theurn:ietf:params:oauth:request_uri:prefix, looks up the PAR state from Redis usingappId + clientId + tokenas the key. - The full
OidcParams(includingauthorization_detailsandlinking_id) are unpacked. - These are encoded into a short-lived
IOFederationStateJWT (thestateparameter) and the user is redirected to the Identity Orchestration workflow URL.
Step 3 — Identity Orchestration Workflow
The workflow URL receives the state JWT. LoginRadius decodes it and makes linking_id and authorization_details available throughout the workflow session.
Workflow Session Data
| Key | Where Available | Value |
|---|---|---|
SharedState["linking_id"] | All workflow nodes and scripts | UUID generated during PAR |
SharedState["authorization_details"] | All workflow nodes and scripts | Raw JSON string of the authorization_details object |
SharedState["type"], SharedState["amount"], etc. | All workflow nodes and scripts | Individual fields from authorization_details, flattened for easy access in scripts |
Recommended SCA Workflow Structure
Has Session
├── true → Custom Script (read user context from linking_id/SharedState)
│ → Identity Lookup (resolves user from session cookie)
│ → MFA State (push configured?)
│ ├── pushauth → Send Push Notification → Verify Push → ✅ Success
│ └── passkey → Verify Passkey → ✅ Success
└── false → Child Workflow (login flow) → loop back
The
Has Sessionnode is critical. Itstruebranch runs only when the user already has an activelr-session-tokencookie — which is expected in SCA flows. Thefalsebranch redirects the user to authenticate first.
Re-auth Detection
A workflow MFA node is considered a re-auth (SCA) flow when both of the following are true:
- The workflow was initiated from an OIDC
/authorizecall (the outbound OIDC session is present in the workflow session). SharedState["linking_id"]is non-empty.
When re-auth is detected, LoginRadius skips creating a new RAAS session after MFA verification. The final access token is issued by the OIDC /token endpoint instead. This applies to all MFA verification nodes: Push, Passkey, OTP (Email & SMS), and Duo.
Do not manually set linking_id in SharedState via a custom script. LoginRadius uses the combination of the outbound OIDC session and linking_id to gate the re-auth path. A script-set linking_id without a valid OIDC session will not trigger re-auth (the guard checks both conditions), but it could cause unexpected behavior in other nodes that read linking_id.
Step 4 — Push Notification with authorization_details
When the workflow reaches the Send Push Notification node, LoginRadius includes linking_id and the display title/message in the push payload sent to the mobile device.
Push Notification Payload (delivered to mobile app)
{
"title": "Confirm Payment",
"message": "Confirm Payment",
"secondfactorauthenticationtoken": "<2fa-token>",
"browser": "Chrome",
"location": "Toronto, Canada",
"timestamp": 1714000000,
"identifier": "user@example.com",
"apikey": "<app-api-key>",
"linking_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"challenge": "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols="
}
linking_idandchallengeare only present for SCA flows (whenauthorization_detailswere passed at PAR time). Standard push MFA flows are unaffected.
The linking_id field allows the mobile app to call the authorization details endpoint to display the full payment/authorization context to the user before they approve or decline. The challenge field is a server-generated HMAC that cryptographically binds the user's approval to the specific transaction — the app must include it in the RSA signature when approving.
Step 5 — Get Authorization Details (Mobile App Polling)
The mobile app uses the linking_id from the push payload to fetch the authorization_details and display them to the user.
Endpoint
GET https://{SiteURL}/login/2fa/par/{linking_id}
Headers
| Header | Value |
|---|---|
X-LoginRadius-Apikey | Your LoginRadius API key |
Example Request
GET /login/2fa/par/f47ac10b-58cc-4372-a567-0e02b2c3d479
X-LoginRadius-Apikey: YOUR_API_KEY
Success Response
{
"authorization_details": {
"type": "lr_payment",
"amount": "500",
"currency": "EUR",
"payee": "Stripe"
},
"challenge": "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols="
}
The
challengevalue returned here matches the one in the push notification payload. The mobile app should displayauthorization_detailsto the user and usechallengewhen computing the approval signature.
Error Responses
| Error Code | Description |
|---|---|
15 | linking_id not found — no 2FA session exists for this identifier |
17 | 2FA token has expired |
This endpoint validates the
linking_idby looking it up in the active 2FA session store. It does not require a user access token — only the app API key.
Step 6 — Push Verification & Re-auth Completion
The mobile app calls the push update endpoint to approve or decline.
Endpoint
PUT https://{SiteURL}/login/2fa/push
Mobile App Signing (SCA flows)
When challenge is present in the push notification, the mobile app must include it in the RSA-signed message:
| Flow | Signed message |
|---|---|
| Standard push MFA (no challenge) | SHA256("Approved" + secondfactorauthenticationtoken) |
| SCA push (challenge present) | SHA256("Approved" + secondfactorauthenticationtoken + challenge) |
The PUT request body remains the same in both cases — only the signature computation changes:
{
"verify": "Approved",
"secondfactorauthenticationtoken": "<2fa-token>",
"signature": "<base64-rsa-signature>"
}
The app does not need to send
challengeback in the request body. The server retrieves the stored challenge from its own database and uses it for signature verification.
After the user approves, the workflow's Verify Push node detects re-auth (linking_id present + outbound OIDC session present), skips DoLogin, and completes the workflow successfully. The user is redirected back to /authorize with an authorization_code.
Step 7 — Token Exchange
The client exchanges the authorization_code for tokens. The authorization_details from the PAR request are included as a claim in the returned JWT.
Endpoint
POST https://{SiteURL}/api/oidc/{OIDCAppName}/token
Success Response
{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "<JWT with authorization_details claim>",
"refresh_token": "<refresh-token>"
}
Decoded id_token payload (example)
{
"sub": "user-uid",
"iss": "https://your-app.hub.loginradius.com/",
"aud": "YOUR_CLIENT_ID",
"iat": 1714000000,
"exp": 1714003600,
"authorization_details": {
"type": "lr_payment",
"amount": "500",
"currency": "EUR",
"payee": "Stripe"
},
"linking_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
Data Flow Summary
Client PAR Request
→ authorization_details (JSON string)
→ linking_id (generated server-side, UUID)
│
▼
Redis (PAR state, keyed by appId:clientId:token, TTL 10min)
│
▼
/authorize → IOFederationState JWT (state param in workflow URL)
│
▼
Workflow SharedState
├── SharedState["linking_id"] = "f47ac10b-..."
├── SharedState["authorization_details"] = "{\"type\":\"lr_payment\",...}"
├── SharedState["type"] = "lr_payment" ← flattened
├── SharedState["amount"] = "500" ← flattened
└── SharedState["currency"] = "EUR" ← flattened
│
├── UserInfo["linkingId"] → SecondFactorToken.LID (MongoDB)
├── UserInfo["authorizationDetails"] → SecondFactorToken.AD (MongoDB)
├── UserInfo["scaChallenge"] → SecondFactorToken.CHG (MongoDB)
│ challenge = HMAC-SHA256(tokenSeed, linkingId + "|" + sha256(authDetails))
└── PushNotification { linking_id, challenge } → Mobile App
│
▼
GET /login/2fa/par/{linking_id}
→ { authorization_details, challenge }
│
▼
Mobile App signs:
SHA256("Approved" + 2faToken + challenge)
with RSA private key (never leaves device)
│
▼
PUT /login/2fa/push { verify, 2faToken, signature }
→ Server verifies RSA signature using stored CHG
│
▼
/token → id_token JWT claim: authorization_details + linking_id
Differences from Standard Authorization Code Flow
| Aspect | Standard Auth Code Flow | PAR + SCA Flow |
|---|---|---|
| Authorization params location | Browser URL (query string) | Back-channel POST to /par |
authorization_details support | Not available | Available via RFC 9396 |
| User session requirement | Not required (fresh login) | User must already be authenticated (Has Session = true) |
| MFA outcome | New RAAS session created | Re-auth: no new session, OIDC /token issues final JWT |
linking_id | Not present | Server-generated; in push payload + JWT claim |
| Dynamic linking (PSD2 SCA) | Not applicable | challenge cryptographically binds user's RSA signature to the specific amount + payee |
| Mobile app context | Not applicable | GET /login/2fa/par/{linking_id} returns payment/action details + challenge |