diff --git a/.github/workflows/azuresdkdrop.yml b/.github/workflows/azuresdkdrop.yml index 1e49f8cd..42e0cb5e 100644 --- a/.github/workflows/azuresdkdrop.yml +++ b/.github/workflows/azuresdkdrop.yml @@ -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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bdf51f4..3850931c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/src/core/constants.ts b/src/core/constants.ts index 4ea23819..86e71a46 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -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 @@ -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: { @@ -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 = { diff --git a/src/msha/auth/index.ts b/src/msha/auth/index.ts index 3ddd8052..d313545c 100644 --- a/src/msha/auth/index.ts +++ b/src/msha/auth/index.ts @@ -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/(?${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/(?${supportedAuthsRegex})(\\?.*)?$`, "i"), function: "auth-login-provider-custom", }); diff --git a/src/msha/auth/routes/auth-login-provider-callback.ts b/src/msha/auth/routes/auth-login-provider-callback.ts index 4b9196ae..617141a4 100644 --- a/src/msha/auth/routes/auth-login-provider-callback.ts +++ b/src/msha/auth/routes/auth-login-provider-callback.ts @@ -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; - 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 }[] = [ @@ -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; } }; @@ -151,27 +152,42 @@ const getOAuthToken = function (authProvider: string, codeValue: string, authCon tenantId = authConfigs?.openIdIssuer.split("/")[3]; } - const data = querystring.stringify({ + const queryString: Record = { 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 = { + "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) => { diff --git a/src/msha/auth/routes/auth-login-provider-custom.ts b/src/msha/auth/routes/auth-login-provider-custom.ts index 5430a3d1..600dcf20 100644 --- a/src/msha/auth/routes/auth-login-provider-custom.ts +++ b/src/msha/auth/routes/auth-login-provider-custom.ts @@ -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; }