UNPKG

supertokens-node

Version:
373 lines (372 loc) 20 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEV_OAUTH_REDIRECT_URL = void 0; exports.isUsingDevelopmentClientId = isUsingDevelopmentClientId; exports.getActualClientIdFromDevelopmentClientId = getActualClientIdFromDevelopmentClientId; exports.default = NewProvider; const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); const utils_1 = require("../../../utils"); const pkce_challenge_1 = __importDefault(require("pkce-challenge")); const jose_1 = require("jose"); const logger_1 = require("../../../logger"); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); const supertokens_1 = __importDefault(require("../../../supertokens")); const recipe_1 = __importDefault(require("../../saml/recipe")); const DEV_OAUTH_AUTHORIZATION_URL = "https://supertokens.io/dev/oauth/redirect-to-provider"; exports.DEV_OAUTH_REDIRECT_URL = "https://supertokens.io/dev/oauth/redirect-to-app"; // If Third Party login is used with one of the following development keys, then the dev authorization url and the redirect url will be used. const DEV_OAUTH_CLIENT_IDS = [ "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", // google "467101b197249757c71f", // github ]; const DEV_KEY_IDENTIFIER = "4398792-"; function isUsingDevelopmentClientId(client_id) { return client_id.startsWith(DEV_KEY_IDENTIFIER) || DEV_OAUTH_CLIENT_IDS.includes(client_id); } function getProviderConfigForClient(providerConfig, clientConfig) { return Object.assign(Object.assign({}, providerConfig), clientConfig); } function getActualClientIdFromDevelopmentClientId(client_id) { if (client_id.startsWith(DEV_KEY_IDENTIFIER)) { return client_id.split(DEV_KEY_IDENTIFIER)[1]; } return client_id; } function accessField(obj, key) { const keyParts = key.split("."); for (const k of keyParts) { if (obj === undefined) { return undefined; } if (typeof obj !== "object") { return undefined; } obj = obj[k]; } return obj; } function getSupertokensUserInfoResultFromRawUserInfo(config, rawUserInfoResponse) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; let thirdPartyUserId = ""; if (((_b = (_a = config.userInfoMap) === null || _a === void 0 ? void 0 : _a.fromUserInfoAPI) === null || _b === void 0 ? void 0 : _b.userId) !== undefined) { const userId = accessField(rawUserInfoResponse.fromUserInfoAPI, config.userInfoMap.fromUserInfoAPI.userId); if (userId !== undefined) { thirdPartyUserId = userId; } } if (((_d = (_c = config.userInfoMap) === null || _c === void 0 ? void 0 : _c.fromIdTokenPayload) === null || _d === void 0 ? void 0 : _d.userId) !== undefined) { const userId = accessField(rawUserInfoResponse.fromIdTokenPayload, config.userInfoMap.fromIdTokenPayload.userId); if (userId !== undefined) { thirdPartyUserId = userId; } } if (thirdPartyUserId === "") { throw new Error("third party user id is missing"); } const result = { thirdPartyUserId, }; let email = ""; if (((_f = (_e = config.userInfoMap) === null || _e === void 0 ? void 0 : _e.fromUserInfoAPI) === null || _f === void 0 ? void 0 : _f.email) !== undefined) { const emailVal = accessField(rawUserInfoResponse.fromUserInfoAPI, config.userInfoMap.fromUserInfoAPI.email); if (emailVal !== undefined) { email = emailVal; } } if (((_h = (_g = config.userInfoMap) === null || _g === void 0 ? void 0 : _g.fromIdTokenPayload) === null || _h === void 0 ? void 0 : _h.email) !== undefined) { const emailVal = accessField(rawUserInfoResponse.fromIdTokenPayload, config.userInfoMap.fromIdTokenPayload.email); if (emailVal !== undefined) { email = emailVal; } } if (email !== "") { result.email = { id: email, isVerified: false, }; if (((_k = (_j = config.userInfoMap) === null || _j === void 0 ? void 0 : _j.fromUserInfoAPI) === null || _k === void 0 ? void 0 : _k.emailVerified) !== undefined) { const emailVerifiedVal = accessField(rawUserInfoResponse.fromUserInfoAPI, config.userInfoMap.fromUserInfoAPI.emailVerified); result.email.isVerified = emailVerifiedVal === true || (typeof emailVerifiedVal === "string" && emailVerifiedVal.toLowerCase() === "true"); } if (((_m = (_l = config.userInfoMap) === null || _l === void 0 ? void 0 : _l.fromIdTokenPayload) === null || _m === void 0 ? void 0 : _m.emailVerified) !== undefined) { const emailVerifiedVal = accessField(rawUserInfoResponse.fromIdTokenPayload, config.userInfoMap.fromIdTokenPayload.emailVerified); result.email.isVerified = emailVerifiedVal === true || emailVerifiedVal === "true"; } } return result; } function NewProvider(input, type = "oauth2") { var _a, _b; // These are safe defaults common to most providers. Each provider implementations override these // as necessary input.config.userInfoMap = { fromIdTokenPayload: Object.assign({ userId: "sub", email: "email", emailVerified: "email_verified" }, (_a = input.config.userInfoMap) === null || _a === void 0 ? void 0 : _a.fromIdTokenPayload), fromUserInfoAPI: Object.assign({ userId: "sub", email: "email", emailVerified: "email_verified" }, (_b = input.config.userInfoMap) === null || _b === void 0 ? void 0 : _b.fromUserInfoAPI), }; if (input.config.generateFakeEmail === undefined) { input.config.generateFakeEmail = async function ({ thirdPartyUserId }) { return `${thirdPartyUserId}.${input.config.thirdPartyId}@stfakeemail.supertokens.com`; }; } let jwks; const supertokens = supertokens_1.default.getInstanceOrThrowError(); const appinfo = supertokens.appInfo; let impl; if (type === "oauth2") { impl = { id: input.config.thirdPartyId, type, config: Object.assign(Object.assign({}, input.config), { clientId: "temp" }), getConfigForClientType: async function ({ clientType }) { if (clientType === undefined) { if (input.config.clients === undefined || input.config.clients.length !== 1) { throw new Error("please provide exactly one client config or pass clientType or tenantId"); } return getProviderConfigForClient(input.config, input.config.clients[0]); } if (input.config.clients !== undefined) { for (const client of input.config.clients) { if (client.clientType === clientType) { return getProviderConfigForClient(input.config, client); } } } throw new Error(`Could not find client config for clientType: ${clientType}`); }, getAuthorisationRedirectURL: async function ({ redirectURIOnProviderDashboard }) { var _a; const queryParams = { client_id: impl.config.clientId, redirect_uri: redirectURIOnProviderDashboard, response_type: "code", }; if (impl.config.scope !== undefined) { queryParams.scope = impl.config.scope.join(" "); } let pkceCodeVerifier = undefined; // Check if the OIDC response had specified PKCE to be used. // Reference: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata // // Generally, we should try to use the best method supported by the provider. // However, we will only use the S256 method if it is present and ignore otherwise, // i.e. if `plain` is present etc. const isS256MethodSupported = (_a = impl.config.codeChallengeMethodsSupported) === null || _a === void 0 ? void 0 : _a.includes("S256"); if (impl.config.clientSecret === undefined || impl.config.forcePKCE || isS256MethodSupported) { const { code_challenge, code_verifier } = (0, pkce_challenge_1.default)(64); // According to https://www.rfc-editor.org/rfc/rfc7636, length must be between 43 and 128 queryParams["code_challenge"] = code_challenge; queryParams["code_challenge_method"] = "S256"; pkceCodeVerifier = code_verifier; } if (impl.config.authorizationEndpointQueryParams !== undefined) { for (const [key, value] of Object.entries(impl.config.authorizationEndpointQueryParams)) { if (value === null) { delete queryParams[key]; } else { queryParams[key] = value; } } } if (impl.config.authorizationEndpoint === undefined) { throw new Error("ThirdParty provider's authorizationEndpoint is not configured."); } let url = impl.config.authorizationEndpoint; /* Transformation needed for dev keys BEGIN */ if (isUsingDevelopmentClientId(impl.config.clientId)) { queryParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); queryParams["actual_redirect_uri"] = url; url = DEV_OAUTH_AUTHORIZATION_URL; } /* Transformation needed for dev keys END */ const urlObj = new URL(url); for (const [key, value] of Object.entries(queryParams)) { urlObj.searchParams.set(key, value); } return { urlWithQueryParams: urlObj.toString(), pkceCodeVerifier: pkceCodeVerifier, }; }, exchangeAuthCodeForOAuthTokens: async function ({ redirectURIInfo }) { if (impl.config.tokenEndpoint === undefined) { throw new Error("ThirdParty provider's tokenEndpoint is not configured."); } const tokenAPIURL = impl.config.tokenEndpoint; const accessTokenAPIParams = { client_id: impl.config.clientId, redirect_uri: redirectURIInfo.redirectURIOnProviderDashboard, code: redirectURIInfo.redirectURIQueryParams["code"], grant_type: "authorization_code", }; if (impl.config.clientSecret !== undefined) { accessTokenAPIParams["client_secret"] = impl.config.clientSecret; } if (redirectURIInfo.pkceCodeVerifier !== undefined) { accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; } for (const key in impl.config.tokenEndpointBodyParams) { if (impl.config.tokenEndpointBodyParams[key] === null) { delete accessTokenAPIParams[key]; } else { accessTokenAPIParams[key] = impl.config.tokenEndpointBodyParams[key]; } } /* Transformation needed for dev keys BEGIN */ if (isUsingDevelopmentClientId(impl.config.clientId)) { accessTokenAPIParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); accessTokenAPIParams["redirect_uri"] = exports.DEV_OAUTH_REDIRECT_URL; } /* Transformation needed for dev keys END */ const tokenResponse = await (0, thirdpartyUtils_1.doPostRequest)(tokenAPIURL, accessTokenAPIParams); if (tokenResponse.status >= 400) { (0, logger_1.logDebugMessage)(`Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}`); throw new Error(`Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}`); } return tokenResponse.jsonResponse; }, getUserInfo: async function ({ oAuthTokens, userContext }) { const accessToken = oAuthTokens["access_token"]; const idToken = oAuthTokens["id_token"]; let rawUserInfoFromProvider = { fromUserInfoAPI: {}, fromIdTokenPayload: {}, }; if (idToken && impl.config.jwksURI !== undefined) { if (jwks === undefined) { jwks = (0, jose_1.createRemoteJWKSet)(new URL(impl.config.jwksURI)); } rawUserInfoFromProvider.fromIdTokenPayload = await (0, thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload)(idToken, jwks, { audience: getActualClientIdFromDevelopmentClientId(impl.config.clientId), }); if (impl.config.validateIdTokenPayload !== undefined) { await impl.config.validateIdTokenPayload({ idTokenPayload: rawUserInfoFromProvider.fromIdTokenPayload, clientConfig: impl.config, userContext: (0, utils_1.getUserContext)(userContext), }); } } if (impl.config.validateAccessToken !== undefined && accessToken !== undefined) { await impl.config.validateAccessToken({ accessToken: accessToken, clientConfig: impl.config, userContext: (0, utils_1.getUserContext)(userContext), }); } if (accessToken && impl.config.userInfoEndpoint !== undefined) { const headers = { Authorization: "Bearer " + accessToken, }; const queryParams = {}; if (impl.config.userInfoEndpointHeaders !== undefined) { for (const [key, value] of Object.entries(impl.config.userInfoEndpointHeaders)) { if (value === null) { delete headers[key]; } else { headers[key] = value; } } } if (impl.config.userInfoEndpointQueryParams !== undefined) { for (const [key, value] of Object.entries(impl.config.userInfoEndpointQueryParams)) { if (value === null) { delete queryParams[key]; } else { queryParams[key] = value; } } } const userInfoFromAccessToken = await (0, thirdpartyUtils_1.doGetRequest)(impl.config.userInfoEndpoint, queryParams, headers); if (userInfoFromAccessToken.status >= 400) { (0, logger_1.logDebugMessage)(`Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}`); throw new Error(`Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}`); } rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; } const userInfoResult = getSupertokensUserInfoResultFromRawUserInfo(impl.config, rawUserInfoFromProvider); return { thirdPartyUserId: userInfoResult.thirdPartyUserId, email: userInfoResult.email, rawUserInfoFromProvider: rawUserInfoFromProvider, }; }, }; } else if (type === "saml") { impl = { id: input.config.thirdPartyId, type, config: Object.assign(Object.assign({}, input.config), { clientId: "temp" }), getConfigForClientType: async function ({ clientType }) { if (clientType === undefined) { if (input.config.clients === undefined || input.config.clients.length !== 1) { throw new Error("please provide exactly one client config or pass clientType or tenantId"); } return getProviderConfigForClient(input.config, input.config.clients[0]); } if (input.config.clients !== undefined) { for (const client of input.config.clients) { if (client.clientType === clientType) { return getProviderConfigForClient(input.config, client); } } } throw new Error(`Could not find client config for clientType: ${clientType}`); }, getAuthorisationRedirectURL: async function ({ tenantId, redirectURIOnProviderDashboard }) { const queryParams = { client_id: impl.config.clientId, redirect_uri: redirectURIOnProviderDashboard, }; return { urlWithQueryParams: appinfo.apiDomain.getAsStringDangerous() + appinfo.apiBasePath .appendPath(new normalisedURLPath_1.default(`/${tenantId}`)) .appendPath(new normalisedURLPath_1.default("/saml/login")) .getAsStringDangerous() + "?" + new URLSearchParams(queryParams).toString(), }; }, getUserInfo: async function ({ tenantId, accessToken, userContext }) { const samlRecipe = recipe_1.default.getInstanceOrThrowError(); const res = await samlRecipe.recipeInterfaceImpl.getUserInfo({ tenantId, clientId: impl.config.clientId, accessToken, userContext: (0, utils_1.getUserContext)(userContext), }); if (res.status !== "OK") { throw new Error(`Failed to get user info: ${res.status}`); } return { thirdPartyUserId: res.sub, email: { id: res.email, isVerified: true, }, rawUserInfoFromProvider: { fromUserInfoAPI: res, }, }; }, }; } else { throw new Error(`should never happen: ${type}`); } // No need to use an overrideable builder here because the functions in the `TypeProvider` // are independent of each other and they have no need to call each other from within. if (input.override !== undefined) { impl = input.override(impl); } return impl; }