Skip to content

Commit

Permalink
Merge pull request #889 from Timothyw0/main
Browse files Browse the repository at this point in the history
[Feature] Add Twitter/X Custom Auth Support
  • Loading branch information
Timothyw0 authored Oct 14, 2024
2 parents 5f440e3 + 738b21e commit 6745391
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/azuresdkdrop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- run: npm pack

- name: Upload
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: package
path: "*.tgz"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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@v2
uses: actions/upload-artifact@v4
with:
name: static-web-apps-cli
path: "*.tgz"
12 changes: 11 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", "facebook", "dummy"];
export const SUPPORTED_CUSTOM_AUTH_PROVIDERS = ["google", "github", "aad", "facebook", "twitter", "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 @@ -73,6 +73,10 @@ 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: {
Expand All @@ -87,18 +91,24 @@ 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 = {
Expand Down
4 changes: 2 additions & 2 deletions src/msha/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// only match for providers with custom auth support implemented (github, google, aad, facebook, twitter)
route: new RegExp(`^/\\.auth/login/(?<provider>${supportedAuthsRegex})/callback(\\?.*)?$`, "i"),
function: "auth-login-provider-callback",
});
paths.push({
method: "GET",
// only match for providers with custom auth support implemented (github, google, aad)
// only match for providers with custom auth support implemented (github, google, aad, facebook, twitter)
route: new RegExp(`^/\\.auth/login/(?<provider>${supportedAuthsRegex})(\\?.*)?$`, "i"),
function: "auth-login-provider-custom",
});
Expand Down
42 changes: 29 additions & 13 deletions src/msha/auth/routes/auth-login-provider-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ const getAuthClientPrincipal = async function (authProvider: string, codeValue:
}

try {
const user = (await getOAuthUser(authProvider, authToken)) as { [key: string]: string };
const user = (await getOAuthUser(authProvider, authToken)) as Record<string, any>;

const userDetails = user["login"] || user["email"];
const name = user["name"];
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"];
const userId = user["id"] || user?.data?.["id"];
const verifiedEmail = user["verified_email"];

const claims: { typ: string; val: string }[] = [
Expand Down Expand Up @@ -134,7 +134,8 @@ const getAuthClientPrincipal = async function (authProvider: string, codeValue:
claims,
userRoles: ["authenticated", "anonymous"],
};
} catch {
} catch (error) {
console.error(`Error while parsing user information: ${error}`);
return null;
}
};
Expand All @@ -151,27 +152,42 @@ const getOAuthToken = function (authProvider: string, codeValue: string, authCon
tenantId = authConfigs?.openIdIssuer.split("/")[3];
}

const data = querystring.stringify({
const queryString: Record<string, string> = {
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<string, string | number> = {
"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: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": Buffer.byteLength(data),
},
headers: headers,
};

return new Promise((resolve, reject) => {
Expand Down
3 changes: 3 additions & 0 deletions src/msha/auth/routes/auth-login-provider-custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ 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;
}
Expand Down

0 comments on commit 6745391

Please sign in to comment.