feat: microsoft sso

This commit is contained in:
Bereket Engida
2024-10-05 10:51:14 +03:00
parent b046d67e59
commit 438c8e2045
21 changed files with 407 additions and 184 deletions

View File

@@ -105,67 +105,88 @@ export default function SignIn() {
>
{loading ? <Loader2 size={16} className="animate-spin" /> : "Login"}
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "github",
callbackURL: "/dashboard",
});
}}
>
<GitHubLogoIcon />
Continue with Github
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "discord",
callbackURL: "/dashboard",
});
}}
>
<DiscordLogoIcon />
Continue with Discord
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="0.98em"
height="1em"
viewBox="0 0 256 262"
<div className="flex items-center gap-2">
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "github",
callbackURL: "/dashboard",
});
}}
>
<path
fill="#4285F4"
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
/>
<path
fill="#34A853"
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
/>
<path
fill="#FBBC05"
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
/>
<path
fill="#EB4335"
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
/>
</svg>
Continue with Google
</Button>
<GitHubLogoIcon />
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "discord",
callbackURL: "/dashboard",
});
}}
>
<DiscordLogoIcon />
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="0.98em"
height="1em"
viewBox="0 0 256 262"
>
<path
fill="#4285F4"
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
/>
<path
fill="#34A853"
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
/>
<path
fill="#FBBC05"
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
/>
<path
fill="#EB4335"
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
/>
</svg>
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "microsoft",
callbackURL: "/dashboard",
});
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
></path>
</svg>
</Button>
</div>
<Button
variant="outline"
className="gap-2"

View File

@@ -12,7 +12,7 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
import { GitHubLogoIcon } from "@radix-ui/react-icons";
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
import { useState } from "react";
import { client, signIn, signUp } from "@/lib/auth-client";
import Image from "next/image";
@@ -174,74 +174,88 @@ export function SignUp() {
"Create an account"
)}
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
const res = await client.signIn.social({
provider: "google",
callbackURL: "/dashboard",
fetchOptions: {
onRequest: () => {
setLoading(true);
},
onResponse: () => {
setLoading(false);
},
},
});
}}
disabled={loading}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="0.98em"
height="1em"
viewBox="0 0 256 262"
>
<path
fill="#4285F4"
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
/>
<path
fill="#34A853"
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
/>
<path
fill="#FBBC05"
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
/>
<path
fill="#EB4335"
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
/>
</svg>
Continue with Google
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social(
{
<div className="flex items-center gap-2">
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "github",
callbackURL: "/dashboard",
},
{
onRequest: () => {
setLoading(true);
},
onResponse: () => {
setLoading(false);
},
},
);
}}
disabled={loading}
>
<GitHubLogoIcon />
Continue with Github
</Button>
});
}}
>
<GitHubLogoIcon />
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "discord",
callbackURL: "/dashboard",
});
}}
>
<DiscordLogoIcon />
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="0.98em"
height="1em"
viewBox="0 0 256 262"
>
<path
fill="#4285F4"
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
/>
<path
fill="#34A853"
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
/>
<path
fill="#FBBC05"
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
/>
<path
fill="#EB4335"
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
/>
</svg>
</Button>
<Button
variant="outline"
className="w-full gap-2"
onClick={async () => {
await signIn.social({
provider: "microsoft",
callbackURL: "/dashboard",
});
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
></path>
</svg>
</Button>
</div>
</div>
</CardContent>
<CardFooter>

View File

@@ -88,5 +88,9 @@ export const auth = betterAuth({
clientId: process.env.DISCORD_CLIENT_ID || "",
clientSecret: process.env.DISCORD_CLIENT_SECRET || "",
},
microsoft: {
clientId: process.env.MICROSOFT_CLIENT_ID || "",
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
},
},
});

View File

