feat: Add twitter oauth provider (#206)

* add twitter oauth

* add slack oauth

* add emailVerified field

* twitter -> x

* fixed x user info

* add slack authed user type & fix token set

* fix endpoint

* fix slack button

* fix slack oauth

* Fix merge conflicts

* merge dev

* fix merge conflicts

---------

Co-authored-by: Zai Shi <zaishi00@outlook.com>
This commit is contained in:
Manoj Kumar 2024-09-20 04:58:58 +05:30 committed by GitHub
parent f5c549a520
commit c4ae4fc4ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 171 additions and 1 deletions

View File

@ -0,0 +1,10 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "StandardOAuthProviderType" ADD VALUE 'X';
ALTER TYPE "StandardOAuthProviderType" ADD VALUE 'SLACK';

View File

@ -568,6 +568,8 @@ enum StandardOAuthProviderType {
BITBUCKET
LINKEDIN
APPLE
X
SLACK
}
model OAuthToken {

View File

@ -14,7 +14,9 @@ import { GoogleProvider } from "./providers/google";
import { LinkedInProvider } from "./providers/linkedin";
import { MicrosoftProvider } from "./providers/microsoft";
import { MockProvider } from "./providers/mock";
import { SlackProvider } from "./providers/slack";
import { SpotifyProvider } from "./providers/spotify";
import { XProvider } from "./providers/x";
const _providers = {
github: GithubProvider,
@ -27,6 +29,8 @@ const _providers = {
apple: AppleProvider,
bitbucket: BitbucketProvider,
linkedin: LinkedInProvider,
x: XProvider,
slack: SlackProvider,
} as const;
const mockProvider = MockProvider;

View File

@ -0,0 +1,46 @@
import { OAuthBaseProvider, TokenSet } from "./base";
import { OAuthUserInfo, validateUserInfo } from "../utils";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
export class SlackProvider extends OAuthBaseProvider {
private constructor(
...args: ConstructorParameters<typeof OAuthBaseProvider>
) {
super(...args);
}
static async create(options: { clientId: string, clientSecret: string }) {
return new SlackProvider(
...(await OAuthBaseProvider.createConstructorArgs({
issuer: "https://slack.com",
authorizationEndpoint: "https://slack.com/oauth/v2/authorize",
tokenEndpoint: "https://slack.com/api/oauth.v2.access",
redirectUri:
getEnvVariable("STACK_BASE_URL") +
"/api/v1/auth/oauth/callback/slack",
baseScope: "",
authorizationExtraParams: {
user_scope: "email,profile,openid",
},
...options,
}))
);
}
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
const userInfo = await fetch(
"https://slack.com/api/openid.connect.userInfo", {
headers: {
Authorization: `Bearer ${tokenSet.accessToken}`,
}
}
).then(res => res.json());
return validateUserInfo({
accountId: userInfo.sub?.toString(),
displayName: userInfo.name,
email: userInfo.email,
profileImageUrl: userInfo.picture,
emailVerified: userInfo.email_verified,
});
}
}

View File

@ -0,0 +1,43 @@
import { OAuthBaseProvider, TokenSet } from "./base";
import { OAuthUserInfo, validateUserInfo } from "../utils";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
export class XProvider extends OAuthBaseProvider {
private constructor(
...args: ConstructorParameters<typeof OAuthBaseProvider>
) {
super(...args);
}
static async create(options: { clientId: string, clientSecret: string }) {
return new XProvider(
...(await OAuthBaseProvider.createConstructorArgs({
issuer: "https://twitter.com",
authorizationEndpoint: "https://twitter.com/i/oauth2/authorize",
tokenEndpoint: "https://api.x.com/2/oauth2/token",
redirectUri: getEnvVariable("STACK_BASE_URL") + "/api/v1/auth/oauth/callback/x",
baseScope: "users.read offline.access tweet.read",
...options,
}))
);
}
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
const { data: userInfo } = await fetch(
"https://api.x.com/2/users/me?user.fields=id,name,profile_image_url",
{
headers: {
Authorization: `Bearer ${tokenSet.accessToken}`,
},
}
).then((res) => res.json());
return validateUserInfo({
accountId: userInfo?.id?.toString(),
displayName: userInfo.name || userInfo.username,
// email: undefined, // There is no way of getting email from X Oauth2.0 API
profileImageUrl: userInfo.profile_image_url as any,
emailVerified: false,
});
}
}

View File

@ -27,6 +27,8 @@ function toTitle(id: string) {
apple: "Apple",
bitbucket: "Bitbucket",
linkedin: "LinkedIn",
x: "X",
slack: "Slack",
}[id];
}

View File

