mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
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:
parent
f5c549a520
commit
c4ae4fc4ed
@ -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';
|
||||
@ -568,6 +568,8 @@ enum StandardOAuthProviderType {
|
||||
BITBUCKET
|
||||
LINKEDIN
|
||||
APPLE
|
||||
X
|
||||
SLACK
|
||||
}
|
||||
|
||||
model OAuthToken {
|
||||
|
||||
@ -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;
|
||||
|
||||
46
apps/backend/src/oauth/providers/slack.tsx
Normal file
46
apps/backend/src/oauth/providers/slack.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
43
apps/backend/src/oauth/providers/x.tsx
Normal file
43
apps/backend/src/oauth/providers/x.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,8 @@ function toTitle(id: string) {
|
||||
apple: "Apple",
|
||||
bitbucket: "Bitbucket",
|
||||
linkedin: "LinkedIn",
|
||||
x: "X",
|
||||
slack: "Slack",
|
||||
}[id];
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,8 @@ const mockedProviders = [
|
||||
"discord",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"x",
|
||||
"slack",
|
||||
];
|
||||
|
||||
const configuration: Configuration = {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user