CIMD Client Registration Lambda

On this page
Enterprise plan required

This feature requires an Enterprise plan.

Available since 1.70.0

When a CIMD client logs in, FusionAuth fetches the client's metadata document and registers the client just-in-time. Using this lambda function, you can allow or reject that registration, change the scopes and token settings the client gets, or revoke a client that is already registered.

A registration lambda is the only way to filter which clients may register when a CIMD domain uses the * wildcard. Without a lambda that rejects clients you don't recognize, a domain that uses the * wildcard accepts every client that publishes a valid metadata document. See CIMD Security.

The CIMD client registration lambda has access to the following information:

  • the client's metadata document, along with the host and path of its client_id and client_uri URLs
  • the scope and token settings FusionAuth will apply by default, taken from the CIMD domain
  • the resource the client requested
  • metadata about the request, such as the IP address and user agent
  • authentication threats detected for the request, when Advanced Threat Detection is enabled

The lambda doesn't receive the user.

You can use Lambda HTTP Connect to access external resources from this lambda, but you should use external resources sparingly. The lambda runs during the user's login, so any additional latency will impact users.

When The Lambda Runs#

FusionAuth runs the registration lambda after the user logs in, in the following cases:

  • A client registers through the domain for the first time.
  • A user logs in after the client's cached metadata document has expired, and FusionAuth fetches the document again.
  • A registered client requests a resource that the lambda hasn't evaluated since the document was last fetched. This can happen during a login or a refresh token request. Each fetch clears the decisions for every other resource, so after a fetch the lambda runs again the first time the client uses each resource.
  • A registered client requests a new resource during the domain's Grace period, after a fetch of the document has failed. In this case, the lambda evaluates the cached metadata.

The lambda doesn't run while the client's cached document is still valid and the lambda has already evaluated the requested resource. It also doesn't run when a resource server validates a token issued to the client.

Lambda Structure#

All lambda implementations must contain a function with the following signature:

function evaluate(result, clientMetadata, context) {
  // Lambda code goes here
}

The evaluate function must accept the following parameters:

  • result - an object used to return the registration decision to FusionAuth. FusionAuth fills it in with the domain's settings before calling the lambda.
  • clientMetadata - the read-only metadata from the client's document.
  • context - a read-only object containing context for the current request.

result is of type ClientRegistrationLambdaResult, with the following structure:

class ClientRegistrationLambdaResult {
    /**
     * The registration decision. One of ALLOW, DENY, or REVOKE. Defaults to ALLOW.
     * DENY rejects the request. FusionAuth stores nothing, and an existing
     * registration and its refresh tokens are left unchanged.
     * REVOKE deletes the client's registration and its refresh tokens.
     * When the client is registering for the first time, REVOKE rejects the request.
     * @type {string}
     */
    decision;

    /**
     * The Id of the JWT populate lambda that populates the access token and Id token issued to this client.
     * Defaults to the domain's JWT populate lambda. Must be the Id of a JWT populate lambda.
     * Only applies when FusionAuth fetches the document; ignored when the lambda evaluates a new resource.
     * @type {string}
     */
    jwtPopulateLambdaId;

    /**
     * Controls the standard OpenID Connect scopes for this client. Defaults to the domain's provided scope policy.
     * @type {ProvidedScopePolicy}
     */
    providedScopePolicy;

    /**
     * The custom OAuth scopes this client may request. Defaults to the domain's custom scopes.
     * Remove entries to narrow what the client may request, or add entries to grant scopes the domain doesn't define.
     * @type {Array<Scope>}
     */
    scopes;

    /**
     * How long, in seconds, FusionAuth caches the client's metadata document.
     * Defaults to the value FusionAuth derived from the document's Cache-Control header and the domain's settings.
     * FusionAuth limits the value to between 900 seconds and the domain's maximum cache duration.
     * Only applies when FusionAuth fetches the document; ignored when the lambda evaluates a new resource.
     * @type {number}
     */
    ttlInSeconds;
}

class ProvidedScopePolicy {
    /**
     * Each property contains two booleans: `enabled` allows the client to request the scope,
     * and `required` means the user must consent to the scope to complete authorization.
     * @type {{enabled: boolean, required: boolean}}
     */
    address;
    email;
    phone;
    profile;
}

class Scope {
    /**
     * The name of the scope, which the client uses to request it.
     * @type {string}
     */
    name;

    /**
     * A description of the scope for internal use.
     * @type {string}
     */
    description;

    /**
     * The default detail to display on the consent screen if one cannot be found in the theme.
     * @type {string}
     */
    defaultConsentDetail;

    /**
     * The default message to display on the consent screen if one cannot be found in the theme.
     * @type {string}
     */
    defaultConsentMessage;

    /**
     * Whether the user must consent to this scope to complete authorization.
     * @type {boolean}
     */
    required;
}

How FusionAuth Uses Each Result Property#

Some properties apply to a single resource and others apply to the client's whole registration. FusionAuth stores the scopes and providedScopePolicy decisions separately for each resource the client uses. The ttlInSeconds and jwtPopulateLambdaId decisions apply to the whole registration.

Property Applies to When FusionAuth uses it
decision The current request, or the whole registration for REVOKE Every time the lambda runs. DENY rejects the request and leaves any existing registration unchanged. REVOKE deletes the registration and its refresh tokens.
scopes The requested resource Every time the lambda runs. FusionAuth saves the result for that resource.
providedScopePolicy The requested resource Every time the lambda runs. FusionAuth saves the result for that resource.
ttlInSeconds The whole registration Only when FusionAuth fetches the document.
jwtPopulateLambdaId The whole registration Only when FusionAuth fetches the document.

