mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 04:19:26 +00:00
fix: OAuth flow errors (#189)
* fix: remove codeverifier * chore: release v0.4.10-beta.6 * fix: add claims and proper scopes for twitch * chore: release v0.4.10-beta.6 * chore: release v0.4.10-beta.7 * fix: generic oauth should use the code from qparam * chore: release v0.4.10-beta.8 * fix: scope failing to merge * chore: release v0.4.10-beta.9 * fix: twitch get profile * chore: release v0.4.10-beta.10 * fix:lint
This commit is contained in:
@@ -15,7 +15,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { PasswordInput } from "@/components/ui/password-input";
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
import { signIn } from "@/lib/auth-client";
|
import { signIn } from "@/lib/auth-client";
|
||||||
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||||
import { Key, Loader2 } from "lucide-react";
|
import { Key, Loader2, TwitchIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -129,6 +129,28 @@ export default function SignIn() {
|
|||||||
>
|
>
|
||||||
<DiscordLogoIcon />
|
<DiscordLogoIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full gap-2"
|
||||||
|
onClick={async () => {
|
||||||
|
await signIn.social({
|
||||||
|
provider: "twitch",
|
||||||
|
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="M11.64 5.93h1.43v4.28h-1.43m3.93-4.28H17v4.28h-1.43M7 2L3.43 5.57v12.86h4.28V22l3.58-3.57h2.85L20.57 12V2m-1.43 9.29l-2.85 2.85h-2.86l-2.5 2.5v-2.5H7.71V3.43h11.43Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full gap-2"
|
className="w-full gap-2"
|
||||||
|
|||||||
@@ -107,5 +107,9 @@ export const auth = betterAuth({
|
|||||||
clientId: process.env.MICROSOFT_CLIENT_ID || "",
|
clientId: process.env.MICROSOFT_CLIENT_ID || "",
|
||||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
|
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
|
||||||
},
|
},
|
||||||
|
twitch: {
|
||||||
|
clientId: process.env.TWITCH_CLIENT_ID || "",
|
||||||
|
clientSecret: process.env.TWITCH_CLIENT_SECRET || "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const forgetPassword = createAuthEndpoint(
|
|||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
|
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
|
||||||
ctx.context.logger.error(
|
ctx.context.logger.error(
|
||||||
"Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function to your auth config!",
|
"Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function in your auth config!",
|
||||||
);
|
);
|
||||||
throw new APIError("BAD_REQUEST", {
|
throw new APIError("BAD_REQUEST", {
|
||||||
message: "Reset password isn't enabled",
|
message: "Reset password isn't enabled",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const signInOAuth = createAuthEndpoint(
|
|||||||
);
|
);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
c.context.logger.error(
|
c.context.logger.error(
|
||||||
"Provider not found. Make sure to add the provider to your auth config",
|
"Provider not found. Make sure to add the provider in your auth config",
|
||||||
{
|
{
|
||||||
provider: c.body.provider,
|
provider: c.body.provider,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export async function createAuthorizationURL({
|
|||||||
state,
|
state,
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
scopes,
|
scopes,
|
||||||
|
claims,
|
||||||
disablePkce,
|
disablePkce,
|
||||||
redirectURI,
|
redirectURI,
|
||||||
}: {
|
}: {
|
||||||
@@ -19,6 +20,7 @@ export async function createAuthorizationURL({
|
|||||||
codeVerifier?: string;
|
codeVerifier?: string;
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
disablePkce?: boolean;
|
disablePkce?: boolean;
|
||||||
|
claims?: string[];
|
||||||
}) {
|
}) {
|
||||||
const url = new URL(authorizationEndpoint);
|
const url = new URL(authorizationEndpoint);
|
||||||
url.searchParams.set("response_type", "code");
|
url.searchParams.set("response_type", "code");
|
||||||
@@ -32,6 +34,20 @@ export async function createAuthorizationURL({
|
|||||||
url.searchParams.set("code_challenge_method", "S256");
|
url.searchParams.set("code_challenge_method", "S256");
|
||||||
url.searchParams.set("code_challenge", codeChallenge);
|
url.searchParams.set("code_challenge", codeChallenge);
|
||||||
}
|
}
|
||||||
|
if (claims) {
|
||||||
|
const claimsObj = claims.reduce(
|
||||||
|
(acc, claim) => {
|
||||||
|
acc[claim] = null;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, null>,
|
||||||
|
);
|
||||||
|
url.searchParams.set(
|
||||||
|
"claims",
|
||||||
|
JSON.stringify({
|
||||||
|
id_token: { email: null, email_verified: null, ...claimsObj },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|||||||
accessTokenExpiresAt: data.expires_at
|
accessTokenExpiresAt: data.expires_at
|
||||||
? new Date((Date.now() + data.expires_in) * 1000)
|
? new Date((Date.now() + data.expires_in) * 1000)
|
||||||
: undefined,
|
: undefined,
|
||||||
scopes: data.scope?.split(" ") || [],
|
scopes: data?.scope
|
||||||
|
? typeof data.scope === "string"
|
||||||
|
? data.scope.split(" ")
|
||||||
|
: data.scope
|
||||||
|
: [],
|
||||||
idToken: data.id_token,
|
idToken: data.id_token,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,8 +301,9 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
|
|||||||
}
|
}
|
||||||
const state = ctx.query.state;
|
const state = ctx.query.state;
|
||||||
const {
|
const {
|
||||||
data: { callbackURL, currentURL, code },
|
data: { callbackURL, currentURL },
|
||||||
} = parsedState;
|
} = parsedState;
|
||||||
|
const code = ctx.query.code;
|
||||||
const errorURL =
|
const errorURL =
|
||||||
parsedState.data?.currentURL || `${ctx.context.baseURL}/error`;
|
parsedState.data?.currentURL || `${ctx.context.baseURL}/error`;
|
||||||
const storedState = await ctx.getSignedCookie(
|
const storedState = await ctx.getSignedCookie(
|
||||||
|
|||||||
@@ -26,11 +26,10 @@ export const facebook = (options: FacebookOptions) => {
|
|||||||
return await createAuthorizationURL({
|
return await createAuthorizationURL({
|
||||||
id: "facebook",
|
id: "facebook",
|
||||||
options,
|
options,
|
||||||
authorizationEndpoint: "https://www.facebook.com/v16.0/dialog/oauth",
|
authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth",
|
||||||
scopes: _scopes,
|
scopes: _scopes,
|
||||||
state,
|
state,
|
||||||
redirectURI,
|
redirectURI,
|
||||||
codeVerifier,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
||||||
@@ -39,12 +38,12 @@ export const facebook = (options: FacebookOptions) => {
|
|||||||
codeVerifier,
|
codeVerifier,
|
||||||
redirectURI: options.redirectURI || redirectURI,
|
redirectURI: options.redirectURI || redirectURI,
|
||||||
options,
|
options,
|
||||||
tokenEndpoint: "https://graph.facebook.com/v16.0/oauth/access_token",
|
tokenEndpoint: "https://graph.facebook.com/oauth/access_token",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async getUserInfo(token) {
|
async getUserInfo(token) {
|
||||||
const { data: profile, error } = await betterFetch<FacebookProfile>(
|
const { data: profile, error } = await betterFetch<FacebookProfile>(
|
||||||
"https://graph.facebook.com/me",
|
"https://graph.facebook.com/me?fields=id,name,email,picture",
|
||||||
{
|
{
|
||||||
auth: {
|
auth: {
|
||||||
type: "Bearer",
|
type: "Bearer",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { betterFetch } from "@better-fetch/fetch";
|
import { betterFetch } from "@better-fetch/fetch";
|
||||||
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
||||||
|
import { logger } from "../utils";
|
||||||
|
import { parseJWT } from "oslo/jwt";
|
||||||
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
|
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
|
||||||
|
|
||||||
export interface TwitchProfile {
|
export interface TwitchProfile {
|
||||||
@@ -21,13 +23,15 @@ export interface TwitchProfile {
|
|||||||
picture: string;
|
picture: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TwitchOptions extends ProviderOptions {}
|
export interface TwitchOptions extends ProviderOptions {
|
||||||
|
claims?: string[];
|
||||||
|
}
|
||||||
export const twitch = (options: TwitchOptions) => {
|
export const twitch = (options: TwitchOptions) => {
|
||||||
return {
|
return {
|
||||||
id: "twitch",
|
id: "twitch",
|
||||||
name: "Twitch",
|
name: "Twitch",
|
||||||
createAuthorizationURL({ state, scopes, redirectURI }) {
|
createAuthorizationURL({ state, scopes, redirectURI }) {
|
||||||
const _scopes = options.scope || scopes || ["activity:write", "read"];
|
const _scopes = options.scope || scopes || ["user:read:email", "openid"];
|
||||||
return createAuthorizationURL({
|
return createAuthorizationURL({
|
||||||
id: "twitch",
|
id: "twitch",
|
||||||
redirectURI,
|
redirectURI,
|
||||||
@@ -35,6 +39,11 @@ export const twitch = (options: TwitchOptions) => {
|
|||||||
authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
|
authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
|
||||||
scopes: _scopes,
|
scopes: _scopes,
|
||||||
state,
|
state,
|
||||||
|
claims: options.claims || [
|
||||||
|
"email",
|
||||||
|
"email_verified",
|
||||||
|
"preferred_username",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
||||||
@@ -46,18 +55,12 @@ export const twitch = (options: TwitchOptions) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
async getUserInfo(token) {
|
async getUserInfo(token) {
|
||||||
const { data: profile, error } = await betterFetch<TwitchProfile>(
|
const idToken = token.idToken;
|
||||||
"https://api.twitch.tv/helix/users",
|
if (!idToken) {
|
||||||
{
|
logger.error("No idToken found in token");
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token.accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (error) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const profile = parseJWT(idToken)?.payload as TwitchProfile;
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: profile.sub,
|
id: profile.sub,
|
||||||
|
|||||||
Reference in New Issue
Block a user