Skip to content

Interactions

This content is for v8.x. Switch to the latest version for up-to-date documentation.

Holds the configuration for interaction policy and a URL to send end-users to when the policy decides to require interaction.

structure of Prompts and their checks formed by Prompt and Check class instances. The default you can get a fresh instance for and the classes are available under Provider.interactionPolicy.

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 = get(oidc.claims, 'id_token.acr', {});
if (!request || !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 = get(oidc.claims, 'id_token.acr', {});
if (!request || !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' in oidc.result))
) {
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' in oidc.result))) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ rar: JSON.parse(oidc.params.authorization_details) })),
)
]

(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.
(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 silenty “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.

(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

Function used to determine where to redirect User-Agent for necessary interaction, can return both absolute and relative urls.

default value:

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