Skip to content

Interactions

End-User Interaction Policy

Specifies the configuration for interaction policy and end-user redirection that shall be applied to determine when user interaction is required during the authorization process. This configuration enables customization of authentication and consent flows according to deployment-specific requirements.


Specifies the structure of Prompts and their associated checks that shall be applied during authorization request processing. The policy is formed by Prompt and Check class instances that define the conditions under which user interaction is required. The default policy implementation provides a fresh instance that can be customized, and the relevant classes are exported for configuration purposes.

default value:

[
/* LOGIN PROMPT */
new Prompt(
{ name: 'login', requestable: true },
(ctx) => {
const { oidc } = ctx;
return {
...(oidc.params.max_age === undefined ? undefined : { max_age: oidc.params.max_age }),
...(oidc.params.login_hint === undefined
? undefined
: { login_hint: oidc.params.login_hint }),
...(oidc.params.id_token_hint === undefined
? undefined
: { id_token_hint: oidc.params.id_token_hint }),
};
},
new Check('no_session', 'End-User authentication is required', (ctx) => {
const { oidc } = ctx;
if (oidc.session.accountId) {
return Check.NO_NEED_TO_PROMPT;
}
return Check.REQUEST_PROMPT;
}),
new Check('max_age', 'End-User authentication could not be obtained', (ctx) => {
const { oidc } = ctx;
if (oidc.params.max_age === undefined) {
return Check.NO_NEED_TO_PROMPT;
}
if (!oidc.session.accountId) {
return Check.REQUEST_PROMPT;
}
if (oidc.session.past(oidc.params.max_age) && (!ctx.oidc.result || !ctx.oidc.result.login)) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}),
new Check(
'id_token_hint',
'id_token_hint and authenticated subject do not match',
async (ctx) => {
const { oidc } = ctx;
if (oidc.entities.IdTokenHint === undefined) {
return Check.NO_NEED_TO_PROMPT;
}
const { payload } = oidc.entities.IdTokenHint;
let sub = oidc.session.accountId;
if (sub === undefined) {
return Check.REQUEST_PROMPT;
}
if (oidc.client.subjectType === 'pairwise') {
sub = await instance(oidc.provider).configuration.pairwiseIdentifier(
ctx,
sub,
oidc.client,
);
}
if (payload.sub !== sub) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
},
),
new Check(
'claims_id_token_sub_value',
'requested subject could not be obtained',
async (ctx) => {
const { oidc } = ctx;
if (
!oidc.claims.id_token
|| !oidc.claims.id_token.sub
|| !('value' in oidc.claims.id_token.sub)
) {
return Check.NO_NEED_TO_PROMPT;
}
let sub = oidc.session.accountId;
if (sub === undefined) {
return Check.REQUEST_PROMPT;
}
if (oidc.client.subjectType === 'pairwise') {
sub = await instance(oidc.provider).configuration.pairwiseIdentifier(
ctx,
sub,
oidc.client,
);
}
if (oidc.claims.id_token.sub.value !== sub) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
},
({ oidc }) => ({ sub: oidc.claims.id_token.sub }),
),
new Check(
'essential_acrs',
'none of the requested ACRs could not be obtained',
(ctx) => {
const { oidc } = ctx;
const request = oidc.claims?.id_token?.acr ?? {};
if (!request?.essential || !request?.values) {
return Check.NO_NEED_TO_PROMPT;
}
if (!Array.isArray(oidc.claims.id_token.acr.values)) {
throw new errors.InvalidRequest('invalid claims.id_token.acr.values type');
}
if (request.values.includes(oidc.acr)) {
return Check.NO_NEED_TO_PROMPT;
}
return Check.REQUEST_PROMPT;
},
({ oidc }) => ({ acr: oidc.claims.id_token.acr }),
),
new Check(
'essential_acr',
'requested ACR could not be obtained',
(ctx) => {
const { oidc } = ctx;
const request = oidc.claims?.id_token?.acr ?? {};
if (!request?.essential || !request?.value) {
return Check.NO_NEED_TO_PROMPT;
}
if (request.value === oidc.acr) {
return Check.NO_NEED_TO_PROMPT;
}
return Check.REQUEST_PROMPT;
},
({ oidc }) => ({ acr: oidc.claims.id_token.acr }),
),
)
/* CONSENT PROMPT */
new Prompt(
{ name: 'consent', requestable: true },
new Check('native_client_prompt', 'native clients require End-User interaction', 'interaction_required', (ctx) => {
const { oidc } = ctx;
if (
oidc.client.applicationType === 'native'
&& oidc.params.response_type !== 'none'
&& (!oidc.result?.consent)
) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}),
new Check('op_scopes_missing', 'requested scopes not granted', (ctx) => {
const { oidc } = ctx;
const encounteredScopes = new Set(oidc.grant.getOIDCScopeEncountered().split(' '));
let missing;
for (const scope of oidc.requestParamOIDCScopes) {
if (!encounteredScopes.has(scope)) {
missing ||= [];
missing.push(scope);
}
}
if (missing?.length) {
ctx.oidc[missingOIDCScope] = missing;
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ missingOIDCScope: oidc[missingOIDCScope] })),
new Check('op_claims_missing', 'requested claims not granted', (ctx) => {
const { oidc } = ctx;
const encounteredClaims = new Set(oidc.grant.getOIDCClaimsEncountered());
let missing;
for (const claim of oidc.requestParamClaims) {
if (!encounteredClaims.has(claim) && !['sub', 'sid', 'auth_time', 'acr', 'amr', 'iss'].includes(claim)) {
missing ||= [];
missing.push(claim);
}
}
if (missing?.length) {
ctx.oidc[missingOIDCClaims] = missing;
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ missingOIDCClaims: oidc[missingOIDCClaims] })),
// checks resource server scopes
new Check('rs_scopes_missing', 'requested scopes not granted', (ctx) => {
const { oidc } = ctx;
let missing;
for (const [indicator, resourceServer] of Object.entries(ctx.oidc.resourceServers)) {
const encounteredScopes = new Set(oidc.grant.getResourceScopeEncountered(indicator).split(' '));
const requestedScopes = ctx.oidc.requestParamScopes;
const availableScopes = resourceServer.scopes;
for (const scope of requestedScopes) {
if (availableScopes.has(scope) && !encounteredScopes.has(scope)) {
missing ||= {};
missing[indicator] ||= [];
missing[indicator].push(scope);
}
}
}
if (missing && Object.keys(missing).length) {
ctx.oidc[missingResourceScopes] = missing;
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ missingResourceScopes: oidc[missingResourceScopes] })),
// checks authorization_details
new Check('rar_prompt', 'authorization_details were requested', (ctx) => {
const { oidc } = ctx;
if (oidc.params.authorization_details && !oidc.result?.consent) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ rar: JSON.parse(oidc.params.authorization_details) })),
)
]