@@ -434,6 +434,23 @@ export const contents: Content[] = [
</svg>
),
},
{
title: "Microsoft",
href: "/docs/authentication/microsoft",
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
></path>
</svg>
),
},
{
title: "Twitch",
href: "/docs/authentication/twitch",

View File

@@ -13,7 +13,7 @@ description: Google Provider
<Step>
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.google` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"

View File

@@ -0,0 +1,55 @@
---
title: Microsoft
description: Microsoft SSO
---
Enabling OAuth with Microsoft Azure Entra ID (formerly Active Directory) allows your users to sign in and sign up to your application with their Microsoft account.
<Steps>
<Step>
### Get your Microsoft credentials
To use Microsoft as a social provider, you need to get your Microsoft credentials. Which involves generating your own Client ID and Client Secret using your Microsoft Entra ID dashboard account.
see the [Microsoft Entra ID documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) for more information.
</Step>
<Step>
### Configure the provider
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.google` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { google } from "better-auth/social-providers"
export const auth = await betterAuth({
socialProviders: { // [!code highlight]
google: { // [!code highlight]
clientId: process.env.MICROSOFT_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
}, // [!code highlight]
})
```
</Step>
</Steps>
## Signin with Microsoft
To signin with Microsoft, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
- `provider`: The provider to use. It should be set to `microsoft`.
```ts title="client.ts" /
import { createAuthClient } from "better-auth/client"
const client = createAuthClient()
const signin = async () => {
const data = await client.signIn.social({
provider: "microsoft",
callbackURL: "/dashboard" //the url to redirect to after the signin
})
}
```

View File

@@ -69,6 +69,7 @@ export const callbackOAuth = createAuthEndpoint(
...user,
id,
});
console.log({ user, data });
const parsedState = parseState(c.query.state);
if (!parsedState.success) {
c.context.logger.error("Unable to parse state");
@@ -77,6 +78,7 @@ export const callbackOAuth = createAuthEndpoint(
);
}
const { callbackURL, currentURL, dontRememberMe } = parsedState.data;
if (!user || data.success === false) {
throw c.redirect(
`${c.context.baseURL}/error?error=oauth_validation_failed`,

View File

@@ -1,5 +1,5 @@
import { OAuth2Tokens } from "arctic";
import type { OAuthProvider } from ".";
import type { OAuthProvider, ProviderOptions } from ".";
import { parseJWT } from "oslo/jwt";
import { betterFetch } from "@better-fetch/fetch";
import { BetterAuthError } from "../error/better-auth-error";
@@ -47,11 +47,7 @@ export interface AppleProfile {
name: string;
}
export interface AppleOptions {
clientId: string;
clientSecret: string;
redirectURI?: string;
}
export interface AppleOptions extends ProviderOptions {}
export const apple = (options: AppleOptions) => {
const tokenEndpoint = "https://appleid.apple.com/auth/token";
@@ -59,7 +55,7 @@ export const apple = (options: AppleOptions) => {
id: "apple",
name: "Apple",
createAuthorizationURL({ state, scopes, redirectURI }) {
const _scope = scopes || ["email", "name", "openid"];
const _scope = options.scope || scopes || ["email", "name", "openid"];
return new URL(
`https://appleid.apple.com/auth/authorize?client_id=${
options.clientId

View File

@@ -1,5 +1,5 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider } from ".";
import type { OAuthProvider, ProviderOptions } from ".";
import { getRedirectURI, validateAuthorizationCode } from "./utils";
export interface DiscordProfile extends Record<string, any> {
@@ -74,18 +74,14 @@ export interface DiscordProfile extends Record<string, any> {
image_url: string;
}
export interface DiscordOptions {
clientId: string;
clientSecret: string;
redirectURI?: string;
}
export interface DiscordOptions extends ProviderOptions {}
export const discord = (options: DiscordOptions) => {
return {
id: "discord",
name: "Discord",
createAuthorizationURL({ state, scopes }) {
const _scopes = scopes || ["identify", "email"];
const _scopes = options.scope || scopes || ["identify", "email"];
return new URL(
`https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
"+",

View File

@@ -1,6 +1,6 @@
import { betterFetch } from "@better-fetch/fetch";
import { Facebook } from "arctic";
import type { OAuthProvider } from ".";
import type { OAuthProvider, ProviderOptions } from ".";
import { getRedirectURI, validateAuthorizationCode } from "./utils";
export interface FacebookProfile {
@@ -17,11 +17,7 @@ export interface FacebookProfile {
};
};
}
export interface FacebookOptions {
clientId: string;
clientSecret: string;
redirectURI?: string;
}
export interface FacebookOptions extends ProviderOptions {}
export const facebook = (options: FacebookOptions) => {
const facebookArctic = new Facebook(
options.clientId,
@@ -32,7 +28,7 @@ export const facebook = (options: FacebookOptions) => {
id: "facebook",
name: "Facebook",
createAuthorizationURL({ state, scopes }) {
const _scopes = scopes || ["email", "public_profile"];
const _scopes = options.scope || scopes || ["email", "public_profile"];
return facebookArctic.createAuthorizationURL(state, _scopes);
},
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {

View File

@@ -1,6 +1,6 @@
import { betterFetch } from "@better-fetch/fetch";
import { GitHub } from "arctic";
import type { OAuthProvider } from ".";
import type { OAuthProvider, ProviderOptions } from ".";
import { getRedirectURI } from "./utils";
export interface GithubProfile {
@@ -52,14 +52,11 @@ export interface GithubProfile {
last_name: string;
}
export interface GithubOptions {
clientId: string;
clientSecret: string;
redirectURI?: string;
}
export interface GithubOptions extends ProviderOptions {}
export const github = ({
clientId,
clientSecret,
scope,
redirectURI,
}: GithubOptions) => {
const githubArctic = new GitHub(
@@ -71,7 +68,7 @@ export const github = ({
id: "github",
name: "Github",
createAuthorizationURL({ state, scopes }) {
const _scopes = scopes || ["user:email"];
const _scopes = scope || scopes || ["user:email"];
return githubArctic.createAuthorizationURL(state, _scopes);
},
validateAuthorizationCode: async (state) => {

View File

@@ -53,7 +53,7 @@ export const google = (options: GoogleOptions) => {
if (!codeVerifier) {
throw new BetterAuthError("codeVerifier is required for Google");
}
const _scopes = scopes || ["email", "profile"];
const _scopes = options.scope || scopes || ["email", "profile"];
const url = googleArctic.createAuthorizationURL(
state,
codeVerifier,

View File

@@ -4,6 +4,7 @@ import { discord } from "./discord";
import { facebook } from "./facebook";
import { github } from "./github";
import { google } from "./google";
import { microsoft } from "./microsoft-entra-id";
import { spotify } from "./spotify";
import { twitch } from "./twitch";
import { twitter } from "./twitter";
@@ -13,6 +14,7 @@ export const oAuthProviders = {
discord,
facebook,
github,
microsoft,
google,
spotify,
twitch,
@@ -35,9 +37,10 @@ export type SocialProviders = typeof oAuthProviders extends {
export * from "./github";
export * from "./google";
export * from "./apple";
export * from "./microsoft-entra-id";
export * from "./discord";
export * from "./spotify";
export * from "./twitch";
export * from "./facebook";
export * from "./twitter";
export * from "../types/provider";
export * from "./types";

View File

@@ -0,0 +1,103 @@
import type { ProviderOptions } from ".";
import {
getRedirectURI,
validateAuthorizationCode,
createAuthorizationURL,
} from "./utils";
import type { OAuthProvider } from "./types";
import { betterFetch } from "@better-fetch/fetch";
import { parseJWT } from "oslo/jwt";
import { logger } from "../utils/logger";
export interface MicrosoftEntraIDProfile extends Record<string, any> {
sub: string;
name: string;
email: string;
picture: string;
}
export interface MicrosoftOptions extends ProviderOptions {
/**
* The tenant ID of the Microsoft account
* @default "common"
*/
tenantId?: string;
/**
* The size of the profile photo
* @default 48
*/
profilePhotoSize?: 48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648;
/**
* Disable profile photo
*/
disableProfilePhoto?: boolean;
}
export const microsoft = (options: MicrosoftOptions) => {
const tenant = options.tenantId || "common";
const authorizationEndpoint = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`;
const tokenEndpoint = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`;
return {
id: "microsoft",
name: "Microsoft EntraID",
createAuthorizationURL(data) {
const scopes = options.scope ||
data.scopes || ["openid", "profile", "email", "User.Read"];
return createAuthorizationURL(
"microsoft",
options,
authorizationEndpoint,
data.state,
data.codeVerifier,
scopes,
);
},
validateAuthorizationCode(code, codeVerifier, redirectURI) {
return validateAuthorizationCode({
code,
codeVerifier,
redirectURI:
redirectURI || getRedirectURI("microsoft", options.redirectURI),
options,
tokenEndpoint,
});
},
async getUserInfo(token) {
const user = parseJWT(token.idToken())
?.payload as MicrosoftEntraIDProfile;
const profilePhotoSize = options.profilePhotoSize || 48;
await betterFetch<ArrayBuffer>(
`https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`,
{
headers: {
Authorization: `Bearer ${token.accessToken()}`,
},
async onResponse(context) {
if (options.disableProfilePhoto || !context.response.ok) {
return;
}
try {
const response = context.response.clone();
const pictureBuffer = await response.arrayBuffer();
const pictureBase64 =
Buffer.from(pictureBuffer).toString("base64");
user.picture = `data:image/jpeg;base64, ${pictureBase64}`;
} catch (e) {
logger.error(e);
}
},
},
);
return {
user: {
id: user.sub,
name: user.name,
email: user.email,
image: user.picture,
emailVerified: true,
},
data: user,
};
},
} satisfies OAuthProvider;
};

View File

@@ -1,6 +1,6 @@
import { betterFetch } from "@better-fetch/fetch";
import { Spotify } from "arctic";
import type { OAuthProvider } from ".";
import type { OAuthProvider, ProviderOptions } from ".";
import { getRedirectURI, validateAuthorizationCode } from "./utils";
export interface SpotifyProfile {
@@ -12,11 +12,7 @@ export interface SpotifyProfile {
}[];
}
export interface SpotifyOptions {
clientId: string;
clientSecret: string;
redirectURI?: string;
}
export interface SpotifyOptions extends ProviderOptions {}
export const spotify = (options: SpotifyOptions) => {
const spotifyArctic = new Spotify(
@@ -28,7 +24,7 @@ export const spotify = (options: SpotifyOptions) => {
id: "spotify",
name: "Spotify",
createAuthorizationURL({ state, scopes }) {
const _scopes = scopes || ["user-read-email"];
const _scopes = options.scope || scopes || ["user-read-email"];
return spotifyArctic.createAuthorizationURL(state, _scopes);
},
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {

View File

@@ -1,6 +1,6 @@
import { betterFetch } from "@better-fetch/fetch";
import { Twitch } from "arctic";
import type { OAuthProvider } from ".";
import type { OAuthProvider, ProviderOptions } from ".";
import { getRedirectURI, validateAuthorizationCode } from "./utils";
export interface TwitchProfile {
@@ -22,12 +22,7 @@ export interface TwitchProfile {
picture: string;
}
export interface TwitchOptions {
clientId: string;
clientSecret: string;
redirectURI?: string;
}
export interface TwitchOptions extends ProviderOptions {}
export const twitch = (options: TwitchOptions) => {
const twitchArctic = new Twitch(
options.clientId,
@@ -38,7 +33,7 @@ export const twitch = (options: TwitchOptions) => {
id: "twitch",
name: "Twitch",
createAuthorizationURL({ state, scopes }) {
const _scopes = scopes || ["activity:write", "read"];
const _scopes = options.scope || scopes || ["activity:write", "read"];
return twitchArctic.createAuthorizationURL(state, _scopes);
},
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {

View File

@@ -1,6 +1,6 @@
import { betterFetch } from "@better-fetch/fetch";
import { Twitter } from "arctic";
import type { OAuthProvider } from ".";
import type { OAuthProvider, ProviderOptions } from ".";
import { getRedirectURI, validateAuthorizationCode } from "./utils";
import { BetterAuthError } from "../error/better-auth-error";
@@ -93,11 +93,7 @@ export interface TwitterProfile {
[claims: string]: unknown;
}
export interface TwitterOption {
clientId: string;
clientSecret: string;
redirectURI?: string;
}
export interface TwitterOption extends ProviderOptions {}
export const twitter = (options: TwitterOption) => {
const twitterArctic = new Twitter(
@@ -109,7 +105,7 @@ export const twitter = (options: TwitterOption) => {
id: "twitter",
name: "Twitter",
createAuthorizationURL(data) {
const _scopes = data.scopes || ["account_info.read"];
const _scopes = options.scope || data.scopes || ["account_info.read"];
return twitterArctic.createAuthorizationURL(
data.state,
data.codeVerifier,

View File

@@ -1,6 +1,6 @@
import type { User } from "../db/schema";
import type { oAuthProviderList } from "../social-providers";
import type { LiteralString } from "./helper";
import type { oAuthProviderList } from ".";
import type { LiteralString } from "../types/helper";
import { OAuth2Tokens } from "arctic";
export interface OAuthProvider<

View File

@@ -2,6 +2,8 @@ import { OAuth2Tokens } from "arctic";
import type { ProviderOptions } from ".";
import { getBaseURL } from "../utils/base-url";
import { betterFetch } from "@better-fetch/fetch";
import { sha256 } from "@noble/hashes/sha256";
import { base64url } from "oslo/encoding";
export function getRedirectURI(providerId: string, redirectURI?: string) {
return redirectURI || `${getBaseURL()}/callback/${providerId}`;
@@ -42,3 +44,33 @@ export async function validateAuthorizationCode({
const tokens = new OAuth2Tokens(data);
return tokens;
}
export function generateCodeChallenge(codeVerifier: string): string {
const codeChallengeBytes = sha256(new TextEncoder().encode(codeVerifier));
return base64url.encode(codeChallengeBytes, {
includePadding: false,
});
}
export function createAuthorizationURL(
id: string,
options: ProviderOptions,
authorizationEndpoint: string,
state: string,
codeVerifier: string,
scopes: string[],
): URL {
const url = new URL(authorizationEndpoint);
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", options.clientId);
url.searchParams.set("state", state);
url.searchParams.set("scope", scopes.join(" "));
url.searchParams.set(
"redirect_uri",
options.redirectURI || getRedirectURI(id),
);
const codeChallenge = generateCodeChallenge(codeVerifier);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("code_challenge", codeChallenge);
return url;
}

View File

@@ -3,7 +3,7 @@ export type * from "./models";
export type * from "../init";
export type * from "./plugins";
export type * from "./helper";
export type * from "./provider";
export type * from "../social-providers/types";
export type * from "./context";
export type * from "./adapter";
export * from "../client/types";

View File

@@ -1,7 +1,7 @@
import type { Dialect, PostgresPool } from "kysely";
import type { Account, Session, User, Verification } from "../db/schema";
import type { BetterAuthPlugin } from "./plugins";
import type { OAuthProviderList } from "./provider";
import type { OAuthProviderList } from "../social-providers/types";
import type { SocialProviders } from "../social-providers";
import type { RateLimit } from "./models";
import type { Adapter } from "./adapter";