UNPKG

@azure/static-web-apps-cli

Version:
342 lines 12.4 kB
import * as http from "node:http"; import * as https from "node:https"; import * as querystring from "node:querystring"; import { CookiesManager, decodeAuthContextCookie, validateAuthContextCookie } from "../../../core/utils/cookie.js"; import { parseUrl, response } from "../../../core/utils/net.js"; import { 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 { checkCustomAuthConfigFields, normalizeAuthProvider } from "./auth-login-provider-custom.js"; import { jwtDecode } from "jwt-decode"; const getAuthClientPrincipal = async function (authProvider, codeValue, authConfigs) { let authToken; try { const authTokenResponse = (await getOAuthToken(authProvider, codeValue, authConfigs)); 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"]; } else { authToken = authTokenParsed["id_token"]; } } catch (error) { console.error(`Error in getting OAuth token: ${error}`); return null; } if (!authToken) { return null; } try { const user = (await getOAuthUser(authProvider, authToken)); const userDetails = user["login"] || user["email"] || user?.data?.["username"]; const name = user["name"] || user?.data?.["name"]; const givenName = user["given_name"]; const familyName = user["family_name"]; const picture = user["picture"]; const userId = user["id"] || user?.data?.["id"]; const verifiedEmail = user["verified_email"]; const claims = [ { typ: "iss", val: CUSTOM_AUTH_ISS_MAPPING?.[authProvider], }, { typ: "azp", val: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName, }, { typ: "aud", val: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName, }, ]; 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) { claims.push({ typ: "email_verified", val: verifiedEmail, }); } if (authProvider === "github") { Object.keys(user).forEach((key) => { claims.push({ typ: `urn:github:${key}`, val: user[key], }); }); } return { identityProvider: authProvider, userDetails, claims, userRoles: ["authenticated", "anonymous"], }; } catch (error) { console.error(`Error while parsing user information: ${error}`); return null; } }; const getOAuthToken = function (authProvider, codeValue, authConfigs) { 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 = authConfigs?.openIdIssuer.split("/")[3]; } const queryString = { code: codeValue, 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 = { "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, }; 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 getOAuthUser = function (authProvider, accessToken) { // 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 = ""; 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, rolesSource) { let cliApiUri = SWA_CLI_API_URI(); const { protocol, hostname, port } = parseUrl(cliApiUri); const target = hostname === "localhost" ? `${protocol}//127.0.0.1:${port}` : cliApiUri; const targetUrl = new URL(target); const data = JSON.stringify(clientPrincipal); const options = { host: targetUrl.hostname, port: targetUrl.port, path: rolesSource, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data), }, }; return new Promise((resolve, reject) => { const protocolModule = targetUrl.protocol === "https:" ? https : http; const req = protocolModule.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.write(data); req.end(); }); }; const httpTrigger = async function (context, request, customAuth) { const providerName = normalizeAuthProvider(context.bindingData?.provider); if (!SUPPORTED_CUSTOM_AUTH_PROVIDERS.includes(providerName)) { context.res = response({ context, status: 400, headers: { ["Content-Type"]: "text/plain" }, body: `Provider '${providerName}' not found`, }); return; } const { cookie } = request.headers; if (!cookie || !validateAuthContextCookie(cookie)) { context.res = response({ context, status: 401, headers: { ["Content-Type"]: "text/plain" }, body: "Invalid login request", }); return; } const url = new URL(request.url, `${SWA_CLI_APP_PROTOCOL}://${request?.headers?.host}`); const codeValue = url.searchParams.get("code"); const stateValue = url.searchParams.get("state"); const authContext = decodeAuthContextCookie(cookie); if (!authContext?.authNonce || hashStateGuid(authContext.authNonce) !== stateValue) { context.res = response({ context, status: 401, headers: { ["Content-Type"]: "text/plain" }, body: "Invalid login request", }); return; } if (isNonceExpired(authContext.authNonce)) { context.res = response({ context, status: 401, headers: { ["Content-Type"]: "text/plain" }, body: "Login timed out. Please try again.", }); return; } const authConfigs = checkCustomAuthConfigFields(context, providerName, customAuth); if (!authConfigs) { return; } const clientPrincipal = await getAuthClientPrincipal(providerName, codeValue, authConfigs); if (clientPrincipal !== null && customAuth?.rolesSource) { try { const rolesResult = (await getRoles(clientPrincipal, customAuth.rolesSource)); clientPrincipal?.userRoles.push(...rolesResult.roles); } catch { } } const authCookieString = clientPrincipal && JSON.stringify(clientPrincipal); const authCookieEncrypted = authCookieString && encryptAndSign(authCookieString); const authCookie = authCookieEncrypted ? btoa(authCookieEncrypted) : undefined; const cookiesManager = new CookiesManager(request.headers.cookie); cookiesManager.addCookieToDelete("StaticWebAppsAuthContextCookie"); if (authCookie) { cookiesManager.addCookieToSet({ name: "StaticWebAppsAuthCookie", value: authCookie, domain: DEFAULT_CONFIG.host, path: "/", secure: true, httpOnly: true, expires: new Date(Date.now() + 1000 * 60 * 60 * 8).toUTCString(), }); } context.res = response({ context, cookies: cookiesManager.getCookies(), status: 302, headers: { status: 302, Location: authContext.postLoginRedirectUri ?? "/", }, body: "", }); }; export default httpTrigger; //# sourceMappingURL=auth-login-provider-callback.js.map