Example: (Click to expand) default interaction policy description.

The default interaction policy consists of two available prompts, login and consent

  • login does the following checks:
    • no_session - checks that there’s an established session, an authenticated end-user
    • max_age - processes the max_age parameter (when the session’s auth_time is too old it requires login)
    • id_token_hint - processes the id_token_hint parameter (when the end-user sub differs it requires login)
    • claims_id_token_sub_value - processes the claims parameter sub (when the claims parameter requested sub differs it requires login)
    • essential_acrs - processes the claims parameter acr (when the current acr is not amongst the claims parameter essential acr.values it requires login)
    • essential_acr - processes the claims parameter acr (when the current acr is not equal to the claims parameter essential acr.value it requires login)
  • consent does the following checks:
    • native_client_prompt - native clients always require re-consent
    • op_scopes_missing - requires consent when the requested scope includes scope values previously not requested
    • op_claims_missing - requires consent when the requested claims parameter includes claims previously not requested
    • rs_scopes_missing - requires consent when the requested resource indicated scope values include scopes previously not requested These checks are the best practice for various privacy and security reasons.
Example: (Click to expand) disabling default consent checks.

You may be required to skip (silently accept) some of the consent checks, while it is discouraged there are valid reasons to do that, for instance in some first-party scenarios or going with pre-existing, previously granted, consents. To simply silently “accept” first-party/resource indicated scopes or pre-agreed-upon claims use the loadExistingGrant configuration helper function, in there you may just instantiate (and save!) a grant for the current clientId and accountId values.

Example: (Click to expand) modifying the default interaction policy.
import { interactionPolicy } from 'oidc-provider';
const { Prompt, Check, base } = interactionPolicy;
const basePolicy = base()
// basePolicy.get(name) => returns a Prompt instance by its name
// basePolicy.remove(name) => removes a Prompt instance by its name
// basePolicy.add(prompt, index) => adds a Prompt instance to a specific index, default is add the prompt as the last one
// prompt.checks.get(reason) => returns a Check instance by its reason
// prompt.checks.remove(reason) => removes a Check instance by its reason
// prompt.checks.add(check, index) => adds a Check instance to a specific index, default is add the check as the last one

Specifies a function that shall be invoked to determine the destination URL for redirecting the User-Agent when user interaction is required during authorization processing. This function enables customization of the interaction endpoint location and may return both absolute and relative URLs according to deployment requirements.

default value:

async function interactionsUrl(ctx, interaction) {
return `/interaction/${interaction.uid}`;
}