Skip to content

Commit

Permalink
Merge pull request #880 from Timothyw0/main
Browse files Browse the repository at this point in the history
[Feature] Add Facebook Custom Auth Support
  • Loading branch information
Timothyw0 authored Sep 19, 2024
2 parents b298821 + 5373c5f commit 5f440e3
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 161 deletions.
30 changes: 22 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"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",
Expand Down
13 changes: 12 additions & 1 deletion src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", "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
Expand All @@ -69,6 +69,10 @@ 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: {
Expand All @@ -88,6 +92,13 @@ 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 = {
Expand Down
2 changes: 1 addition & 1 deletion src/msha/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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\/(?<provider>twitter|facebook|[a-z]+)(\?.*)?$/i,
route: /^\/\.auth\/login\/(?<provider>twitter|[a-z]+)(\?.*)?$/i,
function: "auth-login-provider",
});
paths.push({
Expand Down
153 changes: 53 additions & 100 deletions src/msha/auth/routes/auth-login-provider-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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,
Expand All @@ -15,26 +14,27 @@ import {
} 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 = "",
) {
import { checkCustomAuthConfigFields, normalizeAuthProvider } from "./auth-login-provider-custom.js";
import { jwtDecode } from "jwt-decode";

const getAuthClientPrincipal = async function (authProvider: string, codeValue: string, authConfigs: Record<string, string>) {
let authToken: string;

try {
const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, clientId, clientSecret, openIdIssuer)) as string;
const authTokenResponse = (await getOAuthToken(authProvider, codeValue!, authConfigs)) as string;
let authTokenParsed;
try {
authTokenParsed = JSON.parse(authTokenResponse);
} catch (e) {
authTokenParsed = querystring.parse(authTokenResponse);
}
authToken = authTokenParsed["access_token"] as string;

// 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;
}
} catch (error) {
console.error(`Error in getting OAuth token: ${error}`);
return null;
Expand Down Expand Up @@ -62,11 +62,11 @@ const getAuthClientPrincipal = async function (
},
{
typ: "azp",
val: clientId,
val: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName,
},
{
typ: "aud",
val: clientId,
val: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName,
},
];

Expand Down Expand Up @@ -139,7 +139,7 @@ const getAuthClientPrincipal = async function (
}
};

const getOAuthToken = function (authProvider: string, codeValue: string, clientId: string, clientSecret: string, openIdIssuer: string = "") {
const getOAuthToken = function (authProvider: string, codeValue: string, authConfigs: Record<string, string>) {
const redirectUri = `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}`;
let tenantId;

Expand All @@ -148,13 +148,13 @@ const getOAuthToken = function (authProvider: string, codeValue: string, clientI
}

if (authProvider === "aad") {
tenantId = openIdIssuer.split("/")[3];
tenantId = authConfigs?.openIdIssuer.split("/")[3];
}

const data = querystring.stringify({
code: codeValue,
client_id: clientId,
client_secret: clientSecret,
client_id: authConfigs?.clientIdSettingName || authConfigs?.appIdSettingName,
client_secret: authConfigs?.clientSecretSettingName || authConfigs?.appSecretSettingName,
grant_type: "authorization_code",
redirect_uri: `${redirectUri}/.auth/login/${authProvider}/callback`,
});
Expand Down Expand Up @@ -198,40 +198,45 @@ const getOAuthToken = function (authProvider: string, codeValue: string, clientI
};

const getOAuthUser = function (authProvider: string, accessToken: string) {
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",
},
};
// 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 = "";
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
res.setEncoding("utf8");
let responseBody = "";

res.on("data", (chunk) => {
responseBody += chunk;
res.on("data", (chunk) => {
responseBody += chunk;
});

res.on("end", () => {
try {
resolve(JSON.parse(responseBody));
} catch (err) {
reject(err);
}
});
});

res.on("end", () => {
try {
resolve(JSON.parse(responseBody));
} catch (err) {
reject(err);
}
req.on("error", (err) => {
reject(err);
});
});

req.on("error", (err) => {
reject(err);
req.end();
});

req.end();
});
}
};

const getRoles = function (clientPrincipal: RolesSourceFunctionRequestBody, rolesSource: string) {
Expand Down Expand Up @@ -334,64 +339,12 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess
return;
}

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`,
});
const authConfigs = checkCustomAuthConfigFields(context, providerName, customAuth);
if (!authConfigs) {
return;
}

const clientPrincipal = await getAuthClientPrincipal(providerName, codeValue!, clientId, clientSecret, openIdIssuer!);
const clientPrincipal = await getAuthClientPrincipal(providerName, codeValue!, authConfigs);

if (clientPrincipal !== null && customAuth?.rolesSource) {
try {
Expand Down
Loading

0 comments on commit 5f440e3

Please sign in to comment.