From a2467564eb5dd7cee1dda5f26decbb4296a96402 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Thu, 17 Oct 2024 09:54:42 -0400 Subject: [PATCH 1/9] Fix undefined subscriptions in azureprofile.json --- src/cli/commands/login/login.ts | 16 +++++++++------- src/swa.d.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/login/login.ts b/src/cli/commands/login/login.ts index aceecd6b..682784ad 100644 --- a/src/cli/commands/login/login.ts +++ b/src/cli/commands/login/login.ts @@ -56,7 +56,7 @@ async function setupProjectCredentials(options: SWACLIConfig, credentialChain: T const tenants = await listTenants(credentialChain); if (tenants.length === 0) { throw new Error( - `No Azure tenants found in your account.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/troubleshoot-sign-in-issue` + `No Azure tenants found in your account.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/troubleshoot-sign-in-issue`, ); } else if (tenants.length === 1) { logger.silly(`Found 1 tenant: ${tenants[0].tenantId}`); @@ -81,7 +81,7 @@ async function setupProjectCredentials(options: SWACLIConfig, credentialChain: T const subscriptions = await listSubscriptions(credentialChain); if (subscriptions.length === 0) { throw new Error( - `No valid subscription found for tenant ${tenantId}.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/no-subscriptions-found` + `No valid subscription found for tenant ${tenantId}.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/no-subscriptions-found`, ); } else if (subscriptions.length === 1) { logger.silly(`Found 1 subscription: ${subscriptions[0].subscriptionId}`); @@ -109,7 +109,7 @@ async function storeProjectCredentialsInEnvFile( subscriptionId: string | undefined, tenantId: string | undefined, clientId: string | undefined, - clientSecret: string | undefined + clientSecret: string | undefined, ) { const envFile = path.join(process.cwd(), ENV_FILENAME); const envFileExists = existsSync(envFile); @@ -162,10 +162,12 @@ async function tryGetAzTenantAndSubscription(options: SWACLIConfig) { const azureProfile = await safeReadJson(AZURE_LOGIN_CONFIG); if (azureProfile) { const allSubscriptions = (azureProfile as AzureProfile).subscriptions; - const defaultAzureInfo = allSubscriptions.find((subscription) => subscription.isDefault == true); - if (defaultAzureInfo) { - options.tenantId = defaultAzureInfo.tenantId; - options.subscriptionId = defaultAzureInfo.id; + if (allSubscriptions) { + const defaultAzureInfo = allSubscriptions.find((subscription) => subscription.isDefault == true); + if (defaultAzureInfo) { + options.tenantId = defaultAzureInfo.tenantId; + options.subscriptionId = defaultAzureInfo.id; + } } } diff --git a/src/swa.d.ts b/src/swa.d.ts index 85023c80..c6df14d8 100644 --- a/src/swa.d.ts +++ b/src/swa.d.ts @@ -442,5 +442,5 @@ declare type AzureLoginInfo = { declare interface AzureProfile { installationId: string; - subscriptions: AzureLoginInfo[]; + subscriptions?: AzureLoginInfo[]; } From ec3894989eec0b22dea3cfcfb6972efaf111ae3b Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:41:07 -0500 Subject: [PATCH 2/9] Revert "Merge pull request #895 from jonnekleijer/fix/844-identity-provider-regex" This reverts commit c74de886b65a129d59c66d193ccb9dbd4b77d58a, reversing changes made to 6745391a3045458c91fe7e6cba3044e960a1ea9a. --- src/msha/auth/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index 27317821..d313545c 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -36,7 +36,7 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { } else { paths.push({ method: "GET", - route: /^\/\.auth\/login\/(?github|twitter|google|facebook|[a-z0-9]+)(\?.*)?$/i, + route: /^\/\.auth\/login\/(?github|twitter|google|facebook|[a-z]+)(\?.*)?$/i, function: "auth-login-provider", }); } @@ -54,7 +54,7 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { }, { method: "GET", - route: /^\/\.auth\/purge\/(?aad|github|twitter|google|facebook|[a-z0-9]+)(\?.*)?$/i, + route: /^\/\.auth\/purge\/(?aad|github|twitter|google|facebook|[a-z]+)(\?.*)?$/i, // locally, all purge requests are processed as logout requests function: "auth-logout", }, From 49a6bbc49db7850b9e4d70f5ca158b968fa51c26 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:42:47 -0500 Subject: [PATCH 3/9] Revert "Fix undefined subscriptions in azureprofile.json" This reverts commit a2467564eb5dd7cee1dda5f26decbb4296a96402. --- src/cli/commands/login/login.ts | 16 +++++++--------- src/swa.d.ts | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/cli/commands/login/login.ts b/src/cli/commands/login/login.ts index 682784ad..aceecd6b 100644 --- a/src/cli/commands/login/login.ts +++ b/src/cli/commands/login/login.ts @@ -56,7 +56,7 @@ async function setupProjectCredentials(options: SWACLIConfig, credentialChain: T const tenants = await listTenants(credentialChain); if (tenants.length === 0) { throw new Error( - `No Azure tenants found in your account.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/troubleshoot-sign-in-issue`, + `No Azure tenants found in your account.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/troubleshoot-sign-in-issue` ); } else if (tenants.length === 1) { logger.silly(`Found 1 tenant: ${tenants[0].tenantId}`); @@ -81,7 +81,7 @@ async function setupProjectCredentials(options: SWACLIConfig, credentialChain: T const subscriptions = await listSubscriptions(credentialChain); if (subscriptions.length === 0) { throw new Error( - `No valid subscription found for tenant ${tenantId}.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/no-subscriptions-found`, + `No valid subscription found for tenant ${tenantId}.\n Please read https://docs.microsoft.com/azure/cost-management-billing/manage/no-subscriptions-found` ); } else if (subscriptions.length === 1) { logger.silly(`Found 1 subscription: ${subscriptions[0].subscriptionId}`); @@ -109,7 +109,7 @@ async function storeProjectCredentialsInEnvFile( subscriptionId: string | undefined, tenantId: string | undefined, clientId: string | undefined, - clientSecret: string | undefined, + clientSecret: string | undefined ) { const envFile = path.join(process.cwd(), ENV_FILENAME); const envFileExists = existsSync(envFile); @@ -162,12 +162,10 @@ async function tryGetAzTenantAndSubscription(options: SWACLIConfig) { const azureProfile = await safeReadJson(AZURE_LOGIN_CONFIG); if (azureProfile) { const allSubscriptions = (azureProfile as AzureProfile).subscriptions; - if (allSubscriptions) { - const defaultAzureInfo = allSubscriptions.find((subscription) => subscription.isDefault == true); - if (defaultAzureInfo) { - options.tenantId = defaultAzureInfo.tenantId; - options.subscriptionId = defaultAzureInfo.id; - } + const defaultAzureInfo = allSubscriptions.find((subscription) => subscription.isDefault == true); + if (defaultAzureInfo) { + options.tenantId = defaultAzureInfo.tenantId; + options.subscriptionId = defaultAzureInfo.id; } } diff --git a/src/swa.d.ts b/src/swa.d.ts index c6df14d8..85023c80 100644 --- a/src/swa.d.ts +++ b/src/swa.d.ts @@ -442,5 +442,5 @@ declare type AzureLoginInfo = { declare interface AzureProfile { installationId: string; - subscriptions?: AzureLoginInfo[]; + subscriptions: AzureLoginInfo[]; } From dea34ff124369dfb103bb75d952d46c2e0e95227 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:43:01 -0500 Subject: [PATCH 4/9] Revert "Merge pull request #889 from Timothyw0/main" This reverts commit 6745391a3045458c91fe7e6cba3044e960a1ea9a, reversing changes made to 5f440e3068be7d53e052fae17e58dcc5c8b108e5. --- .github/workflows/azuresdkdrop.yml | 2 +- .github/workflows/ci.yml | 2 +- src/core/constants.ts | 12 +----- src/msha/auth/index.ts | 4 +- .../routes/auth-login-provider-callback.ts | 42 ++++++------------- .../auth/routes/auth-login-provider-custom.ts | 3 -- 6 files changed, 18 insertions(+), 47 deletions(-) diff --git a/.github/workflows/azuresdkdrop.yml b/.github/workflows/azuresdkdrop.yml index 42e0cb5e..1e49f8cd 100644 --- a/.github/workflows/azuresdkdrop.yml +++ b/.github/workflows/azuresdkdrop.yml @@ -45,7 +45,7 @@ jobs: - run: npm pack - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v2 with: name: package path: "*.tgz" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3850931c..1bdf51f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,7 +228,7 @@ jobs: - run: npm version prerelease --preid=ci-$GITHUB_RUN_ID --no-git-tag-version - run: npm pack - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v2 with: name: static-web-apps-cli path: "*.tgz" diff --git a/src/core/constants.ts b/src/core/constants.ts index 86e71a46..4ea23819 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", "twitter", "dummy"]; +export const SUPPORTED_CUSTOM_AUTH_PROVIDERS = ["google", "github", "aad", "facebook", "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 @@ -73,10 +73,6 @@ export const CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { host: "graph.facebook.com", path: "/v11.0/oauth/access_token", }, - twitter: { - host: "api.twitter.com", - path: "/2/oauth2/token", - }, }; export const CUSTOM_AUTH_USER_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { google: { @@ -91,24 +87,18 @@ export const CUSTOM_AUTH_USER_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { host: "graph.microsoft.com", path: "/oidc/userinfo", }, - twitter: { - host: "api.twitter.com", - path: "/2/users/me", - }, }; export const CUSTOM_AUTH_ISS_MAPPING: AuthIdentityIssHosts = { google: "https://account.google.com", github: "", aad: "https://graph.microsoft.com", facebook: "https://www.facebook.com", - twitter: "https://www.x.com", }; export const CUSTOM_AUTH_REQUIRED_FIELDS: AuthIdentityRequiredFields = { google: ["clientIdSettingName", "clientSecretSettingName"], github: ["clientIdSettingName", "clientSecretSettingName"], aad: ["clientIdSettingName", "clientSecretSettingName", "openIdIssuer"], facebook: ["appIdSettingName", "appSecretSettingName"], - twitter: ["consumerKeySettingName", "consumerSecretSettingName"], }; export const AUTH_STATUS = { diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index d313545c..3ddd8052 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -12,13 +12,13 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { paths.push({ method: "GET", - // only match for providers with custom auth support implemented (github, google, aad, facebook, twitter) + // only match for providers with custom auth support implemented (github, google, aad) route: new RegExp(`^/\\.auth/login/(?${supportedAuthsRegex})/callback(\\?.*)?$`, "i"), function: "auth-login-provider-callback", }); paths.push({ method: "GET", - // only match for providers with custom auth support implemented (github, google, aad, facebook, twitter) + // only match for providers with custom auth support implemented (github, google, aad) route: new RegExp(`^/\\.auth/login/(?${supportedAuthsRegex})(\\?.*)?$`, "i"), function: "auth-login-provider-custom", }); diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index 617141a4..4b9196ae 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -45,14 +45,14 @@ const getAuthClientPrincipal = async function (authProvider: string, codeValue: } try { - const user = (await getOAuthUser(authProvider, authToken)) as Record; + const user = (await getOAuthUser(authProvider, authToken)) as { [key: string]: string }; - const userDetails = user["login"] || user["email"] || user?.data?.["username"]; - const name = user["name"] || user?.data?.["name"]; + const userDetails = user["login"] || user["email"]; + const name = user["name"]; const givenName = user["given_name"]; const familyName = user["family_name"]; const picture = user["picture"]; - const userId = user["id"] || user?.data?.["id"]; + const userId = user["id"]; const verifiedEmail = user["verified_email"]; const claims: { typ: string; val: string }[] = [ @@ -134,8 +134,7 @@ const getAuthClientPrincipal = async function (authProvider: string, codeValue: claims, userRoles: ["authenticated", "anonymous"], }; - } catch (error) { - console.error(`Error while parsing user information: ${error}`); + } catch { return null; } }; @@ -152,42 +151,27 @@ const getOAuthToken = function (authProvider: string, codeValue: string, authCon tenantId = authConfigs?.openIdIssuer.split("/")[3]; } - const queryString: Record = { + const data = querystring.stringify({ code: codeValue, + client_id: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName, + client_secret: authConfigs?.clientSecretSettingName || authConfigs?.appSecretSettingName, grant_type: "authorization_code", redirect_uri: `${redirectUri}/.auth/login/${authProvider}/callback`, - }; - - if (authProvider !== "twitter") { - queryString.client_id = authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName; - queryString.client_secret = authConfigs?.clientSecretSettingName || authConfigs?.appSecretSettingName; - } else { - queryString.code_verifier = "challenge"; - } - - const data = querystring.stringify(queryString); + }); let tokenPath = CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.path; if (authProvider === "aad" && tenantId !== undefined) { tokenPath = tokenPath.replace("tenantId", tenantId); } - const headers: Record = { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Length": Buffer.byteLength(data), - }; - - if (authProvider === "twitter") { - const keySecretString = `${authConfigs?.consumerKeySettingName}:${authConfigs?.consumerSecretSettingName}`; - const encryptedCredentials = Buffer.from(keySecretString).toString("base64"); - headers.Authorization = `Basic ${encryptedCredentials}`; - } - const options = { host: CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.host, path: tokenPath, method: "POST", - headers: headers, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": Buffer.byteLength(data), + }, }; return new Promise((resolve, reject) => { diff --git a/src/msha/auth/routes/auth-login-provider-custom.ts b/src/msha/auth/routes/auth-login-provider-custom.ts index 600dcf20..5430a3d1 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -92,9 +92,6 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, 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`; break; - case "twitter": - location = `https://twitter.com/i/oauth2/authorize?response_type=code&client_id=${authFields?.consumerKeySettingName}&redirect_uri=${redirectUri}/.auth/login/twitter/callback&scope=users.read%20tweet.read&state=${hashedState}&code_challenge=challenge&code_challenge_method=plain`; - break; default: break; } From ed735e69caf6ab899c42baaf1231c3926894257f Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:43:17 -0500 Subject: [PATCH 5/9] 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; }; From c62e7d9f568ea620d24fd65bc65e9b2804394e67 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:43:33 -0500 Subject: [PATCH 6/9] Revert "Merge pull request #883 from codingoutloud/main" This reverts commit b298821949654745497e53bebbfec66619afb6a3, reversing changes made to 79383841e87ce0f430c843c6ba0db4b4ca6d378f. --- docs/www/docs/cli/swa.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/www/docs/cli/swa.md b/docs/www/docs/cli/swa.md index 707817e3..a8b59e65 100644 --- a/docs/www/docs/cli/swa.md +++ b/docs/www/docs/cli/swa.md @@ -19,7 +19,7 @@ This is the commandline utility for streamlining local development for Azure Sta - start emulator or bind to dev server - deploy project to Azure Static Web Apps -If you don't enter any command and run `swa`, it will act as a macro command shortcut for `swa init`, `swa build`, `swa login` and `swa deploy`. The `swa init` command will only be executed if the `swa-cli.config.json` does not exist in the current folder. +If you don't enter any command and run `swa`, it will act as a macro command shorcut for `swa init`, `swa build`, `swa login` and `swa deploy`. The `swa init` command will only be executed if the `swa-cli.config.json` does not exist in the current folder. The best way to get started is to use the `swa` command and follow the interactive prompts. From 83d3ca4e904e5b756b985450182bdd567e31aee2 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:43:55 -0500 Subject: [PATCH 7/9] Revert "Merge pull request #878 from Timothyw0/main" This reverts commit 79383841e87ce0f430c843c6ba0db4b4ca6d378f, reversing changes made to 8d0b7aab27f770070de75b7c58889935da95a9c7. --- src/core/constants.ts | 41 -- src/msha/auth/index.ts | 15 +- .../routes/auth-login-provider-callback.ts | 363 +++++++++++------- .../auth/routes/auth-login-provider-custom.ts | 55 +-- src/swa.d.ts | 19 +- 5 files changed, 238 insertions(+), 255 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index a8dd52f1..edb062df 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -49,47 +49,6 @@ export const SWA_AUTH_CONTEXT_COOKIE = `StaticWebAppsAuthContextCookie`; 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", "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 -*/ -export const ENTRAID_FULL_NAME = "azureActiveDirectory"; -export const CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { - google: { - host: "oauth2.googleapis.com", - path: "/token", - }, - github: { - host: "github.com", - path: "/login/oauth/access_token", - }, - aad: { - host: "login.microsoftonline.com", - path: "/tenantId/oauth2/v2.0/token", - }, -}; -export const CUSTOM_AUTH_USER_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { - google: { - host: "www.googleapis.com", - path: "/oauth2/v2/userinfo", - }, - github: { - host: "api.github.com", - path: "/user", - }, - aad: { - host: "graph.microsoft.com", - path: "/oidc/userinfo", - }, -}; -export const CUSTOM_AUTH_ISS_MAPPING: AuthIdentityIssHosts = { - google: "https://account.google.com", - github: "", - aad: "https://graph.microsoft.com", -}; - export const AUTH_STATUS = { NoAuth: 0, HostNameAuthLogin: 1, diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index ac129559..ddc0c812 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -2,30 +2,27 @@ import type http from "node:http"; import { serializeCookie } from "../../core/utils/cookie.js"; import { logger } from "../../core/utils/logger.js"; import { response as newResponse } from "../../core/utils/net.js"; -import { SUPPORTED_CUSTOM_AUTH_PROVIDERS } from "../../core/constants.js"; function getAuthPaths(isCustomAuth: boolean): Path[] { const paths: Path[] = []; if (isCustomAuth) { - const supportedAuthsRegex = SUPPORTED_CUSTOM_AUTH_PROVIDERS.join("|"); - paths.push({ method: "GET", - // only match for providers with custom auth support implemented (github, google, aad) - route: new RegExp(`^/\\.auth/login/(?${supportedAuthsRegex})/callback(\\?.*)?$`, "i"), + // only match for providers with custom auth support implemented (github, google) + route: /^\/\.auth\/login\/(?github|google|dummy)\/callback(\?.*)?$/i, function: "auth-login-provider-callback", }); paths.push({ method: "GET", - // only match for providers with custom auth support implemented (github, google, aad) - route: new RegExp(`^/\\.auth/login/(?${supportedAuthsRegex})(\\?.*)?$`, "i"), + // only match for providers with custom auth support implemented (github, google) + route: /^\/\.auth\/login\/(?github|google|dummy)(\?.*)?$/i, function: "auth-login-provider-custom", }); paths.push({ method: "GET", // For providers with custom auth support not implemented, revert to old behavior - route: /^\/\.auth\/login\/(?twitter|facebook|[a-z]+)(\?.*)?$/i, + route: /^\/\.auth\/login\/(?aad|twitter|facebook|[a-z]+)(\?.*)?$/i, function: "auth-login-provider", }); paths.push({ @@ -36,7 +33,7 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { } else { paths.push({ method: "GET", - route: /^\/\.auth\/login\/(?github|twitter|google|facebook|[a-z]+)(\?.*)?$/i, + route: /^\/\.auth\/login\/(?aad|github|twitter|google|facebook|[a-z]+)(\?.*)?$/i, function: "auth-login-provider", }); } diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index d8e2d47b..41ddc4cf 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -4,39 +4,95 @@ 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, - SUPPORTED_CUSTOM_AUTH_PROVIDERS, - SWA_CLI_API_URI, - SWA_CLI_APP_PROTOCOL, -} from "../../../core/constants.js"; +import { SWA_CLI_API_URI, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; import { DEFAULT_CONFIG } from "../../../config.js"; import { encryptAndSign, hashStateGuid, isNonceExpired } from "../../../core/utils/auth.js"; -import { normalizeAuthProvider } from "./auth-login-provider-custom.js"; - -const getAuthClientPrincipal = async function ( - authProvider: string, - codeValue: string, - clientId: string, - clientSecret: string, - openIdIssuer: string = "", -) { + +const getGithubAuthToken = function (codeValue: string, clientId: string, clientSecret: string) { + const data = querystring.stringify({ + code: codeValue, + client_id: clientId, + client_secret: clientSecret, + }); + + const options = { + host: "github.com", + path: "/login/oauth/access_token", + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": Buffer.byteLength(data), + }, + }; + + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + res.setEncoding("utf8"); + let responseBody = ""; + + res.on("data", (chunk) => { + responseBody += chunk; + }); + + res.on("end", () => { + resolve(responseBody); + }); + }); + + req.on("error", (err: Error) => { + reject(err); + }); + + req.write(data); + req.end(); + }); +}; + +const getGitHubUser = function (accessToken: string) { + const options = { + host: "api.github.com", + path: "/user", + 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 = ""; + + res.on("data", (chunk) => { + responseBody += chunk; + }); + + res.on("end", () => { + try { + resolve(JSON.parse(responseBody)); + } catch (err) { + reject(err); + } + }); + }); + + req.on("error", (err) => { + reject(err); + }); + + req.end(); + }); +}; + +const getGitHubClientPrincipal = async function (codeValue: string, clientId: string, clientSecret: string) { let authToken: string; try { - const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, clientId, clientSecret, openIdIssuer)) as string; - let authTokenParsed; - try { - authTokenParsed = JSON.parse(authTokenResponse); - } catch (e) { - authTokenParsed = querystring.parse(authTokenResponse); - } + const authTokenResponse = (await getGithubAuthToken(codeValue, clientId, clientSecret)) as string; + const authTokenParsed = querystring.parse(authTokenResponse); authToken = authTokenParsed["access_token"] as string; - } catch (error) { - console.error(`Error in getting OAuth token: ${error}`); + } catch { return null; } @@ -45,128 +101,49 @@ const getAuthClientPrincipal = async function ( } try { - const user = (await getOAuthUser(authProvider, authToken)) as { [key: string]: string }; + const user = (await getGitHubUser(authToken)) as { [key: string]: string }; - const userDetails = user["login"] || user["email"]; - const name = user["name"]; - const givenName = user["given_name"]; - const familyName = user["family_name"]; - const picture = user["picture"]; const userId = user["id"]; - const verifiedEmail = user["verified_email"]; + const userDetails = user["login"]; const claims: { typ: string; val: string }[] = [ { - typ: "iss", - val: CUSTOM_AUTH_ISS_MAPPING?.[authProvider], - }, - { - typ: "azp", - val: clientId, - }, - { - typ: "aud", - val: clientId, - }, - ]; - - if (userDetails) { - claims.push({ - typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", - val: userDetails, - }); - } - - if (name) { - claims.push({ - typ: "name", - val: name, - }); - } - - if (picture) { - claims.push({ - typ: "picture", - val: picture, - }); - } - - if (givenName) { - claims.push({ - typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", - val: givenName, - }); - } - - if (familyName) { - claims.push({ - typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", - val: familyName, - }); - } - - if (userId) { - claims.push({ typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", val: userId, - }); - } + }, + ]; - if (verifiedEmail) { + Object.keys(user).forEach((key) => { claims.push({ - typ: "email_verified", - val: verifiedEmail, - }); - } - - if (authProvider === "github") { - Object.keys(user).forEach((key) => { - claims.push({ - typ: `urn:github:${key}`, - val: user[key], - }); + typ: `urn:github:${key}`, + val: user[key], }); - } + }); return { - identityProvider: authProvider, + identityProvider: "github", + userId, userDetails, - claims, userRoles: ["authenticated", "anonymous"], + claims, }; } catch { return null; } }; -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; - - if (!Object.keys(CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING).includes(authProvider)) { - return null; - } - - if (authProvider === "aad") { - tenantId = openIdIssuer.split("/")[3]; - } - +const getGoogleAuthToken = function (codeValue: string, clientId: string, clientSecret: string) { const data = querystring.stringify({ code: codeValue, client_id: clientId, client_secret: clientSecret, grant_type: "authorization_code", - redirect_uri: `${redirectUri}/.auth/login/${authProvider}/callback`, + redirect_uri: `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}/.auth/login/google/callback`, }); - let tokenPath = CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.path; - if (authProvider === "aad" && tenantId !== undefined) { - tokenPath = tokenPath.replace("tenantId", tenantId); - } - const options = { - host: CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.host, - path: tokenPath, + host: "oauth2.googleapis.com", + path: "/token", method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", @@ -197,10 +174,10 @@ const getOAuthToken = function (authProvider: string, codeValue: string, clientI }); }; -const getOAuthUser = function (authProvider: string, accessToken: string) { +const getGoogleUser = function (accessToken: string) { const options = { - host: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.host, - path: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.path, + host: "www.googleapis.com", + path: "/oauth2/v2/userinfo", method: "GET", headers: { Authorization: `Bearer ${accessToken}`, @@ -234,6 +211,108 @@ const getOAuthUser = function (authProvider: string, accessToken: string) { }); }; +const getGoogleClientPrincipal = async function (codeValue: string, clientId: string, clientSecret: string) { + let authToken: string; + + try { + const authTokenResponse = (await getGoogleAuthToken(codeValue!, clientId, clientSecret)) as string; + const authTokenParsed = JSON.parse(authTokenResponse); + authToken = authTokenParsed["access_token"] as string; + } catch { + return null; + } + + if (!authToken) { + return null; + } + + try { + const user = (await getGoogleUser(authToken)) as { [key: string]: string }; + + const userId = user["id"]; + const userDetails = user["email"]; + const verifiedEmail = user["verified_email"]; + const name = user["name"]; + const givenName = user["given_name"]; + const familyName = user["family_name"]; + const picture = user["picture"]; + + const claims: { typ: string; val: string }[] = [ + { + typ: "iss", + val: "https://accounts.google.com", + }, + { + typ: "azp", + val: clientId, + }, + { + typ: "aud", + val: clientId, + }, + ]; + + if (userId) { + claims.push({ + typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", + val: userId, + }); + } + + if (userDetails) { + claims.push({ + typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + val: userDetails, + }); + } + + if (verifiedEmail !== undefined) { + claims.push({ + typ: "email_verified", + val: verifiedEmail, + }); + } + + if (name) { + claims.push({ + typ: "name", + val: name, + }); + } + + if (picture) { + claims.push({ + typ: "picture", + val: picture, + }); + } + + if (givenName) { + claims.push({ + typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", + val: givenName, + }); + } + + if (familyName) { + claims.push({ + typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", + val: familyName, + }); + } + + return { + identityProvider: "google", + userId, + userDetails, + claims, + userRoles: ["authenticated", "anonymous"], + }; + } catch { + return null; + } +}; + const getRoles = function (clientPrincipal: RolesSourceFunctionRequestBody, rolesSource: string) { let cliApiUri = SWA_CLI_API_URI(); const { protocol, hostname, port } = parseUrl(cliApiUri); @@ -283,12 +362,12 @@ const getRoles = function (clientPrincipal: RolesSourceFunctionRequestBody, role }; const httpTrigger = async function (context: Context, request: http.IncomingMessage, customAuth?: SWAConfigFileAuth) { - const providerName = normalizeAuthProvider(context.bindingData?.provider); + const providerName = context.bindingData?.provider?.toLowerCase() || ""; - if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { + if (providerName != "github" && providerName != "google") { context.res = response({ context, - status: 400, + status: 404, headers: { ["Content-Type"]: "text/plain" }, body: `Provider '${providerName}' not found`, }); @@ -334,13 +413,12 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess return; } - const { clientIdSettingName, clientSecretSettingName, openIdIssuer } = - customAuth?.identityProviders?.[providerName == "aad" ? ENTRAID_FULL_NAME : providerName]?.registration || {}; + const { clientIdSettingName, clientSecretSettingName } = customAuth?.identityProviders?.[providerName]?.registration || {}; if (!clientIdSettingName) { context.res = response({ context, - status: 400, + status: 404, headers: { ["Content-Type"]: "text/plain" }, body: `ClientIdSettingName not found for '${providerName}' provider`, }); @@ -350,29 +428,19 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess if (!clientSecretSettingName) { context.res = response({ context, - status: 400, + status: 404, 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, + status: 404, headers: { ["Content-Type"]: "text/plain" }, body: `ClientId not found for '${providerName}' provider`, }); @@ -384,19 +452,22 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess if (!clientSecret) { context.res = response({ context, - status: 400, + status: 404, headers: { ["Content-Type"]: "text/plain" }, body: `ClientSecret not found for '${providerName}' provider`, }); return; } - const clientPrincipal = await getAuthClientPrincipal(providerName, codeValue!, clientId, clientSecret, openIdIssuer!); + const clientPrincipal = + providerName === "github" + ? await getGitHubClientPrincipal(codeValue!, clientId, clientSecret) + : await getGoogleClientPrincipal(codeValue!, clientId, clientSecret); if (clientPrincipal !== null && customAuth?.rolesSource) { try { - const rolesResult = (await getRoles(clientPrincipal as RolesSourceFunctionRequestBody, customAuth.rolesSource)) as { roles: string[] }; - clientPrincipal?.userRoles.push(...rolesResult.roles); + const rolesResult = (await getRoles(clientPrincipal, customAuth.rolesSource)) as { roles: string[] }; + clientPrincipal.userRoles.push(...rolesResult.roles); } catch {} } diff --git a/src/msha/auth/routes/auth-login-provider-custom.ts b/src/msha/auth/routes/auth-login-provider-custom.ts index 5df1c564..e04b655a 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -1,39 +1,31 @@ import { IncomingMessage } from "node:http"; import { CookiesManager } from "../../../core/utils/cookie.js"; import { response } from "../../../core/utils/net.js"; -import { ENTRAID_FULL_NAME, SUPPORTED_CUSTOM_AUTH_PROVIDERS, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; +import { 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 = (providerName?: string) => { - if (providerName === ENTRAID_FULL_NAME) { - return "aad"; - } - return providerName?.toLowerCase() || ""; -}; - const httpTrigger = async function (context: Context, request: IncomingMessage, customAuth?: SWAConfigFileAuth) { await Promise.resolve(); - const providerName: string = normalizeAuthProvider(context.bindingData?.provider); + const providerName = context.bindingData?.provider?.toLowerCase() || ""; - if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { + if (providerName != "github" && providerName != "google") { context.res = response({ context, - status: 400, + status: 404, headers: { ["Content-Type"]: "text/plain" }, body: `Provider '${providerName}' not found`, }); return; } - const clientIdSettingName = - customAuth?.identityProviders?.[providerName == "aad" ? ENTRAID_FULL_NAME : providerName]?.registration?.clientIdSettingName; + const clientIdSettingName = customAuth?.identityProviders?.[providerName]?.registration?.clientIdSettingName; if (!clientIdSettingName) { context.res = response({ context, - status: 400, + status: 404, headers: { ["Content-Type"]: "text/plain" }, body: `ClientIdSettingName not found for '${providerName}' provider`, }); @@ -45,28 +37,13 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, if (!clientId) { context.res = response({ context, - status: 400, + status: 404, 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,20 +58,10 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, const hashedState = hashStateGuid(state); const redirectUri = `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}`; - let location; - switch (providerName) { - case "google": - 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=${clientId}&redirect_uri=${redirectUri}/.auth/login/github/callback&scope=read:user&state=${hashedState}`; - break; - case "aad": - 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; - } + const location = + providerName === "google" + ? `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}` + : `https://github.com/login/oauth/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/github/callback&scope=read:user&state=${hashedState}`; const cookiesManager = new CookiesManager(request.headers.cookie); if (!authContextCookie) { diff --git a/src/swa.d.ts b/src/swa.d.ts index 6e480b4a..052a6c86 100644 --- a/src/swa.d.ts +++ b/src/swa.d.ts @@ -95,7 +95,7 @@ declare interface Context { value: string; expires: string | Date; domaine: string; - }, + } ]; headers?: { [key: string]: string }; body?: { [key: string]: string } | string | null; @@ -290,32 +290,21 @@ declare type SWAConfigFileMimeTypes = { [key: string]: string; }; -declare type AuthIdentityTokenEndpoints = { - [key: string]: { - host: string; - path: string; - }; -}; - -declare type AuthIdentityIssHosts = { - [key: string]: string; -}; - declare type AuthIdentityProvider = { registration: { clientIdSettingName: string; clientSecretSettingName: string; - openIdIssuer?: string; }; }; declare type SWAConfigFileAuthIdenityProviders = { - [key: string]: AuthIdentityProvider; + github?: AuthIdentityProvider; + google?: AuthIdentityProvider; }; declare type SWAConfigFileAuth = { rolesSource?: string; - identityProviders?: SWAConfigFileAuthIdenityProviders; + identityProviders: SWAConfigFileAuthIdenityProviders; }; declare type SWAConfigFile = { From 13a60062a4ece514867fd5e460cfac28d0648121 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:47:49 -0500 Subject: [PATCH 8/9] Update SWA client metadata url to new aka.ms link --- src/core/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index edb062df..eb1c5a4e 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -11,7 +11,7 @@ const __dirname = path.dirname(__filename); export const DEPLOY_BINARY_NAME = "StaticSitesClient"; export const DEPLOY_BINARY_STABLE_TAG = "stable"; export const DEPLOY_FOLDER = path.join(os.homedir(), ".swa", "deploy"); -export const STATIC_SITE_CLIENT_RELEASE_METADATA_URL = "https://swalocaldeploy.azureedge.net/downloads/versions.json"; +export const STATIC_SITE_CLIENT_RELEASE_METADATA_URL = "https://aka.ms/swalocaldeploy"; // Data-api-builder related constants export const DATA_API_BUILDER_BINARY_NAME = "DataApiBuilder"; From 29ca3288339a0ff2922ca84eeedab090e49aee47 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Sun, 15 Dec 2024 06:49:43 -0500 Subject: [PATCH 9/9] 2.0.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 91f16b23..61433741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure/static-web-apps-cli", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@azure/static-web-apps-cli", - "version": "2.0.1", + "version": "2.0.2", "license": "MIT", "dependencies": { "@azure/arm-appservice": "^15.0.0", diff --git a/package.json b/package.json index 2be1e25a..ccc1fe80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/static-web-apps-cli", - "version": "2.0.1", + "version": "2.0.2", "description": "Azure Static Web Apps CLI", "type": "module", "scripts": {