Repository: logto-io/logto
File: packages/core/src/oidc/extra-token-claims.ts (TypeScript)

Code before:
) => {
  // Session UID and account ID are required to fetch the interaction data.
  if (!accountId || !sessionUid) {
    return;
  }

  const { oidcSessionExtensions } = queries;
  const sessionExtension = await oidcSessionExtensions.findBySessionUid(sessionUid);
  if (!sessionExtension || sessionExtension.accountId !== accountId) {
    return;
  }

  const { lastSubmission } = sessionExtension;
  const interactionData = jwtCustomizerUserInteractionContextGuard.safeParse(lastSubmission);
  const sessionContextData = jwtCustomizerSessionContextGuard.safeParse(
    getSessionContextFromLastSubmission(lastSubmission)
  );

  if (!interactionData.success) {
    return;
  }

  return {
    interactionContext: interactionData.data,
    ...conditional(sessionContextData.success && { sessionContext: sessionContextData.data }),
  };
};

/**
 * Safely retrieves the associated subject token for a given access token.
 *
 * - Only processes tokens issued via the Token Exchange grant type.
 * - Safely parses the `extra` field of the access token to extract the `subjectTokenId`.
 * - Fetches the subject token if the `subjectTokenId` is present and valid.
 */
const getAssociatedSubjectToken = async (queries: Queries, token: AccessToken) => {
  if (token.gty !== GrantType.TokenExchange) {
    return;
  }

  const tokenExtraResult = z
    .object({
      subjectTokenId: z.string(),
    })
    .safeParse(token.extra);

  if (!tokenExtraResult.success) {
    return;
  }

  return queries.subjectTokens.findSubjectToken(tokenExtraResult.data.subjectTokenId);

Review comment (suggestion):
`interactionContext` represents the most recently submitted interaction session. Introducing a new, more general `sessionContext` can be confusing. In OIDC, an `interaction` specifically refers to the user’s interaction session, whereas a `session` typically represents the user’s login state under the Logto domain. We should consider using a more precise name, or alternatively, placing all new fields under `interactionContext` to avoid ambiguity.

Code after:
  const { lastSubmission } = sessionExtension;
  const interactionData = jwtCustomizerUserInteractionContextGuard.safeParse(lastSubmission);

  if (!interactionData.success) {
    return;
  }

  return interactionData.data;
};

/**
 * Safely retrieves the associated subject token for a given access token.
 *
 * - Only processes tokens issued via the Token Exchange grant type.
 * - Safely parses the `extra` field of the access token to extract the `subjectTokenId`.
 * - Fetches the subject token if the `subjectTokenId` is present and valid.
 */
const getAssociatedSubjectToken = async (queries: Queries, token: AccessToken) => {
  if (token.gty !== GrantType.TokenExchange) {
    return;
  }

  const tokenExtraResult = z
    .object({
      subjectTokenId: z.string(),
    })
    .safeParse(token.extra);

  if (!tokenExtraResult.success) {
    return;
  }

  return queries.subjectTokens.findSubjectToken(tokenExtraResult.data.subjectTokenId);
};

/* eslint-disable complexity */
export const getExtraTokenClaimsForJwtCustomization = async (
  ctx: KoaContextWithOIDC & WithLogContext,
  token: unknown,
  {
    envSet,
    queries,
    libraries,
    logtoConfigs,
  }: {
    envSet: EnvSet;
    queries: Queries;
    libraries: Libraries;
    logtoConfigs: LogtoConfigLibrary;
  }
): Promise<UnknownObject | undefined> => {