From ed735e69caf6ab899c42baaf1231c3926894257f Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:43:17 -0500 Subject: [PATCH] Revert "Merge pull request #880 from Timothyw0/main" This reverts commit 5f440e3068be7d53e052fae17e58dcc5c8b108e5, reversing changes made to b298821949654745497e53bebbfec66619afb6a3. --- package-lock.json | 30 +--- package.json | 1 - src/core/constants.ts | 13 +- src/msha/auth/index.ts | 2 +- .../routes/auth-login-provider-callback.ts | 153 ++++++++++++------ .../auth/routes/auth-login-provider-custom.ts | 96 +++++------ src/swa.d.ts | 8 +- 7 files changed, 161 insertions(+), 142 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee0fb3c3..91f16b23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "internal-ip": "^6.2.0", "json-schema-library": "^9.3.5", "json-source-map": "^0.6.1", - "jwt-decode": "^4.0.0", "keytar": "^7.9.0", "node-fetch": "^2.7.0", "open": "^8.4.2", @@ -80,8 +79,8 @@ "vitest": "^2.0.2" }, "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" + "node": ">=14.0.0", + "npm": ">=6.0.0" } }, "node_modules/@ampproject/remapping": { @@ -3799,9 +3798,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -7531,14 +7530,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "engines": { - "node": ">=18" - } - }, "node_modules/keytar": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", @@ -17475,9 +17466,9 @@ "dev": true }, "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -20254,11 +20245,6 @@ "safe-buffer": "^5.0.1" } }, - "jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==" - }, "keytar": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", diff --git a/package.json b/package.json index a60d3160..2be1e25a 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "internal-ip": "^6.2.0", "json-schema-library": "^9.3.5", "json-source-map": "^0.6.1", - "jwt-decode": "^4.0.0", "keytar": "^7.9.0", "node-fetch": "^2.7.0", "open": "^8.4.2", diff --git a/src/core/constants.ts b/src/core/constants.ts index 4ea23819..a8dd52f1 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -50,7 +50,7 @@ export const SWA_AUTH_COOKIE = `StaticWebAppsAuthCookie`; export const ALLOWED_HTTP_METHODS_FOR_STATIC_CONTENT = ["GET", "HEAD", "OPTIONS"]; // Custom Auth constants -export const SUPPORTED_CUSTOM_AUTH_PROVIDERS = ["google", "github", "aad", "facebook", "dummy"]; +export const SUPPORTED_CUSTOM_AUTH_PROVIDERS = ["google", "github", "aad", "dummy"]; /* The full name is required in staticwebapp.config.json's schema that will be normalized to aad https://learn.microsoft.com/en-us/azure/static-web-apps/authentication-custom?tabs=aad%2Cinvitations @@ -69,10 +69,6 @@ export const CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { host: "login.microsoftonline.com", path: "/tenantId/oauth2/v2.0/token", }, - facebook: { - host: "graph.facebook.com", - path: "/v11.0/oauth/access_token", - }, }; export const CUSTOM_AUTH_USER_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { google: { @@ -92,13 +88,6 @@ export const CUSTOM_AUTH_ISS_MAPPING: AuthIdentityIssHosts = { google: "https://account.google.com", github: "", aad: "https://graph.microsoft.com", - facebook: "https://www.facebook.com", -}; -export const CUSTOM_AUTH_REQUIRED_FIELDS: AuthIdentityRequiredFields = { - google: ["clientIdSettingName", "clientSecretSettingName"], - github: ["clientIdSettingName", "clientSecretSettingName"], - aad: ["clientIdSettingName", "clientSecretSettingName", "openIdIssuer"], - facebook: ["appIdSettingName", "appSecretSettingName"], }; export const AUTH_STATUS = { diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index 3ddd8052..ac129559 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -25,7 +25,7 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { paths.push({ method: "GET", // For providers with custom auth support not implemented, revert to old behavior - route: /^\/\.auth\/login\/(?twitter|[a-z]+)(\?.*)?$/i, + route: /^\/\.auth\/login\/(?twitter|facebook|[a-z]+)(\?.*)?$/i, function: "auth-login-provider", }); paths.push({ diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index 4b9196ae..d8e2d47b 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -5,6 +5,7 @@ import * as querystring from "node:querystring"; import { CookiesManager, decodeAuthContextCookie, validateAuthContextCookie } from "../../../core/utils/cookie.js"; import { parseUrl, response } from "../../../core/utils/net.js"; import { + ENTRAID_FULL_NAME, CUSTOM_AUTH_ISS_MAPPING, CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING, CUSTOM_AUTH_USER_ENDPOINT_MAPPING, @@ -14,27 +15,26 @@ import { } from "../../../core/constants.js"; import { DEFAULT_CONFIG } from "../../../config.js"; import { encryptAndSign, hashStateGuid, isNonceExpired } from "../../../core/utils/auth.js"; -import { checkCustomAuthConfigFields, normalizeAuthProvider } from "./auth-login-provider-custom.js"; -import { jwtDecode } from "jwt-decode"; - -const getAuthClientPrincipal = async function (authProvider: string, codeValue: string, authConfigs: Record) { +import { normalizeAuthProvider } from "./auth-login-provider-custom.js"; + +const getAuthClientPrincipal = async function ( + authProvider: string, + codeValue: string, + clientId: string, + clientSecret: string, + openIdIssuer: string = "", +) { let authToken: string; try { - const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, authConfigs)) as string; + const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, clientId, clientSecret, openIdIssuer)) as string; let authTokenParsed; try { authTokenParsed = JSON.parse(authTokenResponse); } catch (e) { authTokenParsed = querystring.parse(authTokenResponse); } - - // Facebook sends back a JWT in the id_token - if (authProvider !== "facebook") { - authToken = authTokenParsed["access_token"] as string; - } else { - authToken = authTokenParsed["id_token"] as string; - } + authToken = authTokenParsed["access_token"] as string; } catch (error) { console.error(`Error in getting OAuth token: ${error}`); return null; @@ -62,11 +62,11 @@ const getAuthClientPrincipal = async function (authProvider: string, codeValue: }, { typ: "azp", - val: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName, + val: clientId, }, { typ: "aud", - val: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName, + val: clientId, }, ]; @@ -139,7 +139,7 @@ const getAuthClientPrincipal = async function (authProvider: string, codeValue: } }; -const getOAuthToken = function (authProvider: string, codeValue: string, authConfigs: Record) { +const getOAuthToken = function (authProvider: string, codeValue: string, clientId: string, clientSecret: string, openIdIssuer: string = "") { const redirectUri = `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}`; let tenantId; @@ -148,13 +148,13 @@ const getOAuthToken = function (authProvider: string, codeValue: string, authCon } if (authProvider === "aad") { - tenantId = authConfigs?.openIdIssuer.split("/")[3]; + tenantId = openIdIssuer.split("/")[3]; } const data = querystring.stringify({ code: codeValue, - client_id: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName, - client_secret: authConfigs?.clientSecretSettingName || authConfigs?.appSecretSettingName, + client_id: clientId, + client_secret: clientSecret, grant_type: "authorization_code", redirect_uri: `${redirectUri}/.auth/login/${authProvider}/callback`, }); @@ -198,45 +198,40 @@ const getOAuthToken = function (authProvider: string, codeValue: string, authCon }; const getOAuthUser = function (authProvider: string, accessToken: string) { - // Facebook does not have an OIDC introspection so we need to manually decode the token :( - if (authProvider === "facebook") { - return jwtDecode(accessToken); - } else { - const options = { - host: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.host, - path: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.path, - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - "User-Agent": "Azure Static Web Apps Emulator", - }, - }; - - return new Promise((resolve, reject) => { - const req = https.request(options, (res) => { - res.setEncoding("utf8"); - let responseBody = ""; + const options = { + host: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.host, + path: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.path, + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "User-Agent": "Azure Static Web Apps Emulator", + }, + }; - res.on("data", (chunk) => { - responseBody += chunk; - }); + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + res.setEncoding("utf8"); + let responseBody = ""; - res.on("end", () => { - try { - resolve(JSON.parse(responseBody)); - } catch (err) { - reject(err); - } - }); + res.on("data", (chunk) => { + responseBody += chunk; }); - req.on("error", (err) => { - reject(err); + res.on("end", () => { + try { + resolve(JSON.parse(responseBody)); + } catch (err) { + reject(err); + } }); + }); - req.end(); + req.on("error", (err) => { + reject(err); }); - } + + req.end(); + }); }; const getRoles = function (clientPrincipal: RolesSourceFunctionRequestBody, rolesSource: string) { @@ -339,12 +334,64 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess return; } - const authConfigs = checkCustomAuthConfigFields(context, providerName, customAuth); - if (!authConfigs) { + const { clientIdSettingName, clientSecretSettingName, openIdIssuer } = + customAuth?.identityProviders?.[providerName == "aad" ? ENTRAID_FULL_NAME : providerName]?.registration || {}; + + if (!clientIdSettingName) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `ClientIdSettingName not found for '${providerName}' provider`, + }); + return; + } + + if (!clientSecretSettingName) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `ClientSecretSettingName not found for '${providerName}' provider`, + }); + return; + } + + if (providerName == "aad" && !openIdIssuer) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `openIdIssuer not found for '${providerName}' provider`, + }); + return; + } + + const clientId = process.env[clientIdSettingName]; + + if (!clientId) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `ClientId not found for '${providerName}' provider`, + }); + return; + } + + const clientSecret = process.env[clientSecretSettingName]; + + if (!clientSecret) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `ClientSecret not found for '${providerName}' provider`, + }); return; } - const clientPrincipal = await getAuthClientPrincipal(providerName, codeValue!, authConfigs); + const clientPrincipal = await getAuthClientPrincipal(providerName, codeValue!, clientId, clientSecret, openIdIssuer!); if (clientPrincipal !== null && customAuth?.rolesSource) { try { diff --git a/src/msha/auth/routes/auth-login-provider-custom.ts b/src/msha/auth/routes/auth-login-provider-custom.ts index 5430a3d1..5df1c564 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -1,69 +1,72 @@ import { IncomingMessage } from "node:http"; import { CookiesManager } from "../../../core/utils/cookie.js"; import { response } from "../../../core/utils/net.js"; -import { CUSTOM_AUTH_REQUIRED_FIELDS, ENTRAID_FULL_NAME, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; +import { ENTRAID_FULL_NAME, SUPPORTED_CUSTOM_AUTH_PROVIDERS, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; import { DEFAULT_CONFIG } from "../../../config.js"; import { encryptAndSign, extractPostLoginRedirectUri, hashStateGuid, newNonceWithExpiration } from "../../../core/utils/auth.js"; -export const normalizeAuthProvider = function (providerName?: string) { +export const normalizeAuthProvider = (providerName?: string) => { if (providerName === ENTRAID_FULL_NAME) { return "aad"; } return providerName?.toLowerCase() || ""; }; -export const checkCustomAuthConfigFields = function (context: Context, providerName: string, customAuth?: SWAConfigFileAuth) { - const generateResponse = function (msg: string) { - return { +const httpTrigger = async function (context: Context, request: IncomingMessage, customAuth?: SWAConfigFileAuth) { + await Promise.resolve(); + + const providerName: string = normalizeAuthProvider(context.bindingData?.provider); + + if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { + context.res = response({ context, status: 400, headers: { ["Content-Type"]: "text/plain" }, - body: msg, - }; - }; - - if (!CUSTOM_AUTH_REQUIRED_FIELDS[providerName]) { - context.res = response(generateResponse(`Provider '${providerName}' not found`)); - return false; + body: `Provider '${providerName}' not found`, + }); + return; } - const requiredFields = CUSTOM_AUTH_REQUIRED_FIELDS[providerName]; - const configFileProviderName = providerName === "aad" ? ENTRAID_FULL_NAME : providerName; - const authConfigs: Record = {}; - - for (const field of requiredFields) { - const settingName = customAuth?.identityProviders?.[configFileProviderName]?.registration?.[field]; - if (!settingName) { - context.res = response(generateResponse(`${field} not found for '${providerName}' provider`)); - return false; - } + const clientIdSettingName = + customAuth?.identityProviders?.[providerName == "aad" ? ENTRAID_FULL_NAME : providerName]?.registration?.clientIdSettingName; - // Special case for aad where the openIdIssuer is in the config file itself rather than the env - if (providerName === "aad" && field === "openIdIssuer") { - authConfigs[field] = settingName; - } else { - const settingValue = process.env[settingName]; - if (!settingValue) { - context.res = response(generateResponse(`${settingName} not found in env for '${providerName}' provider`)); - return false; - } - - authConfigs[field] = settingValue; - } + if (!clientIdSettingName) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `ClientIdSettingName not found for '${providerName}' provider`, + }); + return; } - return authConfigs; -}; - -const httpTrigger = async function (context: Context, request: IncomingMessage, customAuth?: SWAConfigFileAuth) { - await Promise.resolve(); + const clientId = process.env[clientIdSettingName]; - const providerName: string = normalizeAuthProvider(context.bindingData?.provider); - const authFields = checkCustomAuthConfigFields(context, providerName, customAuth); - if (!authFields) { + if (!clientId) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `ClientId not found for '${providerName}' provider`, + }); return; } + let aadIssuer; + if (providerName == "aad") { + aadIssuer = customAuth?.identityProviders?.[ENTRAID_FULL_NAME]?.registration?.openIdIssuer; + + if (!aadIssuer) { + context.res = response({ + context, + status: 400, + headers: { ["Content-Type"]: "text/plain" }, + body: `openIdIssuer not found for '${providerName}' provider`, + }); + return; + } + } + const state = newNonceWithExpiration(); const authContext: AuthContext = { @@ -81,16 +84,13 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, let location; switch (providerName) { case "google": - location = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${authFields?.clientIdSettingName}&redirect_uri=${redirectUri}/.auth/login/google/callback&scope=openid+profile+email&state=${hashedState}`; + location = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/google/callback&scope=openid+profile+email&state=${hashedState}`; break; case "github": - location = `https://github.com/login/oauth/authorize?response_type=code&client_id=${authFields?.clientIdSettingName}&redirect_uri=${redirectUri}/.auth/login/github/callback&scope=read:user&state=${hashedState}`; + location = `https://github.com/login/oauth/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/github/callback&scope=read:user&state=${hashedState}`; break; case "aad": - location = `${authFields?.openIdIssuer}/authorize?response_type=code&client_id=${authFields?.clientIdSettingName}&redirect_uri=${redirectUri}/.auth/login/aad/callback&scope=openid+profile+email&state=${hashedState}`; - break; - case "facebook": - location = `https://facebook.com/v11.0/dialog/oauth?client_id=${authFields?.appIdSettingName}&redirect_uri=${redirectUri}/.auth/login/facebook/callback&scope=openid&state=${hashedState}&response_type=code`; + location = `${aadIssuer}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/aad/callback&scope=openid+profile+email&state=${hashedState}`; break; default: break; diff --git a/src/swa.d.ts b/src/swa.d.ts index 85023c80..6e480b4a 100644 --- a/src/swa.d.ts +++ b/src/swa.d.ts @@ -303,14 +303,12 @@ declare type AuthIdentityIssHosts = { declare type AuthIdentityProvider = { registration: { - [key: string]: string; + clientIdSettingName: string; + clientSecretSettingName: string; + openIdIssuer?: string; }; }; -declare type AuthIdentityRequiredFields = { - [key: string]: string[]; -}; - declare type SWAConfigFileAuthIdenityProviders = { [key: string]: AuthIdentityProvider; };