Because of this, the lambda can't change ttlInSeconds or jwtPopulateLambdaId when it runs for a new resource inside the cache window. In that case, FusionAuth ignores both, and the registration keeps the values it got at the last fetch. The lambda's scopes and providedScopePolicy still apply to the new resource. For more on the cache window, see CIMD Registration Caching And Lifecycle.

FusionAuth recalculates a resource's scopes and providedScopePolicy the first time the client uses that resource after each fetch of the document, because a fetch clears them. FusionAuth fetches the document at the first login after the cache window ends. See When A Registration Is Refreshed.

clientMetadata is an immutable argument of type ClientMetadata, with the following structure:

class ClientMetadata {
    /**
     * The client's client_id, which is the URL of its metadata document.
     * @type {string}
     */
    clientId;

    /**
     * The host of the client_id URL, such as `app.example.com`.
     * @type {string}
     */
    clientIdHost;

    /**
     * The path of the client_id URL, such as `/oauth/client.json`.
     * @type {string}
     */
    clientIdPath;

    /**
     * The client_name from the document.
     * @type {string}
     */
    clientName;

    /**
     * The client_uri from the document. This is an optional value and may be null.
     * @type {string}
     */
    clientUri;

    /**
     * The host of the client_uri URL. This is an optional value and may be null.
     * @type {string}
     */
    clientUriHost;

    /**
     * The path of the client_uri URL. This is an optional value and may be null.
     * @type {string}
     */
    clientUriPath;

    /**
     * The redirect_uris from the document.
     * @type {Array<string>}
     */
    redirectUris;
}

context is an immutable argument of type Context, with the following structure:

class Context {
    /**
     * A list of authentication threats detected for this request. This will only be populated when
     * Advanced Threat Detection is enabled.
     * @type {Set<String>} - Possible values are [BlocklistedIp], [BotDetected], [DormantAccount], [DormantPassword],
     * [ImpossibleTravel], [RecentIdentityChange], [RecentPasswordChange], [SuspiciousUserAgent], [UnrecognizedDevice],
     * or [UntrustedDevice].
     */
    authenticationThreats;

    /**
     * Event information about the current request. This is an optional value and may be null.
     * @type {EventInfo} - See the MFA requirement lambda for details
     */
    eventInfo;

    /**
     * The resource the client requested, which is the value of the `resource` request parameter.
     * @type {string}
     */
    resource;

    /**
     * Provides access to services available to the lambda, including secrets.
     * Use `context.services.secrets.get('<key>')` to retrieve a secret value.
     * @type {Services}
     */
    services;
}

For the fields of eventInfo, see the EventInfo class on the MFA requirement lambda page. For more information on authentication threats, see the Advanced Threat Detection documentation.

Failure Handling#

The lambda fails closed. FusionAuth rejects the client when any of the following happens:

  • The lambda throws an error. FusionAuth writes the error to the Event Log.
  • The lambda sets decision to a value other than ALLOW, DENY, or REVOKE. Matching is case-sensitive, but FusionAuth ignores surrounding whitespace.
  • The lambda sets decision to null or deletes it.
  • The lambda sets scopes or providedScopePolicy to null. To remove every custom scope, set scopes to an empty array instead.
  • The lambda sets jwtPopulateLambdaId to an Id that doesn't belong to a JWT populate lambda.

Whenever the lambda rejects a client, on purpose or because it failed, FusionAuth writes an Error entry to the Event Log and sends a CIMD Registration Failed event with the reason lambdaRejected. Neither one says why the lambda rejected the client. To record the reason, call console.info from the lambda, which writes an Event Log entry of type Information.

Assigning The Lambda#

Once a lambda is created, you must assign it to a CIMD domain. Using the FusionAuth admin UI, edit the CIMD domain and find the Registration lambda field on the Lambdas tab. The field only lists lambdas of type CIMD client registration.

Or if you are using the CIMD Domain API, assign the lambda Id to the domain.registrationLambdaId field.

Example Lambdas#

The following example allows only clients hosted on claude.ai or chatgpt.com, which is useful on a CIMD domain that uses the * wildcard:

function evaluate(result, clientMetadata, context) {
  var allowedHosts = ['claude.ai', 'chatgpt.com'];
  if (!allowedHosts.includes(clientMetadata.clientIdHost)) {
    result.decision = 'DENY';
    console.info('Rejected CIMD client ' + clientMetadata.clientId);
  }
}

The following example narrows what one client may request by removing a custom scope and disabling the phone scope:

function evaluate(result, clientMetadata, context) {
  if (clientMetadata.clientIdHost === 'chatgpt.com') {
    result.scopes = result.scopes.filter(function(scope) {
      return scope.name !== 'reports:write';
    });
    result.providedScopePolicy.phone.enabled = false;
  }
}

The following example revokes a specific client. FusionAuth deletes the client's registration and refresh tokens the next time the lambda runs for that client, as described in When The Lambda Runs:

function evaluate(result, clientMetadata, context) {
  var revokedClientIds = ['https://app.example.com/oauth/client.json'];
  if (revokedClientIds.includes(clientMetadata.clientId)) {
    result.decision = 'REVOKE';
  }
}

The following example rejects registrations from requests that look automated. This example assumes you are licensed for the Advanced Threat Detection feature:

function evaluate(result, clientMetadata, context) {
  if (context.authenticationThreats && context.authenticationThreats.includes('BotDetected')) {
    result.decision = 'DENY';
  }
}