@ -11,6 +11,8 @@ const mockedProviders = [
"discord",
"gitlab",
"bitbucket",
"x",
"slack",
];
const configuration: Configuration = {

View File

@ -63,6 +63,16 @@ To use your own OAuth provider setups in production, follow these steps for each
Callback URL:
`https://api.stack-auth.com/api/v1/auth/oauth/callback/linkedin`
</Tab>
<Tab title="X">
[X OAuth Setup Guide](https://developer.x.com/en/docs/apps/overview)
Callback URL:
`https://api.stack-auth.com/api/v1/auth/oauth/callback/x`
</Tab>
<Tab title="Slack">
[X OAuth Setup Guide](https://api.slack.com/authentication/oauth-v2)
Callback URL:
`https://api.stack-auth.com/api/v1/auth/oauth/callback/slack`
</Tab>
</Tabs>
2. **Enter OAuth Credentials**: Go to the `Auth Methods` section in the Stack dashboard, open the provider's settings, switch from shared keys to custom keys, and enter the client ID and client secret.

View File

@ -1,4 +1,4 @@
export const standardProviders = ["google", "github", "microsoft", "spotify", "facebook", "discord", "gitlab", "bitbucket", "linkedin", "apple"] as const;
export const standardProviders = ["google", "github", "microsoft", "spotify", "facebook", "discord", "gitlab", "bitbucket", "linkedin", "apple", "x", "slack"] as const;
// No more shared providers should be added except for special cases
export const sharedProviders = ["google", "github", "microsoft", "spotify"] as const;
export const allProviders = standardProviders;

View File

@ -140,6 +140,39 @@ function AppleIcon({ iconSize } : { iconSize: number} ) {
);
}
function XIcon({ iconSize } : { iconSize: number} ) {
return (
<svg aria-label="X" viewBox="0 0 1200 1227" width={iconSize} height={iconSize} xmlns="http://www.w3.org/2000/svg">
<path fill="#FFFFFF" d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"/>
</svg>
);
}
function SlackIcon({ iconSize } : { iconSize: number} ) {
return (
<svg width={iconSize} height={iconSize} viewBox="0 0 54 54" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path
d="M19.712.133a5.381 5.381 0 0 0-5.376 5.387 5.381 5.381 0 0 0 5.376 5.386h5.376V5.52A5.381 5.381 0 0 0 19.712.133m0 14.365H5.376A5.381 5.381 0 0 0 0 19.884a5.381 5.381 0 0 0 5.376 5.387h14.336a5.381 5.381 0 0 0 5.376-5.387 5.381 5.381 0 0 0-5.376-5.386"
fill="#44BEDF"
></path>
<path
d="M53.76 19.884a5.381 5.381 0 0 0-5.376-5.386 5.381 5.381 0 0 0-5.376 5.386v5.387h5.376a5.381 5.381 0 0 0 5.376-5.387m-14.336 0V5.52A5.381 5.381 0 0 0 34.048.133a5.381 5.381 0 0 0-5.376 5.387v14.364a5.381 5.381 0 0 0 5.376 5.387 5.381 5.381 0 0 0 5.376-5.387"
fill="#2EB67D"
></path>
<path
d="M34.048 54a5.381 5.381 0 0 0 5.376-5.387 5.381 5.381 0 0 0-5.376-5.386h-5.376v5.386A5.381 5.381 0 0 0 34.048 54m0-14.365h14.336a5.381 5.381 0 0 0 5.376-5.386 5.381 5.381 0 0 0-5.376-5.387H34.048a5.381 5.381 0 0 0-5.376 5.387 5.381 5.381 0 0 0 5.376 5.386"
fill="#ECB22E"
></path>
<path
d="M0 34.249a5.381 5.381 0 0 0 5.376 5.386 5.381 5.381 0 0 0 5.376-5.386v-5.387H5.376A5.381 5.381 0 0 0 0 34.25m14.336-.001v14.364A5.381 5.381 0 0 0 19.712 54a5.381 5.381 0 0 0 5.376-5.387V34.25a5.381 5.381 0 0 0-5.376-5.387 5.381 5.381 0 0 0-5.376 5.387"
fill="#E01E5A"
></path>
</g>
</svg>
);
}
const changeColor = (c: Color, value: number) => {
if (c.isLight()) {
value = -value;
@ -261,6 +294,24 @@ export function OAuthButton({
};
break;
}
case 'x': {
style = {
backgroundColor: "#000",
textColor: "#fff",
name: "X",
icon: <XIcon iconSize={iconSize} />,
};
break;
}
case 'slack': {
style = {
backgroundColor: "#611f69",
textColor: "#fff",
name: "Slack",
icon: <SlackIcon iconSize={iconSize} />,
};
break;
}
default: {
style = {
name: provider,