mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(core,schemas): cors allowed origins (#507)
* feat(schemas): cors allowed origins of application in custom OIDC client metadata * refactor(schemas): rename CustomClientMetadataType to CustomClientMetadataKey * feat(core): cors allowed origins
This commit is contained in:
parent
5625b8838c
commit
fb65c65893
4 changed files with 137 additions and 36 deletions
|
@ -1,12 +1,13 @@
|
||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
|
|
||||||
import { customClientMetadataGuard, CustomClientMetadataType } from '@logto/schemas';
|
import { CustomClientMetadataKey } from '@logto/schemas';
|
||||||
import { fromKeyLike } from 'jose/jwk/from_key_like';
|
import { fromKeyLike } from 'jose/jwk/from_key_like';
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import mount from 'koa-mount';
|
import mount from 'koa-mount';
|
||||||
import { Provider, errors } from 'oidc-provider';
|
import { Provider, errors } from 'oidc-provider';
|
||||||
|
|
||||||
import postgresAdapter from '@/oidc/adapter';
|
import postgresAdapter from '@/oidc/adapter';
|
||||||
|
import { isOriginAllowed, validateCustomClientMetadata } from '@/oidc/utils';
|
||||||
import { findResourceByIndicator } from '@/queries/resource';
|
import { findResourceByIndicator } from '@/queries/resource';
|
||||||
import { findUserById } from '@/queries/user';
|
import { findUserById } from '@/queries/user';
|
||||||
import { routes } from '@/routes/consts';
|
import { routes } from '@/routes/consts';
|
||||||
|
@ -76,22 +77,13 @@ export default async function initOidc(app: Koa): Promise<Provider> {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraClientMetadata: {
|
extraClientMetadata: {
|
||||||
properties: Object.keys(CustomClientMetadataType),
|
properties: Object.keys(CustomClientMetadataKey),
|
||||||
validator: (_ctx, key, value) => {
|
validator: (_, key, value) => {
|
||||||
const result = customClientMetadataGuard.pick({ [key]: true }).safeParse({ key: value });
|
validateCustomClientMetadata(key, value);
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new errors.InvalidClientMetadata(key);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
clientBasedCORS: (_, origin) => {
|
// https://github.com/panva/node-oidc-provider/blob/main/recipes/client_based_origins.md
|
||||||
console.log('origin', origin);
|
clientBasedCORS: (_, origin, client) => isOriginAllowed(origin, client.metadata()),
|
||||||
|
|
||||||
return ['http://localhost:3001', 'https://logto.dev'].some((value) =>
|
|
||||||
origin.startsWith(value)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
findAccount: async (ctx, sub) => {
|
findAccount: async (ctx, sub) => {
|
||||||
await findUserById(sub);
|
await findUserById(sub);
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,91 @@
|
||||||
import { ApplicationType } from '@logto/schemas';
|
import { ApplicationType, CustomClientMetadataKey } from '@logto/schemas';
|
||||||
|
|
||||||
import { getApplicationTypeString, buildOidcClientMetadata } from './utils';
|
import {
|
||||||
|
isOriginAllowed,
|
||||||
|
buildOidcClientMetadata,
|
||||||
|
getApplicationTypeString,
|
||||||
|
validateCustomClientMetadata,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
describe('oidc utils method', () => {
|
it('getApplicationTypeString', () => {
|
||||||
it('getApplicationTypeString', () => {
|
expect(getApplicationTypeString(ApplicationType.SPA)).toEqual('web');
|
||||||
expect(getApplicationTypeString(ApplicationType.SPA)).toEqual('web');
|
expect(getApplicationTypeString(ApplicationType.Native)).toEqual('native');
|
||||||
expect(getApplicationTypeString(ApplicationType.Native)).toEqual('native');
|
expect(getApplicationTypeString(ApplicationType.Traditional)).toEqual('web');
|
||||||
expect(getApplicationTypeString(ApplicationType.Traditional)).toEqual('web');
|
});
|
||||||
|
|
||||||
|
it('buildOidcClientMetadata', () => {
|
||||||
|
const metadata = {
|
||||||
|
redirectUris: ['logto.dev'],
|
||||||
|
postLogoutRedirectUris: ['logto.dev'],
|
||||||
|
logoUri: 'logto.pnf',
|
||||||
|
};
|
||||||
|
expect(buildOidcClientMetadata()).toEqual({ redirectUris: [], postLogoutRedirectUris: [] });
|
||||||
|
expect(buildOidcClientMetadata(metadata)).toEqual(metadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateMetadata', () => {
|
||||||
|
describe('corsAllowedOrigins', () => {
|
||||||
|
it('should not throw when corsAllowedOrigins is empty', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateCustomClientMetadata('corsAllowedOrigins', []);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw when corsAllowedOrigins are all valid', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateCustomClientMetadata('corsAllowedOrigins', [
|
||||||
|
'http://localhost:3001',
|
||||||
|
'https://logto.dev',
|
||||||
|
]);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when corsAllowedOrigins are not all valid', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateCustomClientMetadata('corsAllowedOrigins', ['', 'logto.dev']);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('buildOidcClientMetadata', () => {
|
describe.each(['idTokenTtl', 'refreshTokenTtl'])('%s', (ttlKey) => {
|
||||||
const metadata = {
|
test(`${ttlKey} should not throw when it is a number`, () => {
|
||||||
redirectUris: ['logto.dev'],
|
expect(() => {
|
||||||
postLogoutRedirectUris: ['logto.dev'],
|
validateCustomClientMetadata(ttlKey, 5000);
|
||||||
logoUri: 'logto.pnf',
|
}).not.toThrow();
|
||||||
};
|
});
|
||||||
expect(buildOidcClientMetadata()).toEqual({ redirectUris: [], postLogoutRedirectUris: [] });
|
|
||||||
expect(buildOidcClientMetadata(metadata)).toEqual(metadata);
|
test(`${ttlKey} should throw when it is not a number`, () => {
|
||||||
|
expect(() => {
|
||||||
|
validateCustomClientMetadata(ttlKey, 'string_value');
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('corsAllowOrigin', () => {
|
||||||
|
it('should return false if there is no corsAllowOrigins', () => {
|
||||||
|
expect(isOriginAllowed('https://logto.dev', {})).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if corsAllowOrigins is empty', () => {
|
||||||
|
expect(
|
||||||
|
isOriginAllowed('https://logto.dev', { [CustomClientMetadataKey.CorsAllowedOrigins]: [] })
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if corsAllowOrigins do not include the origin', () => {
|
||||||
|
expect(
|
||||||
|
isOriginAllowed('http://localhost:3001', {
|
||||||
|
[CustomClientMetadataKey.CorsAllowedOrigins]: ['https://logto.dev'],
|
||||||
|
})
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if corsAllowOrigins include the origin', () => {
|
||||||
|
expect(
|
||||||
|
isOriginAllowed('https://logto.dev', {
|
||||||
|
[CustomClientMetadataKey.CorsAllowedOrigins]: ['https://logto.dev'],
|
||||||
|
})
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { ApplicationType, OidcClientMetadata } from '@logto/schemas';
|
import {
|
||||||
|
ApplicationType,
|
||||||
|
CustomClientMetadata,
|
||||||
|
customClientMetadataGuard,
|
||||||
|
CustomClientMetadataKey,
|
||||||
|
OidcClientMetadata,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
import { errors } from 'oidc-provider';
|
||||||
|
|
||||||
export const getApplicationTypeString = (type: ApplicationType) =>
|
export const getApplicationTypeString = (type: ApplicationType) =>
|
||||||
type === ApplicationType.Native ? 'native' : 'web';
|
type === ApplicationType.Native ? 'native' : 'web';
|
||||||
|
@ -8,3 +15,33 @@ export const buildOidcClientMetadata = (metadata?: OidcClientMetadata): OidcClie
|
||||||
postLogoutRedirectUris: [],
|
postLogoutRedirectUris: [],
|
||||||
...metadata,
|
...metadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isOrigin = (value: string) => {
|
||||||
|
try {
|
||||||
|
const { origin } = new URL(value);
|
||||||
|
|
||||||
|
// Origin: <scheme> "://" <hostname> [ ":" <port> ]
|
||||||
|
return value === origin;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateCustomClientMetadata = (key: string, value: unknown) => {
|
||||||
|
const result = customClientMetadataGuard.pick({ [key]: true }).safeParse({ [key]: value });
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new errors.InvalidClientMetadata(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
key === CustomClientMetadataKey.CorsAllowedOrigins &&
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.some((origin) => !isOrigin(origin))
|
||||||
|
) {
|
||||||
|
throw new errors.InvalidClientMetadata(CustomClientMetadataKey.CorsAllowedOrigins);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isOriginAllowed = (origin: string, customClientMetadata: CustomClientMetadata) =>
|
||||||
|
Boolean(customClientMetadata.corsAllowedOrigins?.includes(origin));
|
||||||
|
|
|
@ -28,14 +28,16 @@ export const oidcClientMetadataGuard = z.object({
|
||||||
|
|
||||||
export type OidcClientMetadata = z.infer<typeof oidcClientMetadataGuard>;
|
export type OidcClientMetadata = z.infer<typeof oidcClientMetadataGuard>;
|
||||||
|
|
||||||
export enum CustomClientMetadataType {
|
export enum CustomClientMetadataKey {
|
||||||
idTokenTtl = 'idTokenTtl',
|
CorsAllowedOrigins = 'corsAllowedOrigins',
|
||||||
refreshTokenTtl = 'refreshTokenTtl',
|
IdTokenTtl = 'idTokenTtl',
|
||||||
|
RefreshTokenTtl = 'refreshTokenTtl',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customClientMetadataGuard = z.object({
|
export const customClientMetadataGuard = z.object({
|
||||||
[CustomClientMetadataType.idTokenTtl]: z.number().optional(),
|
[CustomClientMetadataKey.CorsAllowedOrigins]: z.string().array().optional(),
|
||||||
[CustomClientMetadataType.refreshTokenTtl]: z.number().optional(),
|
[CustomClientMetadataKey.IdTokenTtl]: z.number().optional(),
|
||||||
|
[CustomClientMetadataKey.RefreshTokenTtl]: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CustomClientMetadata = z.infer<typeof customClientMetadataGuard>;
|
export type CustomClientMetadata = z.infer<typeof customClientMetadataGuard>;
|
||||||
|
|
Loading…
Add table
Reference in a new issue