mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(core): add subject token context to jwt customizer (#6185)
This commit is contained in:
parent
c1a01d6925
commit
1557c34134
5 changed files with 120 additions and 89 deletions
|
@ -5,6 +5,7 @@ import { useState } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||||
import { type JwtCustomizerForm } from '@/pages/CustomizeJwtDetails/type';
|
import { type JwtCustomizerForm } from '@/pages/CustomizeJwtDetails/type';
|
||||||
import {
|
import {
|
||||||
environmentVariablesCodeExample,
|
environmentVariablesCodeExample,
|
||||||
|
@ -78,7 +79,7 @@ function InstructionTab({ isActive }: Props) {
|
||||||
/>
|
/>
|
||||||
</GuideCard>
|
</GuideCard>
|
||||||
)}
|
)}
|
||||||
{tokenType === LogtoJwtTokenKeyType.AccessToken && (
|
{isDevFeaturesEnabled && tokenType === LogtoJwtTokenKeyType.AccessToken && (
|
||||||
<GuideCard
|
<GuideCard
|
||||||
name={CardType.GrantData}
|
name={CardType.GrantData}
|
||||||
isExpanded={expendCard === CardType.GrantData}
|
isExpanded={expendCard === CardType.GrantData}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { type EditorProps } from '@monaco-editor/react';
|
||||||
import TokenFileIcon from '@/assets/icons/token-file-icon.svg';
|
import TokenFileIcon from '@/assets/icons/token-file-icon.svg';
|
||||||
import UserFileIcon from '@/assets/icons/user-file-icon.svg';
|
import UserFileIcon from '@/assets/icons/user-file-icon.svg';
|
||||||
|
|
||||||
|
import { isDevFeaturesEnabled } from '../../../consts/env.js';
|
||||||
import type { ModelSettings } from '../MainContent/MonacoCodeEditor/type.js';
|
import type { ModelSettings } from '../MainContent/MonacoCodeEditor/type.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -209,9 +210,13 @@ const defaultGrantContext: Partial<JwtCustomizerGrantContext> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultUserTokenContextData = {
|
export const defaultUserTokenContextData = isDevFeaturesEnabled
|
||||||
|
? {
|
||||||
user: defaultUserContext,
|
user: defaultUserContext,
|
||||||
grant: defaultGrantContext,
|
grant: defaultGrantContext,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
user: defaultUserContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accessTokenPayloadTestModel: ModelSettings = {
|
export const accessTokenPayloadTestModel: ModelSettings = {
|
||||||
|
|
|
@ -142,6 +142,11 @@ export const getExtraTokenClaimsForJwtCustomization = async (
|
||||||
(await libraries.jwtCustomizers.getUserContext(token.accountId))
|
(await libraries.jwtCustomizers.getUserContext(token.accountId))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const subjectToken =
|
||||||
|
isTokenClientCredentials || token.gty !== GrantType.TokenExchange
|
||||||
|
? undefined
|
||||||
|
: await trySafe(async () => queries.subjectTokens.findSubjectToken(token.grantId));
|
||||||
|
|
||||||
const payload: CustomJwtFetcher = {
|
const payload: CustomJwtFetcher = {
|
||||||
script,
|
script,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
@ -152,8 +157,18 @@ export const getExtraTokenClaimsForJwtCustomization = async (
|
||||||
tokenType: LogtoJwtTokenKeyType.AccessToken,
|
tokenType: LogtoJwtTokenKeyType.AccessToken,
|
||||||
// TODO (LOG-8555): the newly added `UserProfile` type includes undefined fields and can not be directly assigned to `Json` type. And the `undefined` fields should be removed by zod guard.
|
// TODO (LOG-8555): the newly added `UserProfile` type includes undefined fields and can not be directly assigned to `Json` type. And the `undefined` fields should be removed by zod guard.
|
||||||
// `context` parameter is only eligible for user's access token for now.
|
// `context` parameter is only eligible for user's access token for now.
|
||||||
|
context: {
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
context: { user: logtoUserInfo as Record<string, Json> },
|
user: logtoUserInfo as Record<string, Json>,
|
||||||
|
...conditional(
|
||||||
|
subjectToken && {
|
||||||
|
grant: {
|
||||||
|
type: GrantType.TokenExchange,
|
||||||
|
subjectTokenContext: subjectToken.context,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AccessTokenPayload, ClientCredentialsPayload } from '@logto/schemas';
|
import { type AccessTokenPayload, type ClientCredentialsPayload } from '@logto/schemas';
|
||||||
|
|
||||||
const standardTokenPayloadData = {
|
const standardTokenPayloadData = {
|
||||||
jti: 'f1d3d2d1-1f2d-3d4e-5d6f-7d8a9d0e1d2',
|
jti: 'f1d3d2d1-1f2d-3d4e-5d6f-7d8a9d0e1d2',
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { createUserMfaVerification, deleteUser } from '#src/api/admin-user.js';
|
||||||
import { oidcApi } from '#src/api/api.js';
|
import { oidcApi } from '#src/api/api.js';
|
||||||
import { createApplication, deleteApplication } from '#src/api/application.js';
|
import { createApplication, deleteApplication } from '#src/api/application.js';
|
||||||
import { putInteraction } from '#src/api/interaction.js';
|
import { putInteraction } from '#src/api/interaction.js';
|
||||||
|
import { deleteJwtCustomizer, upsertJwtCustomizer } from '#src/api/logto-config.js';
|
||||||
import { createResource, deleteResource } from '#src/api/resource.js';
|
import { createResource, deleteResource } from '#src/api/resource.js';
|
||||||
import { createSubjectToken } from '#src/api/subject-token.js';
|
import { createSubjectToken } from '#src/api/subject-token.js';
|
||||||
import type MockClient from '#src/client/index.js';
|
import type MockClient from '#src/client/index.js';
|
||||||
|
@ -32,38 +33,65 @@ import {
|
||||||
const { describe, it } = devFeatureTest;
|
const { describe, it } = devFeatureTest;
|
||||||
|
|
||||||
describe('Token Exchange', () => {
|
describe('Token Exchange', () => {
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
// Add test resource to ensure that the access token is JWT,
|
||||||
|
// make it easy to check claims.
|
||||||
|
const testApiResourceInfo: Pick<Resource, 'name' | 'indicator'> = {
|
||||||
|
name: 'test-api-resource',
|
||||||
|
indicator: 'https://foo.logto.io/api',
|
||||||
|
};
|
||||||
|
|
||||||
/* eslint-disable @silverhand/fp/no-let */
|
/* eslint-disable @silverhand/fp/no-let */
|
||||||
let userId: string;
|
let testApiResourceId: string;
|
||||||
let applicationId: string;
|
let testApplicationId: string;
|
||||||
|
let testUserId: string;
|
||||||
|
let testAccessToken: string;
|
||||||
|
let client: MockClient;
|
||||||
/* eslint-enable @silverhand/fp/no-let */
|
/* eslint-enable @silverhand/fp/no-let */
|
||||||
|
|
||||||
/* eslint-disable @silverhand/fp/no-mutation */
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const user = await createUserByAdmin();
|
await enableAllPasswordSignInMethods();
|
||||||
userId = user.id;
|
|
||||||
|
/* eslint-disable @silverhand/fp/no-mutation */
|
||||||
|
const resource = await createResource(testApiResourceInfo.name, testApiResourceInfo.indicator);
|
||||||
|
testApiResourceId = resource.id;
|
||||||
const applicationName = 'test-token-exchange-app';
|
const applicationName = 'test-token-exchange-app';
|
||||||
const applicationType = ApplicationType.SPA;
|
const applicationType = ApplicationType.SPA;
|
||||||
const application = await createApplication(applicationName, applicationType, {
|
const application = await createApplication(applicationName, applicationType, {
|
||||||
oidcClientMetadata: { redirectUris: ['http://localhost:3000'], postLogoutRedirectUris: [] },
|
oidcClientMetadata: { redirectUris: ['http://localhost:3000'], postLogoutRedirectUris: [] },
|
||||||
});
|
});
|
||||||
applicationId = application.id;
|
testApplicationId = application.id;
|
||||||
|
const { id } = await createUserByAdmin({ username, password });
|
||||||
|
testUserId = id;
|
||||||
|
client = await initClient({
|
||||||
|
resources: [testApiResourceInfo.indicator],
|
||||||
});
|
});
|
||||||
|
await client.successSend(putInteraction, {
|
||||||
|
event: InteractionEvent.SignIn,
|
||||||
|
identifier: { username, password },
|
||||||
|
});
|
||||||
|
const { redirectTo } = await client.submitInteraction();
|
||||||
|
await processSession(client, redirectTo);
|
||||||
|
testAccessToken = await client.getAccessToken();
|
||||||
/* eslint-enable @silverhand/fp/no-mutation */
|
/* eslint-enable @silverhand/fp/no-mutation */
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await deleteUser(userId);
|
await deleteUser(testUserId);
|
||||||
await deleteApplication(applicationId);
|
await deleteResource(testApiResourceId);
|
||||||
|
await deleteApplication(testApplicationId);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Basic flow', () => {
|
describe('Basic flow', () => {
|
||||||
it('should exchange an access token by a subject token', async () => {
|
it('should exchange an access token by a subject token', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
const body = await oidcApi
|
const body = await oidcApi
|
||||||
.post('token', {
|
.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -78,7 +106,7 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail without valid client_id', async () => {
|
it('should fail without valid client_id', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
oidcApi.post('token', {
|
oidcApi.post('token', {
|
||||||
|
@ -97,7 +125,7 @@ describe('Token Exchange', () => {
|
||||||
oidcApi.post('token', {
|
oidcApi.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: 'invalid_subject_token',
|
subject_token: 'invalid_subject_token',
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -107,12 +135,12 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should failed with consumed subject token', async () => {
|
it('should failed with consumed subject token', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
await oidcApi.post('token', {
|
await oidcApi.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -122,7 +150,7 @@ describe('Token Exchange', () => {
|
||||||
oidcApi.post('token', {
|
oidcApi.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -132,7 +160,7 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail with a third-party application', async () => {
|
it('should fail with a third-party application', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
const thirdPartyApplication = await createApplication(
|
const thirdPartyApplication = await createApplication(
|
||||||
generateName(),
|
generateName(),
|
||||||
ApplicationType.Traditional,
|
ApplicationType.Traditional,
|
||||||
|
@ -157,13 +185,13 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out non-oidc scopes', async () => {
|
it('should filter out non-oidc scopes', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
const body = await oidcApi
|
const body = await oidcApi
|
||||||
.post('token', {
|
.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -193,14 +221,14 @@ describe('Token Exchange', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const organization = await organizationApi.create({ name: 'org1' });
|
const organization = await organizationApi.create({ name: 'org1' });
|
||||||
testOrganizationId = organization.id;
|
testOrganizationId = organization.id;
|
||||||
await organizationApi.addUsers(testOrganizationId, [userId]);
|
await organizationApi.addUsers(testOrganizationId, [testUserId]);
|
||||||
|
|
||||||
const scope = await organizationApi.scopeApi.create({ name: scopeName });
|
const scope = await organizationApi.scopeApi.create({ name: scopeName });
|
||||||
testApiScopeId = scope.id;
|
testApiScopeId = scope.id;
|
||||||
|
|
||||||
const role = await organizationApi.roleApi.create({ name: `role1:${randomString()}` });
|
const role = await organizationApi.roleApi.create({ name: `role1:${randomString()}` });
|
||||||
await organizationApi.roleApi.addScopes(role.id, [scope.id]);
|
await organizationApi.roleApi.addScopes(role.id, [scope.id]);
|
||||||
await organizationApi.addUserRoles(testOrganizationId, userId, [role.id]);
|
await organizationApi.addUserRoles(testOrganizationId, testUserId, [role.id]);
|
||||||
});
|
});
|
||||||
/* eslint-enable @silverhand/fp/no-mutation */
|
/* eslint-enable @silverhand/fp/no-mutation */
|
||||||
|
|
||||||
|
@ -209,13 +237,13 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to get access token for organization with correct scopes', async () => {
|
it('should be able to get access token for organization with correct scopes', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
const { access_token } = await oidcApi
|
const { access_token } = await oidcApi
|
||||||
.post('token', {
|
.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -232,14 +260,14 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when organization requires mfa but user has not configured', async () => {
|
it('should throw when organization requires mfa but user has not configured', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
await organizationApi.update(testOrganizationId, { isMfaRequired: true });
|
await organizationApi.update(testOrganizationId, { isMfaRequired: true });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
oidcApi.post('token', {
|
oidcApi.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -250,13 +278,13 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to get access token for organization when user has mfa configured', async () => {
|
it('should be able to get access token for organization when user has mfa configured', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
await createUserMfaVerification(userId, MfaFactor.TOTP);
|
await createUserMfaVerification(testUserId, MfaFactor.TOTP);
|
||||||
const { access_token } = await oidcApi
|
const { access_token } = await oidcApi
|
||||||
.post('token', {
|
.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -272,59 +300,14 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with actor token', () => {
|
describe('with actor token', () => {
|
||||||
const username = generateUsername();
|
|
||||||
const password = generatePassword();
|
|
||||||
// Add test resource to ensure that the access token is JWT,
|
|
||||||
// make it easy to check claims.
|
|
||||||
const testApiResourceInfo: Pick<Resource, 'name' | 'indicator'> = {
|
|
||||||
name: 'test-api-resource',
|
|
||||||
indicator: 'https://foo.logto.io/api',
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable @silverhand/fp/no-let */
|
|
||||||
let testApiResourceId: string;
|
|
||||||
let testUserId: string;
|
|
||||||
let testAccessToken: string;
|
|
||||||
let client: MockClient;
|
|
||||||
/* eslint-enable @silverhand/fp/no-let */
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await enableAllPasswordSignInMethods();
|
|
||||||
|
|
||||||
/* eslint-disable @silverhand/fp/no-mutation */
|
|
||||||
const resource = await createResource(
|
|
||||||
testApiResourceInfo.name,
|
|
||||||
testApiResourceInfo.indicator
|
|
||||||
);
|
|
||||||
testApiResourceId = resource.id;
|
|
||||||
const { id } = await createUserByAdmin({ username, password });
|
|
||||||
testUserId = id;
|
|
||||||
client = await initClient({
|
|
||||||
resources: [testApiResourceInfo.indicator],
|
|
||||||
});
|
|
||||||
await client.successSend(putInteraction, {
|
|
||||||
event: InteractionEvent.SignIn,
|
|
||||||
identifier: { username, password },
|
|
||||||
});
|
|
||||||
const { redirectTo } = await client.submitInteraction();
|
|
||||||
await processSession(client, redirectTo);
|
|
||||||
testAccessToken = await client.getAccessToken();
|
|
||||||
/* eslint-enable @silverhand/fp/no-mutation */
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await deleteUser(testUserId);
|
|
||||||
await deleteResource(testApiResourceId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exchange an access token with `act` claim', async () => {
|
it('should exchange an access token with `act` claim', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
const { access_token } = await oidcApi
|
const { access_token } = await oidcApi
|
||||||
.post('token', {
|
.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -339,13 +322,13 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail with invalid actor_token_type', async () => {
|
it('should fail with invalid actor_token_type', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
oidcApi.post('token', {
|
oidcApi.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -358,13 +341,13 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail with invalid actor_token', async () => {
|
it('should fail with invalid actor_token', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
oidcApi.post('token', {
|
oidcApi.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -377,7 +360,7 @@ describe('Token Exchange', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail when the actor token do not have `openid` scope', async () => {
|
it('should fail when the actor token do not have `openid` scope', async () => {
|
||||||
const { subjectToken } = await createSubjectToken(userId);
|
const { subjectToken } = await createSubjectToken(testUserId);
|
||||||
// Set `resource` to ensure that the access token is JWT, and then it won't have `openid` scope.
|
// Set `resource` to ensure that the access token is JWT, and then it won't have `openid` scope.
|
||||||
const accessToken = await client.getAccessToken(testApiResourceInfo.indicator);
|
const accessToken = await client.getAccessToken(testApiResourceInfo.indicator);
|
||||||
|
|
||||||
|
@ -385,7 +368,7 @@ describe('Token Exchange', () => {
|
||||||
oidcApi.post('token', {
|
oidcApi.post('token', {
|
||||||
headers: formUrlEncodedHeaders,
|
headers: formUrlEncodedHeaders,
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: applicationId,
|
client_id: testApplicationId,
|
||||||
grant_type: GrantType.TokenExchange,
|
grant_type: GrantType.TokenExchange,
|
||||||
subject_token: subjectToken,
|
subject_token: subjectToken,
|
||||||
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
@ -397,4 +380,31 @@ describe('Token Exchange', () => {
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('custom jwt', () => {
|
||||||
|
it('should get context from subject token', async () => {
|
||||||
|
const { subjectToken } = await createSubjectToken(testUserId, { foo: 'bar' });
|
||||||
|
await upsertJwtCustomizer('access-token', {
|
||||||
|
script: `const getCustomJwtClaims = async ({ token, context, environmentVariables }) => {
|
||||||
|
return { foo: context?.grant?.subjectTokenContext?.foo };
|
||||||
|
};`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { access_token } = await oidcApi
|
||||||
|
.post('token', {
|
||||||
|
headers: formUrlEncodedHeaders,
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: testApplicationId,
|
||||||
|
grant_type: GrantType.TokenExchange,
|
||||||
|
subject_token: subjectToken,
|
||||||
|
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
||||||
|
resource: testApiResourceInfo.indicator,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.json<{ access_token: string }>();
|
||||||
|
|
||||||
|
expect(getAccessTokenPayload(access_token)).toHaveProperty('foo', 'bar');
|
||||||
|
await deleteJwtCustomizer('access-token');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue