mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 20:27:44 +00:00
feat: microsoft sso
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
55
docs/content/docs/authentication/microsoft.mdx
Normal file
55
docs/content/docs/authentication/microsoft.mdx
Normal 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
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
"+",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
103
packages/better-auth/src/social-providers/microsoft-entra-id.ts
Normal file
103
packages/better-auth/src/social-providers/microsoft-entra-id.ts
Normal 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;
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user