Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add Facebook Custom Auth Support #880

Merged
merged 3 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the comments in Line 15:
// only match for providers with custom auth support implemented (github, google, aad)

Should we update the supported provider type?

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
Loading