mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
fix(oauth)!: derive redirect URI from per-request baseURL via declarative callbackPath
Multi-host deployments configured with `baseURL: { allowedHosts: [...] }`
now produce absolute `redirect_uri` values for generic-OAuth providers. Auth0
and other strict authorization servers previously rejected the authorize step
because `genericOAuth` captured `ctx.baseURL` at init time, which is the empty
sentinel under dynamic config.
Every `OAuthProvider` now declares `callbackPath: string`. Endpoints compose
`redirectURI = ctx.context.baseURL + provider.callbackPath` in the
social-sign-in, callback, link-account, and oauth-proxy handlers, removing the
init-time `ctx.baseURL` read across the OAuth surface. Built-in providers use
`/callback/<id>`; generic-OAuth uses `/oauth2/callback/<id>`.
The interface change is invisible to app developers. Third-party plugin
authors who construct their own `OAuthProvider` must add `callbackPath` to
their factory.
Resolves #9593. Partial fix for #4151.
Co-Authored-By: thaoms <2323270+thaoms@users.noreply.github.com>
This commit is contained in:
co-authored by
thaoms
parent
8dcddf786c
commit
2c29c1e919
@@ -0,0 +1,22 @@
|
||||
---
|
||||
"better-auth": minor
|
||||
"@better-auth/core": minor
|
||||
---
|
||||
|
||||
Derive OAuth `redirect_uri` from the per-request `baseURL` so social and generic-OAuth sign-in work end-to-end under `baseURL: { allowedHosts: [...] }`.
|
||||
|
||||
Before this change, the `genericOAuth` plugin captured `ctx.baseURL` at init time and sent providers a relative URI like `/oauth2/callback/<id>` whenever a dynamic baseURL was configured. Auth0 and other strict providers rejected the authorize request. Built-in social providers were unaffected because their endpoints already resolved per-request.
|
||||
|
||||
The fix is structural rather than a string patch. Every `OAuthProvider` now declares a `callbackPath: string` that describes its callback path under the per-request `baseURL`. The framework composes `redirectURI = ctx.context.baseURL + provider.callbackPath` inside the sign-in, callback, link-account, and oauth-proxy endpoints. The init-time `ctx.baseURL` is no longer read by any OAuth provider, which removes the bug class.
|
||||
|
||||
Resolves #9593. Partial fix for #4151 — `redirect_uri` composition now respects `allowedHosts` for both built-in providers and generic-OAuth providers.
|
||||
|
||||
BREAKING CHANGE: Custom `OAuthProvider` implementations must declare `callbackPath`. End-user `betterAuth({...})` config is unaffected; only authors of custom OAuth provider plugins need to update their factories. Convention:
|
||||
|
||||
```ts
|
||||
// built-in lane
|
||||
callbackPath: `/callback/<provider-id>`,
|
||||
|
||||
// generic-oauth lane
|
||||
callbackPath: `/oauth2/callback/<provider-id>`,
|
||||
```
|
||||
@@ -378,7 +378,7 @@ export const linkSocialAccount = createAuthEndpoint(
|
||||
const url = await provider.createAuthorizationURL({
|
||||
state: state.state,
|
||||
codeVerifier: state.codeVerifier,
|
||||
redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
|
||||
redirectURI: `${c.context.baseURL}${provider.callbackPath}`,
|
||||
scopes: c.body.scopes,
|
||||
loginHint: c.body.loginHint,
|
||||
additionalParams: c.body.additionalParams,
|
||||
|
||||
@@ -172,7 +172,7 @@ export const callbackOAuth = createAuthEndpoint(
|
||||
code: code,
|
||||
codeVerifier,
|
||||
deviceId: device_id,
|
||||
redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
|
||||
redirectURI: `${c.context.baseURL}${provider.callbackPath}`,
|
||||
});
|
||||
} catch (e) {
|
||||
c.context.logger.error("", e);
|
||||
|
||||
@@ -349,7 +349,7 @@ export const signInSocial = <O extends BetterAuthOptions>() =>
|
||||
const url = await provider.createAuthorizationURL({
|
||||
state,
|
||||
codeVerifier,
|
||||
redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
|
||||
redirectURI: `${c.context.baseURL}${provider.callbackPath}`,
|
||||
scopes: c.body.scopes,
|
||||
loginHint: c.body.loginHint,
|
||||
additionalParams: c.body.additionalParams,
|
||||
|
||||
@@ -2906,3 +2906,124 @@ describe("oauth2", async () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/4151
|
||||
* @see https://github.com/better-auth/better-auth/issues/9593
|
||||
*/
|
||||
describe("redirect_uri composition under dynamic baseURL", async () => {
|
||||
const dynamicServer = new OAuth2Server();
|
||||
await dynamicServer.start();
|
||||
const dynamicPort = Number(dynamicServer.issuer.url?.split(":")[2]!);
|
||||
|
||||
afterAll(async () => {
|
||||
await dynamicServer.stop();
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
await dynamicServer.issuer.keys.generate("RS256");
|
||||
});
|
||||
|
||||
async function getAuthorizeRedirectUri(
|
||||
auth: { handler: (request: Request) => Promise<Response> },
|
||||
signInPath: string,
|
||||
host: string,
|
||||
providerId: string,
|
||||
) {
|
||||
const response = await auth.handler(
|
||||
new Request(`http://${host}${signInPath}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
provider: providerId,
|
||||
callbackURL: "/dashboard",
|
||||
disableRedirect: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const json = (await response.json()) as { url?: string };
|
||||
expect(json.url).toBeDefined();
|
||||
const authUrl = new URL(json.url!);
|
||||
return authUrl.searchParams.get("redirect_uri");
|
||||
}
|
||||
|
||||
it("composes an absolute redirect_uri for a generic-oauth provider at the default basePath", async () => {
|
||||
const { auth } = await getTestInstance({
|
||||
baseURL: { allowedHosts: ["localhost:3000"] },
|
||||
plugins: [
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: "dynamic-oauth-test",
|
||||
discoveryUrl: `http://localhost:${dynamicPort}/.well-known/openid-configuration`,
|
||||
clientId: "test-client-id",
|
||||
clientSecret: "test-client-secret",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const redirectUri = await getAuthorizeRedirectUri(
|
||||
auth,
|
||||
"/api/auth/sign-in/social",
|
||||
"localhost:3000",
|
||||
"dynamic-oauth-test",
|
||||
);
|
||||
|
||||
expect(redirectUri).toBe(
|
||||
"http://localhost:3000/api/auth/oauth2/callback/dynamic-oauth-test",
|
||||
);
|
||||
});
|
||||
|
||||
it("composes an absolute redirect_uri for a generic-oauth provider at a custom basePath", async () => {
|
||||
const { auth } = await getTestInstance({
|
||||
baseURL: { allowedHosts: ["localhost:3000"] },
|
||||
basePath: "/auth",
|
||||
plugins: [
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: "dynamic-oauth-test",
|
||||
discoveryUrl: `http://localhost:${dynamicPort}/.well-known/openid-configuration`,
|
||||
clientId: "test-client-id",
|
||||
clientSecret: "test-client-secret",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const redirectUri = await getAuthorizeRedirectUri(
|
||||
auth,
|
||||
"/auth/sign-in/social",
|
||||
"localhost:3000",
|
||||
"dynamic-oauth-test",
|
||||
);
|
||||
|
||||
expect(redirectUri).toBe(
|
||||
"http://localhost:3000/auth/oauth2/callback/dynamic-oauth-test",
|
||||
);
|
||||
});
|
||||
|
||||
it("composes an absolute redirect_uri for a built-in social provider", async () => {
|
||||
const { auth } = await getTestInstance({
|
||||
baseURL: { allowedHosts: ["localhost:3000"] },
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: "google-client-id",
|
||||
clientSecret: "google-client-secret",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const redirectUri = await getAuthorizeRedirectUri(
|
||||
auth,
|
||||
"/api/auth/sign-in/social",
|
||||
"localhost:3000",
|
||||
"google",
|
||||
);
|
||||
|
||||
expect(redirectUri).toBe("http://localhost:3000/api/auth/callback/google");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,6 +252,7 @@ export const genericOAuth = <const ID extends string>(
|
||||
genericProviders.push({
|
||||
id: c.providerId,
|
||||
name: c.name ?? c.providerId,
|
||||
callbackPath: `/oauth2/callback/${c.providerId}`,
|
||||
issuer,
|
||||
allowIdpInitiated: c.allowIdpInitiated,
|
||||
createAuthorizationURL(data) {
|
||||
|
||||
@@ -436,7 +436,7 @@ export const oAuthProxy = <O extends OAuthProxyOptions>(opts?: O) => {
|
||||
tokens = await provider.validateAuthorizationCode({
|
||||
code,
|
||||
codeVerifier: stateData.codeVerifier,
|
||||
redirectURI: `${ctx.context.baseURL}/callback/${provider.id}`,
|
||||
redirectURI: `${ctx.context.baseURL}${provider.callbackPath}`,
|
||||
});
|
||||
} catch (e) {
|
||||
ctx.context.logger.error(
|
||||
|
||||
@@ -28,6 +28,17 @@ export interface OAuthProvider<
|
||||
O extends Record<string, any> = Partial<ProviderOptions>,
|
||||
> {
|
||||
id: LiteralString;
|
||||
/**
|
||||
* Path under the resolved per-request `baseURL` where this provider's
|
||||
* OAuth callback handler is mounted. Endpoints compose
|
||||
* `redirectURI = ctx.context.baseURL + callbackPath` per request, so the
|
||||
* provider must not hardcode an origin or `baseURL` here.
|
||||
*
|
||||
* Built-in social providers use `/callback/<id>`. Plugin-provided OAuth
|
||||
* providers (e.g. generic-oauth) use their plugin's own lane, such as
|
||||
* `/oauth2/callback/<id>`.
|
||||
*/
|
||||
callbackPath: string;
|
||||
createAuthorizationURL: (data: {
|
||||
state: string;
|
||||
codeVerifier: string;
|
||||
|
||||
@@ -99,6 +99,7 @@ export const apple = (options: AppleOptions) => {
|
||||
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
||||
return {
|
||||
id: "apple",
|
||||
callbackPath: "/callback/apple",
|
||||
name: "Apple",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -33,6 +33,7 @@ export const atlassian = (options: AtlassianOptions) => {
|
||||
const tokenEndpoint = "https://auth.atlassian.com/oauth/token";
|
||||
return {
|
||||
id: "atlassian",
|
||||
callbackPath: "/callback/atlassian",
|
||||
name: "Atlassian",
|
||||
|
||||
async createAuthorizationURL({
|
||||
|
||||
@@ -72,6 +72,7 @@ export const cognito = (options: CognitoOptions) => {
|
||||
|
||||
return {
|
||||
id: "cognito",
|
||||
callbackPath: "/callback/cognito",
|
||||
name: "Cognito",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -87,6 +87,7 @@ export const discord = (options: DiscordOptions) => {
|
||||
const tokenEndpoint = "https://discord.com/api/oauth2/token";
|
||||
return {
|
||||
id: "discord",
|
||||
callbackPath: "/callback/discord",
|
||||
name: "Discord",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
|
||||
|
||||
@@ -30,6 +30,7 @@ export const dropbox = (options: DropboxOptions) => {
|
||||
|
||||
return {
|
||||
id: "dropbox",
|
||||
callbackPath: "/callback/dropbox",
|
||||
name: "Dropbox",
|
||||
createAuthorizationURL: async ({
|
||||
state,
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
|
||||
export const facebook = (options: FacebookOptions) => {
|
||||
return {
|
||||
id: "facebook",
|
||||
callbackPath: "/callback/facebook",
|
||||
name: "Facebook",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -23,6 +23,7 @@ export const figma = (options: FigmaOptions) => {
|
||||
const tokenEndpoint = "https://api.figma.com/v1/oauth/token";
|
||||
return {
|
||||
id: "figma",
|
||||
callbackPath: "/callback/figma",
|
||||
name: "Figma",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -62,6 +62,7 @@ export const github = (options: GithubOptions) => {
|
||||
const tokenEndpoint = "https://github.com/login/oauth/access_token";
|
||||
return {
|
||||
id: "github",
|
||||
callbackPath: "/callback/github",
|
||||
name: "GitHub",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -80,6 +80,7 @@ export const gitlab = (options: GitlabOptions) => {
|
||||
const issuerName = "Gitlab";
|
||||
return {
|
||||
id: issuerId,
|
||||
callbackPath: `/callback/${issuerId}`,
|
||||
name: issuerName,
|
||||
createAuthorizationURL: async ({
|
||||
state,
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface GoogleOptions extends ProviderOptions<GoogleProfile> {
|
||||
export const google = (options: GoogleOptions) => {
|
||||
return {
|
||||
id: "google",
|
||||
callbackPath: "/callback/google",
|
||||
name: "Google",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -46,6 +46,7 @@ export const huggingface = (options: HuggingFaceOptions) => {
|
||||
const tokenEndpoint = "https://huggingface.co/oauth/token";
|
||||
return {
|
||||
id: "huggingface",
|
||||
callbackPath: "/callback/huggingface",
|
||||
name: "Hugging Face",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -105,6 +105,7 @@ export const kakao = (options: KakaoOptions) => {
|
||||
const tokenEndpoint = "https://kauth.kakao.com/oauth/token";
|
||||
return {
|
||||
id: "kakao",
|
||||
callbackPath: "/callback/kakao",
|
||||
name: "Kakao",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface KickOptions extends ProviderOptions<KickProfile> {
|
||||
export const kick = (options: KickOptions) => {
|
||||
return {
|
||||
id: "kick",
|
||||
callbackPath: "/callback/kick",
|
||||
name: "Kick",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -49,6 +49,7 @@ export const line = (options: LineOptions) => {
|
||||
|
||||
return {
|
||||
id: "line",
|
||||
callbackPath: "/callback/line",
|
||||
name: "LINE",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -30,6 +30,7 @@ export const linear = (options: LinearOptions) => {
|
||||
const tokenEndpoint = "https://api.linear.app/oauth/token";
|
||||
return {
|
||||
id: "linear",
|
||||
callbackPath: "/callback/linear",
|
||||
name: "Linear",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -31,6 +31,7 @@ export const linkedin = (options: LinkedInOptions) => {
|
||||
|
||||
return {
|
||||
id: "linkedin",
|
||||
callbackPath: "/callback/linkedin",
|
||||
name: "Linkedin",
|
||||
createAuthorizationURL: async ({
|
||||
state,
|
||||
|
||||
@@ -148,6 +148,7 @@ export const microsoft = (options: MicrosoftOptions) => {
|
||||
const tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`;
|
||||
return {
|
||||
id: "microsoft",
|
||||
callbackPath: "/callback/microsoft",
|
||||
name: "Microsoft EntraID",
|
||||
createAuthorizationURL(data) {
|
||||
// Microsoft Entra supports public clients (SPA / native apps with
|
||||
|
||||
@@ -43,6 +43,7 @@ export const naver = (options: NaverOptions) => {
|
||||
const tokenEndpoint = "https://nid.naver.com/oauth2.0/token";
|
||||
return {
|
||||
id: "naver",
|
||||
callbackPath: "/callback/naver",
|
||||
name: "Naver",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
|
||||
|
||||
@@ -27,6 +27,7 @@ export const notion = (options: NotionOptions) => {
|
||||
const tokenEndpoint = "https://api.notion.com/v1/oauth/token";
|
||||
return {
|
||||
id: "notion",
|
||||
callbackPath: "/callback/notion",
|
||||
name: "Notion",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -35,6 +35,7 @@ export const paybin = (options: PaybinOptions) => {
|
||||
|
||||
return {
|
||||
id: "paybin",
|
||||
callbackPath: "/callback/paybin",
|
||||
name: "Paybin",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -77,6 +77,7 @@ export const paypal = (options: PayPalOptions) => {
|
||||
|
||||
return {
|
||||
id: "paypal",
|
||||
callbackPath: "/callback/paypal",
|
||||
name: "PayPal",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -36,6 +36,7 @@ export const polar = (options: PolarOptions) => {
|
||||
const tokenEndpoint = "https://api.polar.sh/v1/oauth2/token";
|
||||
return {
|
||||
id: "polar",
|
||||
callbackPath: "/callback/polar",
|
||||
name: "Polar",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface RailwayOptions extends ProviderOptions<RailwayProfile> {
|
||||
export const railway = (options: RailwayOptions) => {
|
||||
return {
|
||||
id: "railway",
|
||||
callbackPath: "/callback/railway",
|
||||
name: "Railway",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface RedditOptions extends ProviderOptions<RedditProfile> {
|
||||
export const reddit = (options: RedditOptions) => {
|
||||
return {
|
||||
id: "reddit",
|
||||
callbackPath: "/callback/reddit",
|
||||
name: "Reddit",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope ? [] : ["identity"];
|
||||
|
||||
@@ -40,6 +40,7 @@ export const roblox = (options: RobloxOptions) => {
|
||||
const tokenEndpoint = "https://apis.roblox.com/oauth/v1/token";
|
||||
return {
|
||||
id: "roblox",
|
||||
callbackPath: "/callback/roblox",
|
||||
name: "Roblox",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope ? [] : ["openid", "profile"];
|
||||
|
||||
@@ -62,6 +62,7 @@ export const salesforce = (options: SalesforceOptions) => {
|
||||
|
||||
return {
|
||||
id: "salesforce",
|
||||
callbackPath: "/callback/salesforce",
|
||||
name: "Salesforce",
|
||||
|
||||
async createAuthorizationURL({
|
||||
|
||||
@@ -45,6 +45,7 @@ export const slack = (options: SlackOptions) => {
|
||||
const tokenEndpoint = "https://slack.com/api/openid.connect.token";
|
||||
return {
|
||||
id: "slack",
|
||||
callbackPath: "/callback/slack",
|
||||
name: "Slack",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope
|
||||
|
||||
@@ -23,6 +23,7 @@ export const spotify = (options: SpotifyOptions) => {
|
||||
const tokenEndpoint = "https://accounts.spotify.com/api/token";
|
||||
return {
|
||||
id: "spotify",
|
||||
callbackPath: "/callback/spotify",
|
||||
name: "Spotify",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -134,6 +134,7 @@ export const tiktok = (options: TiktokOptions) => {
|
||||
const tokenEndpoint = "https://open.tiktokapis.com/v2/oauth/token/";
|
||||
return {
|
||||
id: "tiktok",
|
||||
callbackPath: "/callback/tiktok",
|
||||
name: "TikTok",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"];
|
||||
|
||||
@@ -41,6 +41,7 @@ export const twitch = (options: TwitchOptions) => {
|
||||
const tokenEndpoint = "https://id.twitch.tv/oauth2/token";
|
||||
return {
|
||||
id: "twitch",
|
||||
callbackPath: "/callback/twitch",
|
||||
name: "Twitch",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope
|
||||
|
||||
@@ -107,6 +107,7 @@ export const twitter = (options: TwitterOption) => {
|
||||
const tokenEndpoint = "https://api.x.com/2/oauth2/token";
|
||||
return {
|
||||
id: "twitter",
|
||||
callbackPath: "/callback/twitter",
|
||||
name: "Twitter",
|
||||
createAuthorizationURL(data) {
|
||||
const _scopes = options.disableDefaultScope
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface VercelOptions extends ProviderOptions<VercelProfile> {
|
||||
export const vercel = (options: VercelOptions) => {
|
||||
return {
|
||||
id: "vercel",
|
||||
callbackPath: "/callback/vercel",
|
||||
name: "Vercel",
|
||||
createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -29,6 +29,7 @@ export const vk = (options: VkOption) => {
|
||||
const tokenEndpoint = "https://id.vk.com/oauth2/auth";
|
||||
return {
|
||||
id: "vk",
|
||||
callbackPath: "/callback/vk",
|
||||
name: "VK",
|
||||
async createAuthorizationURL({
|
||||
state,
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface WeChatOptions extends ProviderOptions<WeChatProfile> {
|
||||
export const wechat = (options: WeChatOptions) => {
|
||||
return {
|
||||
id: "wechat",
|
||||
callbackPath: "/callback/wechat",
|
||||
name: "WeChat",
|
||||
createAuthorizationURL({ state, scopes, redirectURI, additionalParams }) {
|
||||
const _scopes = options.disableDefaultScope ? [] : ["snsapi_login"];
|
||||
|
||||
@@ -151,6 +151,7 @@ export const zoom = (userOptions: ZoomOptions) => {
|
||||
|
||||
return {
|
||||
id: "zoom",
|
||||
callbackPath: "/callback/zoom",
|
||||
name: "Zoom",
|
||||
createAuthorizationURL: async ({
|
||||
state,
|
||||
|
||||
Reference in New Issue
Block a user