Token Exchange Policy Lambda
On this page
This feature requires an Enterprise plan.
1.70.0
A Token Exchange Policy lambda decides whether FusionAuth allows a token exchange request, and which audiences, scopes, and lifetime the issued token receives. FusionAuth runs this lambda for every exchange in a Tenant that uses the Lambda policy engine. It runs after FusionAuth authenticates the exchanger and validates the subject token.
Lambda Structure#
The lambda must contain a function named policy with the following signature:
function policy(result, exchange, context) {
// Lambda code goes here
}
The function must declare all three parameters, even if it doesn't use them:
result: the object the lambda populates with its decision. This object is mutable.exchange: information about the exchange request. This object is read-only.context: information about the Tenant, plus access to secrets. This object is read-only.
The Admin UI fills a new lambda of this type with an example implementation.
The exchange Object#
clientIdThe client Id of the exchanger Entity.
requestedAudiencesAndResourcesThe values of the audience and resource request parameters, in that order. Empty when the request includes neither.
requestedScopesThe scopes from the scope request parameter. Empty when the request doesn't include scope.
subjectTokenClaimsThe claims of the validated subject token, such as sub, iss, aud, scope, and act. Claim values keep the types they have in the token. For example, scope is a space-delimited string.
subjectTokenValidationStatusHow FusionAuth validated the subject token. The possible values are:
AUTHORITATIVE: the subject token is an access token that this Tenant issued, verified with FusionAuth's own keys. FusionAuth uses this value whensubject_token_typeisurn:ietf:params:oauth:token-type:access_token.BEST_EFFORT: the subject token is a JWT from an issuer in the Tenant's Allowed issuers, verified with keys from the issuer's OpenID Connect discovery document. FusionAuth uses this value whensubject_token_typeisurn:ietf:params:oauth:token-type:jwt.
The context Object#
tenantIdThe Id of the Tenant processing the exchange.
services.secrets Provides access to secrets through context.services.secrets.get('<secret-name>').
The result Object#
The lambda sets result.decision to ALLOW or DENY, then populates result.granted or result.denial.
To allow an exchange, set all three fields of result.granted:
granted.aud The audiences of the issued token.
granted.scope The scopes of the issued token, as a space-delimited string or an array. To grant no scopes, use an empty array.
granted.expires_in The lifetime of the issued token, in seconds.
result.decision = 'ALLOW';
result.granted = {
aud: ['email-service'],
scope: ['email:send'],
expires_in: 300
};
To deny an exchange, set both fields of result.denial:
denial.error_code The OAuth error code to return to the client. The possible values are invalid_request, invalid_target, and unauthorized_client.
denial.error_description A description of the denial to return to the client.
result.decision = 'DENY';
result.denial = {
error_code: 'unauthorized_client',
error_description: 'This client may not exchange tokens for the requested audience.'
};
FusionAuth responds to a denied exchange with a 403 status code. The response contains the error_code value in error, the error_description value, and an error_reason of lambda_denied.
Limits on the Grant#
FusionAuth applies the following rules to an ALLOW decision before it builds the token:
- FusionAuth drops any value in
granted.audthat isn't inexchange.requestedAudiencesAndResources. If no values remain, FusionAuth denies the exchange with aninvalid_targeterror. - When the subject token has a
scopeclaim, FusionAuth drops any value ingranted.scopethat the subject token'sscopeclaim doesn't contain. If the request includedscopeand no granted scopes remain, FusionAuth denies the exchange with aninvalid_scopeerror. - FusionAuth sets the token lifetime to
granted.expires_inor the default lifetime, whichever is shorter. The default lifetime is the exchanger's Entity Type JWT duration when the Entity Type has JWT configuration enabled, and the Tenant's JWT duration otherwise.
FusionAuth writes an Information entry to the Event Log listing any audiences and scopes it dropped.
Errors#
FusionAuth treats the following as a lambda error and rejects the exchange:
- The lambda throws an exception.
result.decisionis neitherALLOWnorDENY.- An
ALLOWdecision is missinggrantedor any of its fields. - A
DENYdecision is missingdenialor either of its fields, or uses an unsupportederror_code.
FusionAuth responds to a lambda error with a 400 status code and an invalid_request error, and records the attempt in the token exchange audit log with the POLICY_LAMBDA_ERROR outcome. The response doesn't include exception details. To see them, enable debugging on the lambda.
Assigning the Lambda#
To assign the lambda in the Admin UI:
- Navigate to Tenants and click on the Tenant.
- Select the OAuth tab.
- In the Token exchange settings section, set Token exchange policy engine to
Lambda. - Select the lambda in Token exchange policy lambda.
- Click .
To assign the lambda with the Tenant API, set tenant.tokenExchangeConfiguration.policyEngine to LAMBDA and tenant.tokenExchangeConfiguration.policyEngineLambdaId to the lambda's Id.
Examples#
Allow Specific Exchangers for Each Audience#
This lambda maps each exchanger to the one audience it may call and the scopes it may receive. It grants the intersection of the requested scopes and the allowed scopes:
function policy(result, exchange, context) {
const rules = {
'todo-service': { audience: 'email-service', scopes: ['email:send'] },
'billing-service': { audience: 'invoice-service', scopes: ['invoice:read', 'invoice:write'] }
};
const rule = rules[exchange.clientId];
if (!rule) {
result.decision = 'DENY';
result.denial = {
error_code: 'unauthorized_client',
error_description: 'This client may not exchange tokens.'
};
return;
}
if (exchange.requestedAudiencesAndResources.indexOf(rule.audience) === -1) {
result.decision = 'DENY';
result.denial = {
error_code: 'invalid_target',
error_description: 'The requested audience is not permitted for this client.'
};
return;
}
result.decision = 'ALLOW';
result.granted = {
aud: [rule.audience],
scope: exchange.requestedScopes.filter(scope => rule.scopes.indexOf(scope) !== -1),
expires_in: 300
};
}
Limit Tokens From External Issuers#
This lambda accepts JWTs from external issuers, but only issues short-lived, read-only tokens for them:
function policy(result, exchange, context) {
const external = exchange.subjectTokenValidationStatus !== 'AUTHORITATIVE';
result.decision = 'ALLOW';
result.granted = {
aud: exchange.requestedAudiencesAndResources,
scope: external ? ['read'] : exchange.requestedScopes,
expires_in: external ? 60 : 900
};
}
Call an External Authorization Service#
This lambda asks an external service like FusionAuth FGA by Permify whether to allow the exchange. It reads the service's API key from a secret and calls the service with Lambda HTTP Connect:
function policy(result, exchange, context) {
const response = fetch('https://authz.example.com/api/exchange', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + context.services.secrets.get('authz-api-key'),
'Content-Type': 'application/json'
},
body: JSON.stringify({
exchanger: exchange.clientId,
subject: exchange.subjectTokenClaims.sub,
audiences: exchange.requestedAudiencesAndResources,
scopes: exchange.requestedScopes
})
});
if (response.status !== 200) {
result.decision = 'DENY';
result.denial = {
error_code: 'invalid_request',
error_description: 'The authorization service denied the exchange.'
};
return;
}
// assumes a response like {"aud": [...], "scope": [...], "expires_in": 300}
const decision = JSON.parse(response.body);
result.decision = 'ALLOW';
result.granted = {
aud: decision.aud,
scope: decision.scope,
expires_in: decision.expires_in
};
}
Debugging#
To troubleshoot a policy lambda:
- Enable debugging on the lambda to write
console.debug()output and exception details to the Event Log. See Console. - Enable Enable debug logging in the Tenant's token exchange settings to write the reason for each failed exchange to the Event Log.
- Review the outcome and policy decision for each attempt in the Admin UI under System -> Token Exchange Log , or with the Search the Token Exchange Audit Log API.
Related
API documentation for the FusionAuth Retrieve a Token Exchange Chain API.
API documentation for the FusionAuth Search the Token Exchange Audit Log API.
Reference for the Token Exchange Populate lambda, which adds or narrows claims in tokens issued through token exchange.
Learn about the Token Exchange event.