From 309304ccef50feb717f0f7713b4451bdffd85acc Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Wed, 7 Aug 2024 13:16:03 -0400 Subject: [PATCH 1/7] Add AAD custom authentication in MSHA emulator --- src/core/constants.ts | 1 + src/msha/auth/index.ts | 12 +- .../routes/auth-login-provider-callback.ts | 217 ++++++++++++++++-- .../auth/routes/auth-login-provider-custom.ts | 45 +++- src/swa.d.ts | 8 +- 5 files changed, 248 insertions(+), 35 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index edb062df..87b50bad 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -48,6 +48,7 @@ export const SWA_RUNTIME_CONFIG_MAX_SIZE_IN_KB = 20; // 20kb export const SWA_AUTH_CONTEXT_COOKIE = `StaticWebAppsAuthContextCookie`; export const SWA_AUTH_COOKIE = `StaticWebAppsAuthCookie`; export const ALLOWED_HTTP_METHODS_FOR_STATIC_CONTENT = ["GET", "HEAD", "OPTIONS"]; +export const SUPPORTED_CUSTOM_AUTH_PROVIDERS = ["google", "github", "azureActiveDirectory"]; export const AUTH_STATUS = { NoAuth: 0, diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index ddc0c812..775ca66d 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -9,20 +9,20 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { if (isCustomAuth) { paths.push({ method: "GET", - // only match for providers with custom auth support implemented (github, google) - route: /^\/\.auth\/login\/(?github|google|dummy)\/callback(\?.*)?$/i, + // only match for providers with custom auth support implemented (github, google, aad) + route: /^\/\.auth\/login\/(?github|google|azureActiveDirectory|dummy)\/callback(\?.*)?$/i, function: "auth-login-provider-callback", }); paths.push({ method: "GET", - // only match for providers with custom auth support implemented (github, google) - route: /^\/\.auth\/login\/(?github|google|dummy)(\?.*)?$/i, + // only match for providers with custom auth support implemented (github, google, aad) + route: /^\/\.auth\/login\/(?github|google|azureActiveDirectory|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\/(?aad|twitter|facebook|[a-z]+)(\?.*)?$/i, + route: /^\/\.auth\/login\/(?twitter|facebook|[a-z]+)(\?.*)?$/i, function: "auth-login-provider", }); paths.push({ @@ -33,7 +33,7 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { } else { paths.push({ method: "GET", - route: /^\/\.auth\/login\/(?aad|github|twitter|google|facebook|[a-z]+)(\?.*)?$/i, + route: /^\/\.auth\/login\/(?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 41ddc4cf..4802b459 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -4,7 +4,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 { SWA_CLI_API_URI, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; +import { SUPPORTED_CUSTOM_AUTH_PROVIDERS, 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"; @@ -313,6 +313,173 @@ const getGoogleClientPrincipal = async function (codeValue: string, clientId: st } }; +const getAADClientPrincipal = async function (codeValue: string, clientId: string, clientSecret: string, openIdIssuer: string) { + let authToken: string; + + try { + const authTokenResponse = (await getAADAuthToken(codeValue!, clientId, clientSecret, openIdIssuer)) as string; + const authTokenParsed = JSON.parse(authTokenResponse); + authToken = authTokenParsed["access_token"] as string; + } catch { + return null; + } + + if (!authToken) { + return null; + } + + try { + const user = (await getAADUser(authToken)) as { [key: string]: string }; + + const userDetails = user["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://graph.microsoft.com", + }, + { + 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, + }); + } + + return { + identityProvider: "azureActiveDirectory", + userDetails, + claims, + userRoles: ["authenticated", "anonymous"], + }; + } catch { + return null; + } +}; + +const getAADAuthToken = function (codeValue: string, clientId: string, clientSecret: string, openIdIssuer: string) { + const redirectUri = `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}`; + const tenantId = openIdIssuer.split("/")[3]; + + const data = querystring.stringify({ + code: codeValue, + client_id: clientId, + client_secret: clientSecret, + grant_type: "authorization_code", + redirect_uri: `${redirectUri}/.auth/login/azureActiveDirectory/callback`, + }); + + const options = { + host: `login.microsoft.com`, + path: `/${tenantId}/oauth2/v2.0/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) => { + reject(err); + }); + + req.write(data); + req.end(); + }); +}; + +const getAADUser = function (accessToken: string) { + const options = { + host: "graph.microsoft.com", + path: "/oidc/userinfo", + 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 getRoles = function (clientPrincipal: RolesSourceFunctionRequestBody, rolesSource: string) { let cliApiUri = SWA_CLI_API_URI(); const { protocol, hostname, port } = parseUrl(cliApiUri); @@ -362,12 +529,12 @@ const getRoles = function (clientPrincipal: RolesSourceFunctionRequestBody, role }; const httpTrigger = async function (context: Context, request: http.IncomingMessage, customAuth?: SWAConfigFileAuth) { - const providerName = context.bindingData?.provider?.toLowerCase() || ""; + const providerName = context.bindingData?.provider || ""; - if (providerName != "github" && providerName != "google") { + if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { context.res = response({ context, - status: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `Provider '${providerName}' not found`, }); @@ -413,12 +580,12 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess return; } - const { clientIdSettingName, clientSecretSettingName } = customAuth?.identityProviders?.[providerName]?.registration || {}; + const { clientIdSettingName, clientSecretSettingName, openIdIssuer } = customAuth?.identityProviders?.[providerName]?.registration || {}; if (!clientIdSettingName) { context.res = response({ context, - status: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `ClientIdSettingName not found for '${providerName}' provider`, }); @@ -428,19 +595,29 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess if (!clientSecretSettingName) { context.res = response({ context, - status: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `ClientSecretSettingName not found for '${providerName}' provider`, }); return; } + if (providerName == "azureActiveDirectory" && !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: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `ClientId not found for '${providerName}' provider`, }); @@ -452,22 +629,32 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess if (!clientSecret) { context.res = response({ context, - status: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `ClientSecret not found for '${providerName}' provider`, }); return; } - const clientPrincipal = - providerName === "github" - ? await getGitHubClientPrincipal(codeValue!, clientId, clientSecret) - : await getGoogleClientPrincipal(codeValue!, clientId, clientSecret); + let clientPrincipal; + switch (providerName) { + case "github": + clientPrincipal = await getGitHubClientPrincipal(codeValue!, clientId, clientSecret); + break; + case "google": + clientPrincipal = await getGoogleClientPrincipal(codeValue!, clientId, clientSecret); + break; + case "azureActiveDirectory": + clientPrincipal = await getAADClientPrincipal(codeValue!, clientId, clientSecret, openIdIssuer!); + break; + default: + break; + } if (clientPrincipal !== null && customAuth?.rolesSource) { try { - const rolesResult = (await getRoles(clientPrincipal, customAuth.rolesSource)) as { roles: string[] }; - clientPrincipal.userRoles.push(...rolesResult.roles); + const rolesResult = (await getRoles(clientPrincipal as RolesSourceFunctionRequestBody, 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 e04b655a..37251974 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -1,19 +1,19 @@ import { IncomingMessage } from "node:http"; import { CookiesManager } from "../../../core/utils/cookie.js"; import { response } from "../../../core/utils/net.js"; -import { SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; +import { 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"; const httpTrigger = async function (context: Context, request: IncomingMessage, customAuth?: SWAConfigFileAuth) { await Promise.resolve(); - const providerName = context.bindingData?.provider?.toLowerCase() || ""; + const providerName: string = context.bindingData?.provider || ""; - if (providerName != "github" && providerName != "google") { + if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { context.res = response({ context, - status: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `Provider '${providerName}' not found`, }); @@ -25,7 +25,7 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, if (!clientIdSettingName) { context.res = response({ context, - status: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `ClientIdSettingName not found for '${providerName}' provider`, }); @@ -37,13 +37,28 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, if (!clientId) { context.res = response({ context, - status: 404, + status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `ClientId not found for '${providerName}' provider`, }); return; } + let aadIssuer; + if (providerName == "azureActiveDirectory") { + aadIssuer = customAuth?.identityProviders?.[providerName]?.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 = { @@ -58,10 +73,20 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, const hashedState = hashStateGuid(state); const redirectUri = `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}`; - 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}`; + 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 "azureActiveDirectory": + location = `${aadIssuer}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/azureActiveDirectory/callback&scope=openid&state=${hashedState}`; + break; + default: + break; + } const cookiesManager = new CookiesManager(request.headers.cookie); if (!authContextCookie) { diff --git a/src/swa.d.ts b/src/swa.d.ts index 052a6c86..139d8dba 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; @@ -294,17 +294,17 @@ declare type AuthIdentityProvider = { registration: { clientIdSettingName: string; clientSecretSettingName: string; + openIdIssuer?: string; }; }; declare type SWAConfigFileAuthIdenityProviders = { - github?: AuthIdentityProvider; - google?: AuthIdentityProvider; + [key: string]: AuthIdentityProvider; }; declare type SWAConfigFileAuth = { rolesSource?: string; - identityProviders: SWAConfigFileAuthIdenityProviders; + identityProviders?: SWAConfigFileAuthIdenityProviders; }; declare type SWAConfigFile = { From c2016f6b8ca4bc48fa5986f1c70b51f868a79fec Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Wed, 7 Aug 2024 13:28:18 -0400 Subject: [PATCH 2/7] Change AAD auth routes to use aad in the URL --- src/core/constants.ts | 4 +++- src/msha/auth/index.ts | 4 ++-- .../routes/auth-login-provider-callback.ts | 14 +++++++----- .../auth/routes/auth-login-provider-custom.ts | 22 +++++++++++++------ 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index 87b50bad..d4f4dda4 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -48,7 +48,9 @@ export const SWA_RUNTIME_CONFIG_MAX_SIZE_IN_KB = 20; // 20kb export const SWA_AUTH_CONTEXT_COOKIE = `StaticWebAppsAuthContextCookie`; export const SWA_AUTH_COOKIE = `StaticWebAppsAuthCookie`; export const ALLOWED_HTTP_METHODS_FOR_STATIC_CONTENT = ["GET", "HEAD", "OPTIONS"]; -export const SUPPORTED_CUSTOM_AUTH_PROVIDERS = ["google", "github", "azureActiveDirectory"]; +export const SUPPORTED_CUSTOM_AUTH_PROVIDERS = ["google", "github", "aad"]; +// Full name is required in staticwebapp.config.json's schema so we will normalize it to aad +export const AAD_FULL_NAME = "azureActiveDirectory"; export const AUTH_STATUS = { NoAuth: 0, diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index 775ca66d..bb660c62 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -10,13 +10,13 @@ function getAuthPaths(isCustomAuth: boolean): Path[] { paths.push({ method: "GET", // only match for providers with custom auth support implemented (github, google, aad) - route: /^\/\.auth\/login\/(?github|google|azureActiveDirectory|dummy)\/callback(\?.*)?$/i, + route: /^\/\.auth\/login\/(?github|google|aad|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: /^\/\.auth\/login\/(?github|google|azureActiveDirectory|dummy)(\?.*)?$/i, + route: /^\/\.auth\/login\/(?github|google|aad|dummy)(\?.*)?$/i, function: "auth-login-provider-custom", }); 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 4802b459..81ca794f 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -4,9 +4,10 @@ import * as querystring from "node:querystring"; import { CookiesManager, decodeAuthContextCookie, validateAuthContextCookie } from "../../../core/utils/cookie.js"; import { parseUrl, response } from "../../../core/utils/net.js"; -import { SUPPORTED_CUSTOM_AUTH_PROVIDERS, SWA_CLI_API_URI, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; +import { AAD_FULL_NAME, SUPPORTED_CUSTOM_AUTH_PROVIDERS, 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 getGithubAuthToken = function (codeValue: string, clientId: string, clientSecret: string) { const data = querystring.stringify({ @@ -407,7 +408,7 @@ const getAADAuthToken = function (codeValue: string, clientId: string, clientSec client_id: clientId, client_secret: clientSecret, grant_type: "authorization_code", - redirect_uri: `${redirectUri}/.auth/login/azureActiveDirectory/callback`, + redirect_uri: `${redirectUri}/.auth/login/aad/callback`, }); const options = { @@ -529,7 +530,7 @@ const getRoles = function (clientPrincipal: RolesSourceFunctionRequestBody, role }; const httpTrigger = async function (context: Context, request: http.IncomingMessage, customAuth?: SWAConfigFileAuth) { - const providerName = context.bindingData?.provider || ""; + const providerName = normalizeAuthProvider(context.bindingData?.provider); if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { context.res = response({ @@ -580,7 +581,8 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess return; } - const { clientIdSettingName, clientSecretSettingName, openIdIssuer } = customAuth?.identityProviders?.[providerName]?.registration || {}; + const { clientIdSettingName, clientSecretSettingName, openIdIssuer } = + customAuth?.identityProviders?.[providerName == "aad" ? AAD_FULL_NAME : providerName]?.registration || {}; if (!clientIdSettingName) { context.res = response({ @@ -602,7 +604,7 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess return; } - if (providerName == "azureActiveDirectory" && !openIdIssuer) { + if (providerName == "aad" && !openIdIssuer) { context.res = response({ context, status: 400, @@ -644,7 +646,7 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess case "google": clientPrincipal = await getGoogleClientPrincipal(codeValue!, clientId, clientSecret); break; - case "azureActiveDirectory": + case "aad": clientPrincipal = await getAADClientPrincipal(codeValue!, clientId, clientSecret, openIdIssuer!); break; default: diff --git a/src/msha/auth/routes/auth-login-provider-custom.ts b/src/msha/auth/routes/auth-login-provider-custom.ts index 37251974..517af0d9 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -1,14 +1,21 @@ import { IncomingMessage } from "node:http"; import { CookiesManager } from "../../../core/utils/cookie.js"; import { response } from "../../../core/utils/net.js"; -import { SUPPORTED_CUSTOM_AUTH_PROVIDERS, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; +import { AAD_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 = (providerName?: string) => { + if (providerName === AAD_FULL_NAME) { + return "aad"; + } + return providerName?.toLowerCase() || ""; +}; + const httpTrigger = async function (context: Context, request: IncomingMessage, customAuth?: SWAConfigFileAuth) { await Promise.resolve(); - const providerName: string = context.bindingData?.provider || ""; + const providerName: string = normalizeAuthProvider(context.bindingData?.provider); if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { context.res = response({ @@ -20,7 +27,8 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, return; } - const clientIdSettingName = customAuth?.identityProviders?.[providerName]?.registration?.clientIdSettingName; + const clientIdSettingName = + customAuth?.identityProviders?.[providerName == "aad" ? AAD_FULL_NAME : providerName]?.registration?.clientIdSettingName; if (!clientIdSettingName) { context.res = response({ @@ -45,8 +53,8 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, } let aadIssuer; - if (providerName == "azureActiveDirectory") { - aadIssuer = customAuth?.identityProviders?.[providerName]?.registration?.openIdIssuer; + if (providerName == "aad") { + aadIssuer = customAuth?.identityProviders?.[AAD_FULL_NAME]?.registration?.openIdIssuer; if (!aadIssuer) { context.res = response({ @@ -81,8 +89,8 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, 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 "azureActiveDirectory": - location = `${aadIssuer}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/azureActiveDirectory/callback&scope=openid&state=${hashedState}`; + case "aad": + location = `${aadIssuer}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/aad/callback&scope=openid&state=${hashedState}`; break; default: break; From 8386aab9c6b5dcae4eeefbc196f1d5614187db90 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Wed, 7 Aug 2024 13:52:14 -0400 Subject: [PATCH 3/7] Refactor custom auth files to generic functions for all auth providers --- src/core/constants.ts | 35 ++ .../routes/auth-login-provider-callback.ts | 380 +++--------------- src/swa.d.ts | 11 + 3 files changed, 103 insertions(+), 323 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index d4f4dda4..15e55f7e 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -48,9 +48,44 @@ export const SWA_RUNTIME_CONFIG_MAX_SIZE_IN_KB = 20; // 20kb 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"]; // Full name is required in staticwebapp.config.json's schema so we will normalize it to aad export const AAD_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.microsoft.com", + path: "/tenantId/oauth/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, diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index 81ca794f..a57e492c 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -4,219 +4,30 @@ import * as querystring from "node:querystring"; import { CookiesManager, decodeAuthContextCookie, validateAuthContextCookie } from "../../../core/utils/cookie.js"; import { parseUrl, response } from "../../../core/utils/net.js"; -import { AAD_FULL_NAME, SUPPORTED_CUSTOM_AUTH_PROVIDERS, SWA_CLI_API_URI, SWA_CLI_APP_PROTOCOL } from "../../../core/constants.js"; +import { + AAD_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 { DEFAULT_CONFIG } from "../../../config.js"; import { encryptAndSign, hashStateGuid, isNonceExpired } from "../../../core/utils/auth.js"; import { normalizeAuthProvider } from "./auth-login-provider-custom.js"; -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 getGithubAuthToken(codeValue, clientId, clientSecret)) as string; - const authTokenParsed = querystring.parse(authTokenResponse); - authToken = authTokenParsed["access_token"] as string; - } catch { - return null; - } - - if (!authToken) { - return null; - } - - try { - const user = (await getGitHubUser(authToken)) as { [key: string]: string }; - - const userId = user["id"]; - const userDetails = user["login"]; - - const claims: { typ: string; val: string }[] = [ - { - typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", - val: userId, - }, - ]; - - Object.keys(user).forEach((key) => { - claims.push({ - typ: `urn:github:${key}`, - val: user[key], - }); - }); - - return { - identityProvider: "github", - userId, - userDetails, - userRoles: ["authenticated", "anonymous"], - claims, - }; - } catch { - return null; - } -}; - -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: `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}/.auth/login/google/callback`, - }); - - const options = { - host: "oauth2.googleapis.com", - path: "/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) => { - reject(err); - }); - - req.write(data); - req.end(); - }); -}; - -const getGoogleUser = function (accessToken: string) { - const options = { - host: "www.googleapis.com", - path: "/oauth2/v2/userinfo", - 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 getGoogleClientPrincipal = async function (codeValue: string, clientId: string, clientSecret: string) { +const getAuthClientPrincipal = async function ( + authProvider: string, + codeValue: string, + clientId: string, + clientSecret: string, + openIdIssuer: string = "", +) { let authToken: string; try { - const authTokenResponse = (await getGoogleAuthToken(codeValue!, clientId, clientSecret)) as string; + const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, clientId, clientSecret, openIdIssuer)) as string; const authTokenParsed = JSON.parse(authTokenResponse); authToken = authTokenParsed["access_token"] as string; } catch { @@ -228,20 +39,20 @@ const getGoogleClientPrincipal = async function (codeValue: string, clientId: st } try { - const user = (await getGoogleUser(authToken)) as { [key: string]: string }; + const user = (await getOAuthUser(authProvider, authToken)) as { [key: string]: string }; - const userId = user["id"]; - const userDetails = user["email"]; - const verifiedEmail = user["verified_email"]; + 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 claims: { typ: string; val: string }[] = [ { typ: "iss", - val: "https://accounts.google.com", + val: CUSTOM_AUTH_ISS_MAPPING?.[authProvider], }, { typ: "azp", @@ -253,13 +64,6 @@ const getGoogleClientPrincipal = async function (codeValue: string, clientId: st }, ]; - 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", @@ -267,13 +71,6 @@ const getGoogleClientPrincipal = async function (codeValue: string, clientId: st }); } - if (verifiedEmail !== undefined) { - claims.push({ - typ: "email_verified", - val: verifiedEmail, - }); - } - if (name) { claims.push({ typ: "name", @@ -302,94 +99,31 @@ const getGoogleClientPrincipal = async function (codeValue: string, clientId: st }); } - return { - identityProvider: "google", - userId, - userDetails, - claims, - userRoles: ["authenticated", "anonymous"], - }; - } catch { - return null; - } -}; - -const getAADClientPrincipal = async function (codeValue: string, clientId: string, clientSecret: string, openIdIssuer: string) { - let authToken: string; - - try { - const authTokenResponse = (await getAADAuthToken(codeValue!, clientId, clientSecret, openIdIssuer)) as string; - const authTokenParsed = JSON.parse(authTokenResponse); - authToken = authTokenParsed["access_token"] as string; - } catch { - return null; - } - - if (!authToken) { - return null; - } - - try { - const user = (await getAADUser(authToken)) as { [key: string]: string }; - - const userDetails = user["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://graph.microsoft.com", - }, - { - 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) { + if (userId) { claims.push({ - typ: "picture", - val: picture, + typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", + val: userId, }); } - if (givenName) { + if (verifiedEmail) { claims.push({ - typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", - val: givenName, + typ: "email_verified", + val: verifiedEmail, }); } - if (familyName) { - claims.push({ - typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", - val: familyName, + if (authProvider === "github") { + Object.keys(user).forEach((key) => { + claims.push({ + typ: `urn:github:${key}`, + val: user[key], + }); }); } return { - identityProvider: "azureActiveDirectory", + identityProvider: authProvider, userDetails, claims, userRoles: ["authenticated", "anonymous"], @@ -399,20 +133,33 @@ const getAADClientPrincipal = async function (codeValue: string, clientId: strin } }; -const getAADAuthToken = function (codeValue: string, clientId: string, clientSecret: string, openIdIssuer: string) { +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}`; - const tenantId = openIdIssuer.split("/")[3]; + let tenantId; + + if (!Object.keys(CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING).includes(authProvider)) { + return null; + } + + if (authProvider === "aad") { + tenantId = openIdIssuer.split("/")[3]; + } const data = querystring.stringify({ code: codeValue, client_id: clientId, client_secret: clientSecret, - grant_type: "authorization_code", - redirect_uri: `${redirectUri}/.auth/login/aad/callback`, + grant_type: authProvider !== "github" && "authorization_code", + redirect_uri: authProvider !== "github" && `${redirectUri}/.auth/login/${authProvider}/callback`, }); + let tokenPath = CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.path; + if (authProvider === "aad" && tenantId !== undefined) { + tokenPath = tokenPath.replace("tenantId", tenantId); + } + const options = { - host: `login.microsoft.com`, + host: CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.host, path: `/${tenantId}/oauth2/v2.0/token`, method: "POST", headers: { @@ -444,10 +191,10 @@ const getAADAuthToken = function (codeValue: string, clientId: string, clientSec }); }; -const getAADUser = function (accessToken: string) { +const getOAuthUser = function (authProvider: string, accessToken: string) { const options = { - host: "graph.microsoft.com", - path: "/oidc/userinfo", + host: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.host, + path: CUSTOM_AUTH_USER_ENDPOINT_MAPPING?.[authProvider]?.path, method: "GET", headers: { Authorization: `Bearer ${accessToken}`, @@ -638,20 +385,7 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess return; } - let clientPrincipal; - switch (providerName) { - case "github": - clientPrincipal = await getGitHubClientPrincipal(codeValue!, clientId, clientSecret); - break; - case "google": - clientPrincipal = await getGoogleClientPrincipal(codeValue!, clientId, clientSecret); - break; - case "aad": - clientPrincipal = await getAADClientPrincipal(codeValue!, clientId, clientSecret, openIdIssuer!); - break; - default: - break; - } + const clientPrincipal = await getAuthClientPrincipal(providerName, codeValue!, clientId, clientSecret, openIdIssuer!); if (clientPrincipal !== null && customAuth?.rolesSource) { try { diff --git a/src/swa.d.ts b/src/swa.d.ts index 139d8dba..6e480b4a 100644 --- a/src/swa.d.ts +++ b/src/swa.d.ts @@ -290,6 +290,17 @@ 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; From 9fc685d53fc03033bd1559b05671d08a403329b8 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Wed, 7 Aug 2024 14:08:02 -0400 Subject: [PATCH 4/7] Fix missed changes --- src/msha/auth/routes/auth-login-provider-callback.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index a57e492c..685e8065 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -30,7 +30,8 @@ const getAuthClientPrincipal = async function ( const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, clientId, clientSecret, openIdIssuer)) as string; const authTokenParsed = JSON.parse(authTokenResponse); authToken = authTokenParsed["access_token"] as string; - } catch { + } catch (error) { + console.error(`Error in getting OAuth token: ${error}`); return null; } @@ -149,8 +150,8 @@ const getOAuthToken = function (authProvider: string, codeValue: string, clientI code: codeValue, client_id: clientId, client_secret: clientSecret, - grant_type: authProvider !== "github" && "authorization_code", - redirect_uri: authProvider !== "github" && `${redirectUri}/.auth/login/${authProvider}/callback`, + ...(authProvider !== "github" && { grant_type: authProvider }), + ...(authProvider !== "github" && { redirect_uri: `${redirectUri}/.auth/login/${authProvider}/callback` }), }); let tokenPath = CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.path; @@ -160,7 +161,7 @@ const getOAuthToken = function (authProvider: string, codeValue: string, clientI const options = { host: CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.host, - path: `/${tenantId}/oauth2/v2.0/token`, + path: tokenPath, method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", From 5feccd64b8433c33b8dcd1e9da242badabd27730 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Wed, 7 Aug 2024 14:29:40 -0400 Subject: [PATCH 5/7] Fix typos --- src/core/constants.ts | 4 ++-- src/msha/auth/routes/auth-login-provider-callback.ts | 4 ++-- src/msha/auth/routes/auth-login-provider-custom.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index 15e55f7e..d921d2fa 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -63,8 +63,8 @@ export const CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { path: "/login/oauth/access_token", }, aad: { - host: "login.microsoft.com", - path: "/tenantId/oauth/v2.0/token", + host: "login.microsoftonline.com", + path: "/tenantId/oauth2/v2.0/token", }, }; export const CUSTOM_AUTH_USER_ENDPOINT_MAPPING: AuthIdentityTokenEndpoints = { diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index 685e8065..aaa98b6b 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -150,8 +150,8 @@ const getOAuthToken = function (authProvider: string, codeValue: string, clientI code: codeValue, client_id: clientId, client_secret: clientSecret, - ...(authProvider !== "github" && { grant_type: authProvider }), - ...(authProvider !== "github" && { redirect_uri: `${redirectUri}/.auth/login/${authProvider}/callback` }), + grant_type: "authorization_code", + redirect_uri: `${redirectUri}/.auth/login/${authProvider}/callback`, }); let tokenPath = CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING?.[authProvider]?.path; diff --git a/src/msha/auth/routes/auth-login-provider-custom.ts b/src/msha/auth/routes/auth-login-provider-custom.ts index 517af0d9..00051392 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -90,7 +90,7 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, 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&state=${hashedState}`; + 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; From 3b1a557fda8a92fef7e15c58f9617ba161f3c6d9 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Wed, 7 Aug 2024 15:50:33 -0400 Subject: [PATCH 6/7] Fix typos --- src/msha/auth/routes/auth-login-provider-callback.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index aaa98b6b..839a5bf5 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -28,7 +28,12 @@ const getAuthClientPrincipal = async function ( try { const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, clientId, clientSecret, openIdIssuer)) as string; - const authTokenParsed = JSON.parse(authTokenResponse); + let authTokenParsed; + try { + authTokenParsed = JSON.parse(authTokenResponse); + } catch (e) { + authTokenParsed = querystring.parse(authTokenResponse); + } authToken = authTokenParsed["access_token"] as string; } catch (error) { console.error(`Error in getting OAuth token: ${error}`); From 6c2ca6ceb969a422a30f08b7131e3e4f9d7fb970 Mon Sep 17 00:00:00 2001 From: Timothy Wang Date: Thu, 8 Aug 2024 10:10:36 -0400 Subject: [PATCH 7/7] Address PR comments. Change AAD variable to ENTRA_ID. Change route matching to use supported auth constant list. --- src/core/constants.ts | 9 ++++++--- src/msha/auth/index.ts | 7 +++++-- src/msha/auth/routes/auth-login-provider-callback.ts | 4 ++-- src/msha/auth/routes/auth-login-provider-custom.ts | 8 ++++---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index d921d2fa..a8dd52f1 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -50,9 +50,12 @@ 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"]; -// Full name is required in staticwebapp.config.json's schema so we will normalize it to aad -export const AAD_FULL_NAME = "azureActiveDirectory"; +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", diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index bb660c62..ac129559 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -2,21 +2,24 @@ 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: /^\/\.auth\/login\/(?github|google|aad|dummy)\/callback(\?.*)?$/i, + 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) - route: /^\/\.auth\/login\/(?github|google|aad|dummy)(\?.*)?$/i, + route: new RegExp(`^/\\.auth/login/(?${supportedAuthsRegex})(\\?.*)?$`, "i"), function: "auth-login-provider-custom", }); 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 839a5bf5..d8e2d47b 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -5,7 +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 { - AAD_FULL_NAME, + ENTRAID_FULL_NAME, CUSTOM_AUTH_ISS_MAPPING, CUSTOM_AUTH_TOKEN_ENDPOINT_MAPPING, CUSTOM_AUTH_USER_ENDPOINT_MAPPING, @@ -335,7 +335,7 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess } const { clientIdSettingName, clientSecretSettingName, openIdIssuer } = - customAuth?.identityProviders?.[providerName == "aad" ? AAD_FULL_NAME : providerName]?.registration || {}; + customAuth?.identityProviders?.[providerName == "aad" ? ENTRAID_FULL_NAME : providerName]?.registration || {}; if (!clientIdSettingName) { context.res = response({ diff --git a/src/msha/auth/routes/auth-login-provider-custom.ts b/src/msha/auth/routes/auth-login-provider-custom.ts index 00051392..5df1c564 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -1,12 +1,12 @@ import { IncomingMessage } from "node:http"; import { CookiesManager } from "../../../core/utils/cookie.js"; import { response } from "../../../core/utils/net.js"; -import { AAD_FULL_NAME, SUPPORTED_CUSTOM_AUTH_PROVIDERS, 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 = (providerName?: string) => { - if (providerName === AAD_FULL_NAME) { + if (providerName === ENTRAID_FULL_NAME) { return "aad"; } return providerName?.toLowerCase() || ""; @@ -28,7 +28,7 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, } const clientIdSettingName = - customAuth?.identityProviders?.[providerName == "aad" ? AAD_FULL_NAME : providerName]?.registration?.clientIdSettingName; + customAuth?.identityProviders?.[providerName == "aad" ? ENTRAID_FULL_NAME : providerName]?.registration?.clientIdSettingName; if (!clientIdSettingName) { context.res = response({ @@ -54,7 +54,7 @@ const httpTrigger = async function (context: Context, request: IncomingMessage, let aadIssuer; if (providerName == "aad") { - aadIssuer = customAuth?.identityProviders?.[AAD_FULL_NAME]?.registration?.openIdIssuer; + aadIssuer = customAuth?.identityProviders?.[ENTRAID_FULL_NAME]?.registration?.openIdIssuer; if (!aadIssuer) { context.res = response({