mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
feat: upgrade supertokens SDK (#3328)
This commit is contained in:
parent
fce33b23e6
commit
2bcfdf8a1d
11 changed files with 287 additions and 263 deletions
|
|
@ -1,5 +1,5 @@
|
|||
const getUser = () =>
|
||||
({ email: `${crypto.randomUUID()}@local.host`, password: 'Loc@l.h0st' } as const);
|
||||
({ email: `${crypto.randomUUID()}@local.host`, password: 'Loc@l.h0st' }) as const;
|
||||
|
||||
Cypress.on('uncaught:exception', (_err, _runnable) => {
|
||||
return false;
|
||||
|
|
@ -82,3 +82,11 @@ it('oidc login for organization', () => {
|
|||
cy.get('[data-cy="organization-picker-current"]').contains('Bubatzbieber');
|
||||
});
|
||||
});
|
||||
|
||||
it('oidc login for invalid url shows correct error message', () => {
|
||||
cy.clearAllCookies();
|
||||
cy.clearAllLocalStorage();
|
||||
cy.clearAllSessionStorage();
|
||||
cy.visit('/auth/oidc?id=invalid');
|
||||
cy.get('div.text-red-500').contains('Could not find OIDC integration.');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
"param-case": "3.0.4",
|
||||
"prom-client": "15.0.0",
|
||||
"redlock": "5.0.0-beta.2",
|
||||
"supertokens-node": "14.1.3",
|
||||
"supertokens-node": "15.2.1",
|
||||
"tslib": "2.6.2",
|
||||
"zod": "3.22.4",
|
||||
"zod-validation-error": "2.1.0"
|
||||
|
|
|
|||
|
|
@ -84,10 +84,10 @@
|
|||
"react-window": "1.8.9",
|
||||
"regenerator-runtime": "0.14.0",
|
||||
"snarkdown": "2.0.0",
|
||||
"supertokens-auth-react": "0.33.1",
|
||||
"supertokens-auth-react": "0.35.6",
|
||||
"supertokens-js-override": "0.0.4",
|
||||
"supertokens-node": "14.1.3",
|
||||
"supertokens-web-js": "0.6.0",
|
||||
"supertokens-node": "15.2.1",
|
||||
"supertokens-web-js": "0.8.0",
|
||||
"tailwind-merge": "2.0.0",
|
||||
"tslib": "2.6.2",
|
||||
"urql": "4.0.5",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as crypto from 'node:crypto';
|
|||
import { OverrideableBuilder } from 'supertokens-js-override/lib/build';
|
||||
import EmailVerification from 'supertokens-node/recipe/emailverification';
|
||||
import SessionNode from 'supertokens-node/recipe/session';
|
||||
import { TypeProvider } from 'supertokens-node/recipe/thirdparty/types';
|
||||
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
|
||||
import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemailpassword';
|
||||
import { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
|
||||
import { TypeInput } from 'supertokens-node/types';
|
||||
|
|
@ -10,14 +10,12 @@ import zod from 'zod';
|
|||
import { env } from '@/env/backend';
|
||||
import { appInfo } from '@/lib/supertokens/app-info';
|
||||
import {
|
||||
createOIDCSuperTokensNoopProvider,
|
||||
getOIDCThirdPartyEmailPasswordNodeOverrides,
|
||||
createOIDCSuperTokensProvider,
|
||||
getOIDCSuperTokensOverrides,
|
||||
} from '@/lib/supertokens/third-party-email-password-node-oidc-provider';
|
||||
import { createThirdPartyEmailPasswordNodeOktaProvider } from '@/lib/supertokens/third-party-email-password-node-okta-provider';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies -- TODO: should we move to "dependencies"?
|
||||
import { EmailsApi } from '@hive/emails';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies -- TODO: should we move to "dependencies"?
|
||||
import { type InternalApi } from '@hive/server';
|
||||
import type { EmailsApi } from '@hive/emails';
|
||||
import type { InternalApi } from '@hive/server';
|
||||
import { createTRPCProxyClient, CreateTRPCProxyClient, httpLink } from '@trpc/client';
|
||||
import { fetch } from '@whatwg-node/fetch';
|
||||
|
||||
|
|
@ -28,29 +26,39 @@ export const backendConfig = (): TypeInput => {
|
|||
const internalApi = createTRPCProxyClient<InternalApi>({
|
||||
links: [httpLink({ url: `${env.serverEndpoint}/trpc` })],
|
||||
});
|
||||
const providers: TypeProvider[] = [];
|
||||
const providers: ProviderInput[] = [];
|
||||
|
||||
if (env.auth.github) {
|
||||
providers.push(
|
||||
ThirdPartyEmailPasswordNode.Github({
|
||||
clientId: env.auth.github.clientId,
|
||||
clientSecret: env.auth.github.clientSecret,
|
||||
scope: ['read:user', 'user:email'],
|
||||
}),
|
||||
);
|
||||
providers.push({
|
||||
config: {
|
||||
thirdPartyId: 'github',
|
||||
clients: [
|
||||
{
|
||||
scope: ['read:user', 'user:email'],
|
||||
clientId: env.auth.github.clientId,
|
||||
clientSecret: env.auth.github.clientSecret,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (env.auth.google) {
|
||||
providers.push(
|
||||
ThirdPartyEmailPasswordNode.Google({
|
||||
clientId: env.auth.google.clientId,
|
||||
clientSecret: env.auth.google.clientSecret,
|
||||
scope: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'openid',
|
||||
providers.push({
|
||||
config: {
|
||||
thirdPartyId: 'google',
|
||||
clients: [
|
||||
{
|
||||
clientId: env.auth.google.clientId,
|
||||
clientSecret: env.auth.google.clientSecret,
|
||||
scope: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'openid',
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (env.auth.okta) {
|
||||
|
|
@ -58,7 +66,11 @@ export const backendConfig = (): TypeInput => {
|
|||
}
|
||||
|
||||
if (env.auth.organizationOIDC) {
|
||||
providers.push(createOIDCSuperTokensNoopProvider());
|
||||
providers.push(
|
||||
createOIDCSuperTokensProvider({
|
||||
internalApi,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -91,9 +103,7 @@ export const backendConfig = (): TypeInput => {
|
|||
},
|
||||
override: composeSuperTokensOverrides([
|
||||
getEnsureUserOverrides(internalApi),
|
||||
env.auth.organizationOIDC
|
||||
? getOIDCThirdPartyEmailPasswordNodeOverrides({ internalApi })
|
||||
: null,
|
||||
env.auth.organizationOIDC ? getOIDCSuperTokensOverrides() : null,
|
||||
/**
|
||||
* These overrides are only relevant for the legacy Auth0 -> SuperTokens migration (period).
|
||||
*/
|
||||
|
|
@ -206,14 +216,24 @@ const getEnsureUserOverrides = (
|
|||
throw Error('Should never come here');
|
||||
}
|
||||
|
||||
function extractOidcId(args: typeof input) {
|
||||
if (input.provider.id === 'oidc') {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const oidcId: unknown = args.userContext['oidcId'];
|
||||
if (typeof oidcId === 'string') {
|
||||
return oidcId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await originalImplementation.thirdPartySignInUpPOST(input);
|
||||
|
||||
if (response.status === 'OK') {
|
||||
await internalApi.ensureUser.mutate({
|
||||
superTokensUserId: response.user.id,
|
||||
email: response.user.email,
|
||||
// This is provided via `getOIDCThirdPartyEmailPasswordNodeOverrides` if it is enabled.
|
||||
oidcIntegrationId: input.userContext['oidcIntegrationId'] ?? null,
|
||||
oidcIntegrationId: extractOidcId(input),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +315,7 @@ const getAuth0Overrides = (config: Exclude<typeof env.auth.legacyAuth0, null>) =
|
|||
|
||||
if (email) {
|
||||
// We first use the existing implementation for looking for users within supertokens.
|
||||
const users = await ThirdPartyEmailPasswordNode.getUsersByEmail(email);
|
||||
const users = await ThirdPartyEmailPasswordNode.getUsersByEmail('public', email);
|
||||
|
||||
// If there is no email/password SuperTokens user yet, we need to check if there is an Auth0 user for this email.
|
||||
if (!users.some(user => user.thirdParty == null)) {
|
||||
|
|
@ -308,6 +328,7 @@ const getAuth0Overrides = (config: Exclude<typeof env.auth.legacyAuth0, null>) =
|
|||
if (dbUser) {
|
||||
// If we have this user within our database we create our new supertokens user
|
||||
const newUserResult = await ThirdPartyEmailPasswordNode.emailPasswordSignUp(
|
||||
'public',
|
||||
dbUser.email,
|
||||
await generateRandomPassword(),
|
||||
);
|
||||
|
|
@ -337,6 +358,7 @@ const getAuth0Overrides = (config: Exclude<typeof env.auth.legacyAuth0, null>) =
|
|||
if (await doesUserExistInAuth0(config, input.email)) {
|
||||
// check if user exists in SuperTokens
|
||||
const superTokensUsers = await this.getUsersByEmail({
|
||||
tenantId: 'public',
|
||||
email: input.email,
|
||||
userContext: input.userContext,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
createThirdPartyEmailPasswordReactOIDCProvider,
|
||||
getOIDCOverrides,
|
||||
} from '@/lib/supertokens/third-party-email-password-react-oidc-provider';
|
||||
import { createThirdPartyEmailPasswordReactOktaProvider } from '@/lib/supertokens/third-party-email-password-react-okta-provider';
|
||||
|
||||
export const frontendConfig = () => {
|
||||
const providers: Array<Provider | CustomProviderConfig> = [];
|
||||
|
|
@ -32,7 +31,7 @@ export const frontendConfig = () => {
|
|||
// Only show Okta via query parameter
|
||||
new URLSearchParams(globalThis.window?.location.search ?? '').get('show_okta') === '1'))
|
||||
) {
|
||||
providers.push(createThirdPartyEmailPasswordReactOktaProvider());
|
||||
providers.push(ThirdPartyEmailPasswordReact.Okta.init());
|
||||
}
|
||||
|
||||
const url = new URL(globalThis.window.location.toString());
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import { env } from '@/env/frontend';
|
|||
/**
|
||||
* utility for starting the login flow manually without clicking a button
|
||||
*/
|
||||
export const startAuthFlowForProvider = async (providerId: 'google' | 'okta' | 'github') => {
|
||||
export const startAuthFlowForProvider = async (thirdPartyId: 'google' | 'okta' | 'github') => {
|
||||
const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({
|
||||
providerId,
|
||||
authorisationURL: `${env.appBaseUrl}/auth/callback/${providerId}`,
|
||||
thirdPartyId,
|
||||
frontendRedirectURI: `${env.appBaseUrl}/auth/callback/${thirdPartyId}`,
|
||||
});
|
||||
|
||||
window.location.assign(authUrl);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,143 @@
|
|||
import { ExpressRequest } from 'supertokens-node/lib/build/framework/express/framework';
|
||||
import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemailpassword';
|
||||
import { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
|
||||
import type { ExpressRequest } from 'supertokens-node/lib/build/framework/express/framework';
|
||||
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
|
||||
import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
|
||||
import zod from 'zod';
|
||||
import { env } from '@/env/backend';
|
||||
import { getLogger } from '@/server-logger';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies -- TODO: should we move to "dependencies"?
|
||||
import { type InternalApi } from '@hive/server';
|
||||
import { CreateTRPCProxyClient } from '@trpc/client';
|
||||
import type { InternalApi } from '@hive/server';
|
||||
import type { CreateTRPCProxyClient } from '@trpc/client';
|
||||
|
||||
const couldNotResolveOidcIntegrationSymbol = Symbol('could_not_resolve_oidc_integration');
|
||||
|
||||
export const getOIDCSuperTokensOverrides = (): ThirdPartEmailPasswordTypeInput['override'] => ({
|
||||
apis(originalImplementation) {
|
||||
return {
|
||||
...originalImplementation,
|
||||
async authorisationUrlGET(input) {
|
||||
if (input.userContext?.[couldNotResolveOidcIntegrationSymbol] === true) {
|
||||
return {
|
||||
status: 'GENERAL_ERROR',
|
||||
message: 'Could not find OIDC integration.',
|
||||
};
|
||||
}
|
||||
|
||||
return originalImplementation.authorisationUrlGET!(input);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const createOIDCSuperTokensProvider = (args: {
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>;
|
||||
}): ProviderInput => ({
|
||||
config: {
|
||||
thirdPartyId: 'oidc',
|
||||
},
|
||||
override(originalImplementation) {
|
||||
const logger = getLogger();
|
||||
return {
|
||||
...originalImplementation,
|
||||
|
||||
async getConfigForClientType(input) {
|
||||
logger.info('resolve config for OIDC provider.');
|
||||
const config = await getOIDCConfigFromInput(args.internalApi, input);
|
||||
if (!config) {
|
||||
// In the next step the override `authorisationUrlGET` from `getOIDCSuperTokensOverrides` is called.
|
||||
// We use the user context to return a `GENERAL_ERROR` with a human readable message.
|
||||
// We cannot return an error here (except an "Unexpected error"), so we also need to return fake dat
|
||||
input.userContext[couldNotResolveOidcIntegrationSymbol] = true;
|
||||
|
||||
return {
|
||||
thirdPartyId: 'oidc',
|
||||
get clientId(): string {
|
||||
throw new Error('Noop value accessed.');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
thirdPartyId: 'oidc',
|
||||
clientId: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
authorizationEndpoint: config.authorizationEndpoint,
|
||||
userInfoEndpoint: config.userinfoEndpoint,
|
||||
tokenEndpoint: config.tokenEndpoint,
|
||||
scope: ['openid', 'email'],
|
||||
};
|
||||
},
|
||||
|
||||
async getAuthorisationRedirectURL(input) {
|
||||
logger.info('resolve config for OIDC provider.');
|
||||
const oidcConfig = await getOIDCConfigFromInput(args.internalApi, input);
|
||||
|
||||
if (!oidcConfig) {
|
||||
// This case should never be reached (guarded by getConfigForClientType).
|
||||
// We still have it for security reasons.
|
||||
throw new Error('Could not find OIDC integration.');
|
||||
}
|
||||
|
||||
const authorizationRedirectUrl =
|
||||
await originalImplementation.getAuthorisationRedirectURL(input);
|
||||
|
||||
const url = new URL(authorizationRedirectUrl.urlWithQueryParams);
|
||||
url.searchParams.set('state', oidcConfig.id);
|
||||
|
||||
return {
|
||||
...authorizationRedirectUrl,
|
||||
urlWithQueryParams: url.toString(),
|
||||
};
|
||||
},
|
||||
|
||||
async getUserInfo(input) {
|
||||
logger.info('retrieve profile info from OIDC provider');
|
||||
const config = await getOIDCConfigFromInput(args.internalApi, input);
|
||||
if (!config) {
|
||||
// This case should never be reached (guarded by getConfigForClientType).
|
||||
// We still have it for security reasons.
|
||||
throw new Error('Could not find OIDC integration.');
|
||||
}
|
||||
|
||||
logger.info('fetch info for OIDC provider (oidcId=%s)', config.id);
|
||||
|
||||
const tokenResponse = OIDCTokenSchema.parse(input.oAuthTokens);
|
||||
const rawData: unknown = await fetch(config.userinfoEndpoint, {
|
||||
headers: {
|
||||
authorization: `Bearer ${tokenResponse.access_token}`,
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}).then(res => res.json());
|
||||
|
||||
logger.info('retrieved profile info for provider (oidcId=%s)', config.id);
|
||||
|
||||
const data = OIDCProfileInfoSchema.parse(rawData);
|
||||
|
||||
// Set the oidcId to the user context so it can be used in `thirdPartySignInUpPOST` for linking the user account to the OIDC integration.
|
||||
input.userContext.oidcId = config.id;
|
||||
|
||||
return {
|
||||
thirdPartyUserId: `${config.id}-${data.sub}`,
|
||||
email: {
|
||||
id: data.email,
|
||||
isVerified: true,
|
||||
},
|
||||
rawUserInfoFromProvider: {
|
||||
fromIdTokenPayload: undefined,
|
||||
fromUserInfoAPI: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
type OIDCCOnfig = {
|
||||
id: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
tokenEndpoint: string;
|
||||
userinfoEndpoint: string;
|
||||
authorizationEndpoint: string;
|
||||
};
|
||||
|
||||
const OIDCProfileInfoSchema = zod.object({
|
||||
sub: zod.string(),
|
||||
|
|
@ -15,69 +146,6 @@ const OIDCProfileInfoSchema = zod.object({
|
|||
|
||||
const OIDCTokenSchema = zod.object({ access_token: zod.string() });
|
||||
|
||||
const createOIDCSuperTokensProvider = (oidcConfig: {
|
||||
id: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
tokenEndpoint: string;
|
||||
userinfoEndpoint: string;
|
||||
authorizationEndpoint: string;
|
||||
}): ThirdPartyEmailPasswordNode.TypeProvider => ({
|
||||
id: 'oidc',
|
||||
get: (redirectURI, authCodeFromRequest) => ({
|
||||
getClientId: () => oidcConfig.clientId,
|
||||
getProfileInfo: async (rawTokenAPIResponse: unknown) => {
|
||||
const tokenResponse = OIDCTokenSchema.parse(rawTokenAPIResponse);
|
||||
const rawData: unknown = await fetch(oidcConfig.userinfoEndpoint, {
|
||||
headers: {
|
||||
authorization: `Bearer ${tokenResponse.access_token}`,
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
}).then(res => res.json());
|
||||
|
||||
const logger = getLogger();
|
||||
logger.info(
|
||||
`getProfileInfo: fetched OIDC (${
|
||||
oidcConfig.userinfoEndpoint
|
||||
}) profile info: ${JSON.stringify(rawData)}`,
|
||||
);
|
||||
|
||||
const data = OIDCProfileInfoSchema.parse(rawData);
|
||||
|
||||
return {
|
||||
// We scope the user id to the oidc config id to avoid potential collisions
|
||||
id: `${oidcConfig.id}-${data.sub}`,
|
||||
email: {
|
||||
id: data.email,
|
||||
isVerified: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
accessTokenAPI: {
|
||||
url: oidcConfig.tokenEndpoint,
|
||||
params: {
|
||||
client_id: oidcConfig.clientId,
|
||||
client_secret: oidcConfig.clientSecret,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectURI ?? '',
|
||||
code: authCodeFromRequest ?? '',
|
||||
},
|
||||
},
|
||||
authorisationRedirect: {
|
||||
// this contains info about forming the authorisation redirect URL without the state params and without the redirect_uri param
|
||||
url: oidcConfig.authorizationEndpoint,
|
||||
params: {
|
||||
client_id: oidcConfig.clientId,
|
||||
scope: 'openid email',
|
||||
response_type: 'code',
|
||||
redirect_uri: `${env.appBaseUrl}/auth/callback/oidc`,
|
||||
state: oidcConfig.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const getOIDCIdFromInput = (input: { userContext: any }): string => {
|
||||
const expressRequest = input.userContext._default.request as ExpressRequest;
|
||||
const originalUrl = 'http://localhost' + expressRequest.getOriginalURL();
|
||||
|
|
@ -92,68 +160,29 @@ const getOIDCIdFromInput = (input: { userContext: any }): string => {
|
|||
return oidcId;
|
||||
};
|
||||
|
||||
export const getOIDCThirdPartyEmailPasswordNodeOverrides = (args: {
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>;
|
||||
}): ThirdPartEmailPasswordTypeInput['override'] => ({
|
||||
apis: originalImplementation => ({
|
||||
...originalImplementation,
|
||||
thirdPartySignInUpPOST: async input => {
|
||||
if (input.provider.id !== 'oidc') {
|
||||
return originalImplementation.thirdPartySignInUpPOST!(input);
|
||||
}
|
||||
const configCache = new WeakMap<ExpressRequest, OIDCCOnfig | null>();
|
||||
|
||||
const oidcId = getOIDCIdFromInput(input);
|
||||
const config = await fetchOIDCConfig(args.internalApi, oidcId);
|
||||
/**
|
||||
* Get cached OIDC config from the supertokens input.
|
||||
*/
|
||||
async function getOIDCConfigFromInput(
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>,
|
||||
input: { userContext: any },
|
||||
) {
|
||||
const expressRequest = input.userContext._default.request as ExpressRequest;
|
||||
if (configCache.has(expressRequest)) {
|
||||
return configCache.get(expressRequest) ?? null;
|
||||
}
|
||||
|
||||
if (config === null) {
|
||||
return {
|
||||
status: 'GENERAL_ERROR',
|
||||
message: 'Could not find OIDC integration.',
|
||||
};
|
||||
}
|
||||
|
||||
return originalImplementation.thirdPartySignInUpPOST!({
|
||||
...input,
|
||||
provider: createOIDCSuperTokensProvider(config),
|
||||
userContext: {
|
||||
...input.userContext,
|
||||
oidcIntegrationId: oidcId,
|
||||
},
|
||||
});
|
||||
},
|
||||
authorisationUrlGET: async input => {
|
||||
if (input.provider.id !== 'oidc') {
|
||||
return originalImplementation.authorisationUrlGET!(input);
|
||||
}
|
||||
|
||||
const oidcId = getOIDCIdFromInput(input);
|
||||
const config = await fetchOIDCConfig(args.internalApi, oidcId);
|
||||
|
||||
if (config === null) {
|
||||
return {
|
||||
status: 'GENERAL_ERROR',
|
||||
message: 'Could not find OIDC integration.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = originalImplementation.authorisationUrlGET!({
|
||||
...input,
|
||||
provider: createOIDCSuperTokensProvider(config),
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const createOIDCSuperTokensNoopProvider = () => ({
|
||||
id: 'oidc',
|
||||
get() {
|
||||
const oidcId = getOIDCIdFromInput(input);
|
||||
const config = await fetchOIDCConfig(internalApi, oidcId);
|
||||
configCache.set(expressRequest, config);
|
||||
if (!config) {
|
||||
const logger = getLogger();
|
||||
logger.error('OIDC provider implementation was not provided via overrides.');
|
||||
throw new Error('Provider implementation was not provided via overrides.');
|
||||
},
|
||||
});
|
||||
logger.error('Could not find OIDC integration (oidcId: %s)', oidcId);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
const fetchOIDCConfig = async (
|
||||
internalApi: CreateTRPCProxyClient<InternalApi>,
|
||||
|
|
@ -169,7 +198,7 @@ const fetchOIDCConfig = async (
|
|||
const result = await internalApi.getOIDCIntegrationById.query({ oidcIntegrationId });
|
||||
if (result === null) {
|
||||
const logger = getLogger();
|
||||
logger.error('OIDC integration not found: %s', oidcIntegrationId);
|
||||
logger.error('OIDC integration not found. (oidcId=%s)', oidcIntegrationId);
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword';
|
||||
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
|
||||
import zod from 'zod';
|
||||
import { env } from '@/env/backend';
|
||||
|
||||
|
|
@ -7,46 +7,39 @@ type OktaConfig = Exclude<(typeof env)['auth']['okta'], null>;
|
|||
/**
|
||||
* Custom (server) provider for SuperTokens in order to allow Okta users to sign in.
|
||||
*/
|
||||
export const createThirdPartyEmailPasswordNodeOktaProvider = (config: OktaConfig): TypeProvider => {
|
||||
export const createThirdPartyEmailPasswordNodeOktaProvider = (
|
||||
config: OktaConfig,
|
||||
): ProviderInput => {
|
||||
return {
|
||||
id: 'okta',
|
||||
get(redirectURI, authCodeFromRequest) {
|
||||
config: {
|
||||
thirdPartyId: 'okta',
|
||||
clients: [
|
||||
{
|
||||
clientId: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
scope: ['openid', 'email', 'profile', 'okta.users.read.self'],
|
||||
},
|
||||
],
|
||||
authorizationEndpoint: `${config.endpoint}/oauth2/v1/authorize`,
|
||||
tokenEndpoint: `${config.endpoint}/oauth2/v1/token`,
|
||||
},
|
||||
override(originalImplementation) {
|
||||
return {
|
||||
accessTokenAPI: {
|
||||
// this contains info about the token endpoint which exchanges the auth code with the access token and profile info.
|
||||
url: `${config.endpoint}/oauth2/v1/token`,
|
||||
params: {
|
||||
// example post params
|
||||
client_id: config.clientId,
|
||||
client_secret: config.clientSecret,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectURI || '',
|
||||
code: authCodeFromRequest || '',
|
||||
},
|
||||
},
|
||||
authorisationRedirect: {
|
||||
// this contains info about forming the authorisation redirect URL without the state params and without the redirect_uri param
|
||||
url: `${config.endpoint}/oauth2/v1/authorize`,
|
||||
params: {
|
||||
client_id: config.clientId,
|
||||
scope: 'openid email profile okta.users.read.self',
|
||||
response_type: 'code',
|
||||
redirect_uri: `${env.appBaseUrl}/auth/callback/okta`,
|
||||
},
|
||||
},
|
||||
getClientId: () => {
|
||||
return config.clientId;
|
||||
},
|
||||
getProfileInfo: async (accessTokenAPIResponse: unknown) => {
|
||||
const data = OktaAccessTokenResponseModel.parse(accessTokenAPIResponse);
|
||||
...originalImplementation,
|
||||
async getUserInfo(input) {
|
||||
const data = OktaAccessTokenResponseModel.parse(input.oAuthTokens);
|
||||
const userData = await fetchOktaProfile(config, data.access_token);
|
||||
|
||||
return {
|
||||
id: userData.id,
|
||||
thirdPartyUserId: userData.id,
|
||||
email: {
|
||||
id: userData.profile.email,
|
||||
isVerified: true,
|
||||
},
|
||||
rawUserInfoFromProvider: {
|
||||
fromIdTokenPayload: undefined,
|
||||
fromUserInfoAPI: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const getOIDCOverrides = (): UserInput['override'] => ({
|
|||
...originalImplementation,
|
||||
generateStateToSendToOAuthProvider(input) {
|
||||
const hash = originalImplementation.generateStateToSendToOAuthProvider(input);
|
||||
const { oidcId } = input.userContext;
|
||||
const oidcId = input?.userContext?.['oidcId'];
|
||||
|
||||
if (typeof oidcId === 'string') {
|
||||
return `${hash}${delimiter}${oidcId}`;
|
||||
|
|
@ -71,8 +71,8 @@ export const getOIDCOverrides = (): UserInput['override'] => ({
|
|||
|
||||
export const startAuthFlowForOIDCProvider = async (oidcId: string) => {
|
||||
const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({
|
||||
providerId: 'oidc',
|
||||
authorisationURL: `${env.appBaseUrl}/auth/callback/oidc`,
|
||||
thirdPartyId: 'oidc',
|
||||
frontendRedirectURI: `${env.appBaseUrl}/auth/callback/oidc`,
|
||||
// The user context is very important - we store the OIDC ID so we can use it later on.
|
||||
userContext: {
|
||||
oidcId,
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { CustomProviderConfig } from 'supertokens-auth-react/lib/build/recipe/thirdparty/providers/types';
|
||||
|
||||
export const createThirdPartyEmailPasswordReactOktaProvider = (): CustomProviderConfig => ({
|
||||
id: 'okta',
|
||||
name: 'Okta',
|
||||
buttonComponent: (
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: '1',
|
||||
paddingTop: '5px',
|
||||
paddingBottom: '5px',
|
||||
borderRadius: '5px',
|
||||
borderStyle: 'solid',
|
||||
background: '#00297A',
|
||||
}}
|
||||
>
|
||||
Login with Okta
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
|
@ -671,8 +671,8 @@ importers:
|
|||
specifier: 5.0.0-beta.2
|
||||
version: 5.0.0-beta.2
|
||||
supertokens-node:
|
||||
specifier: 14.1.3
|
||||
version: 14.1.3
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1
|
||||
tslib:
|
||||
specifier: 2.6.2
|
||||
version: 2.6.2
|
||||
|
|
@ -1665,17 +1665,17 @@ importers:
|
|||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
supertokens-auth-react:
|
||||
specifier: 0.33.1
|
||||
version: 0.33.1(react-dom@18.2.0)(react@18.2.0)(supertokens-web-js@0.6.0)
|
||||
specifier: 0.35.6
|
||||
version: 0.35.6(react-dom@18.2.0)(react@18.2.0)(supertokens-web-js@0.8.0)
|
||||
supertokens-js-override:
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4
|
||||
supertokens-node:
|
||||
specifier: 14.1.3
|
||||
version: 14.1.3
|
||||
specifier: 15.2.1
|
||||
version: 15.2.1
|
||||
supertokens-web-js:
|
||||
specifier: 0.6.0
|
||||
version: 0.6.0
|
||||
specifier: 0.8.0
|
||||
version: 0.8.0
|
||||
tailwind-merge:
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
|
|
@ -16548,6 +16548,9 @@ packages:
|
|||
randombytes: 2.1.0
|
||||
randomfill: 1.0.4
|
||||
|
||||
/crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
/crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -24901,14 +24904,9 @@ packages:
|
|||
nodemailer-fetch: 1.6.0
|
||||
dev: true
|
||||
|
||||
/nodemailer@6.9.3:
|
||||
resolution: {integrity: sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
/nodemailer@6.9.7:
|
||||
resolution: {integrity: sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: true
|
||||
|
||||
/noms@0.0.0:
|
||||
resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==}
|
||||
|
|
@ -26165,6 +26163,11 @@ packages:
|
|||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/pkce-challenge@3.1.0:
|
||||
resolution: {integrity: sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==}
|
||||
dependencies:
|
||||
crypto-js: 4.2.0
|
||||
|
||||
/pkg-dir@3.0.0:
|
||||
resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -27176,15 +27179,6 @@ packages:
|
|||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
|
||||
/raw-body@2.5.2:
|
||||
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
|
||||
/react-avatar@5.0.3(@babel/runtime@7.23.1)(core-js-pure@3.28.0)(prop-types@15.8.1)(react@18.2.0):
|
||||
resolution: {integrity: sha512-DNc+qkWH9QehSEZqHBhqpXWsPY+rU9W7kD68QFHfu8Atfsvx/3ML0DzAePgTUd96nCXQQ3KZMcC3LKYT8FiBIg==}
|
||||
peerDependencies:
|
||||
|
|
@ -29441,27 +29435,27 @@ packages:
|
|||
ts-interface-checker: 0.1.13
|
||||
dev: true
|
||||
|
||||
/supertokens-auth-react@0.33.1(react-dom@18.2.0)(react@18.2.0)(supertokens-web-js@0.6.0):
|
||||
resolution: {integrity: sha512-TEzwW7k+ACZ4kkmRvOm6U8VAub0ykcCamBL0n43zOvjo+K+1NIhRycbpFdAh1sadk9EWzTXRM7wOEq6TRtfVEg==}
|
||||
/supertokens-auth-react@0.35.6(react-dom@18.2.0)(react@18.2.0)(supertokens-web-js@0.8.0):
|
||||
resolution: {integrity: sha512-Bt+rYunzqP6g331Ks5Q66obiIU/t2k6siG6dtpJNY2DZqAnjgJlDr8S/UAU8I72f4qWwlYv8FKUTa8+O/0z3zA==}
|
||||
engines: {node: '>=16.0.0', npm: '>=8'}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
supertokens-web-js: ^0.6.0
|
||||
supertokens-web-js: ^0.8.0
|
||||
dependencies:
|
||||
intl-tel-input: 17.0.19
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
supertokens-js-override: 0.0.4
|
||||
supertokens-web-js: 0.6.0
|
||||
supertokens-web-js: 0.8.0
|
||||
dev: false
|
||||
|
||||
/supertokens-js-override@0.0.4:
|
||||
resolution: {integrity: sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==}
|
||||
|
||||
/supertokens-node@14.1.3:
|
||||
resolution: {integrity: sha512-cwnzmJMHRQvqiztdRITlkK0o2psAw2wAYP+tJVYyIk4fm/93Yb0e9QMekuCJnhZAP0KXaZi+PGjA6F5FEE123Q==}
|
||||
/supertokens-node@15.2.1:
|
||||
resolution: {integrity: sha512-3zJ2EsiHYJHnYwAzDQI5Alp+4x/KcwEOBgeoPN5bWglZY0Xw0AzcZvd8S3N71vjLGvab0J3XxWmHeEHqSz5dbg==}
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
cookie: 0.4.0
|
||||
|
|
@ -29470,24 +29464,24 @@ packages:
|
|||
inflation: 2.0.0
|
||||
jose: 4.14.4
|
||||
libphonenumber-js: 1.10.14
|
||||
nodemailer: 6.9.3
|
||||
nodemailer: 6.9.7
|
||||
pkce-challenge: 3.1.0
|
||||
psl: 1.8.0
|
||||
raw-body: 2.5.2
|
||||
supertokens-js-override: 0.0.4
|
||||
twilio: 4.7.2(debug@4.3.4)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
/supertokens-web-js@0.6.0:
|
||||
resolution: {integrity: sha512-2KOgWNGanh+/zpdaFQgiA9dQa6T9wA6LcoB6m1BTZ7JIqR3mYmpLD5uu8ERtISNAGUjcXziON/Fp5ZnlBByCcw==}
|
||||
/supertokens-web-js@0.8.0:
|
||||
resolution: {integrity: sha512-kkvuPbdy1I0e7nejVJAhpPJhR5h7EUHdn7Fh1YqfSjZfSJh46J3JU2qWGCgSDVZuD1hSuUKb6skF9CQnltHWrQ==}
|
||||
dependencies:
|
||||
supertokens-js-override: 0.0.4
|
||||
supertokens-website: 17.0.1
|
||||
supertokens-website: 17.0.4
|
||||
dev: false
|
||||
|
||||
/supertokens-website@17.0.1:
|
||||
resolution: {integrity: sha512-RCBPIj4A5mVdiYEL6ArvFrZcQ8ByENe3FJEpBXIP0SB++kLuV54zU8lfL99kHcc9kbOHTdUjpWxrwEpvlTTPUQ==}
|
||||
/supertokens-website@17.0.4:
|
||||
resolution: {integrity: sha512-ayWhEFvspUe26YhM1bq11ssEpnFCZIsoHZtJwJHgHsoflfMUKdgrzOix/bboI0PWJeNTUphHyZebw0ApctaS1Q==}
|
||||
dependencies:
|
||||
browser-tabs-lock: 1.3.0
|
||||
supertokens-js-override: 0.0.4
|
||||
|
|
|
|||
Loading…
Reference in a new issue