Filip Kajetaniak
04/03/2026, 1:01 PMDevRev
04/03/2026, 1:02 PMsu: true claim to your session JWT. If you're adding custom claims during step-up flows, these get added to the token as well. However, multiple concurrent step-up flows do not automatically merge their custom claims; each successful step-up updates the session token.
Securing Fine-Grained Actions
For your specific use case (X EUR transaction to Y IBAN), consider this approach:
1. Action-Specific Custom Claims
Use custom claims that are specific to the intended action:
// During step-up flow for specific transaction
const loginOptions = {
stepup: true,
customClaims: {
"approved_transaction_id": "txn_12345",
"approved_amount": "1000_EUR",
"approved_iban": "DE89370400440532013000",
"expires_at": "2024-01-01T12:00:00Z"
}
}
2. Backend Validation
Your backend should:
• Verify the su: true claim exists
• Check that custom claims match the exact transaction being attempted
• Validate claims haven't expired
• Consume/invalidate claims after use
3. Secure vs Non-Secure Claims
Custom claims added through Flows are marked as secure (not in the nsec claim), meaning they're trusted by your backend. Claims added by client SDKs would be in the nsec claim and shouldn't be trusted for authorization.
Best Practices
• Set short expiration times for transaction-specific claims
• Include unique transaction identifiers in claims
• Validate all claim details match the attempted action exactly
• Consider using single-use tokens for high-value transactions
The key is treating step-up custom claims as temporary, action-specific permissions rather than persistent authorizations.Steffen Hanikel
04/03/2026, 1:03 PMAkash Jivane
04/03/2026, 1:41 PMnsec and shouldn't be trusted for authz):
{
"approved_transaction_id": "txn_abc123",
"approved_amount_eur": 1000,
"approved_iban": "DE89370400440532013000"
}
Your backend then validates that these claims exactly match the action being requested. Even if the session token gets overwritten, the action-specific token you captured locally still has the right claims — and the backend rejects anything that doesn't match exactly.