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:
Bereket Engida
2024-10-16 20:45:03 +03:00
committed by GitHub
parent ccf8f0f364
commit 1d77d0c338
9 changed files with 71 additions and 22 deletions

View File

@@ -15,7 +15,7 @@ import { Label } from "@/components/ui/label";
import { PasswordInput } from "@/components/ui/password-input";
import { signIn } from "@/lib/auth-client";
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 { useRouter } from "next/navigation";
import { useState } from "react";
@@ -129,6 +129,28 @@ export default function SignIn() {
>
<DiscordLogoIcon />
</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
variant="outline"
className="w-full gap-2"

View File

@@ -107,5 +107,9 @@ export const auth = betterAuth({
clientId: process.env.MICROSOFT_CLIENT_ID || "",
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID || "",
clientSecret: process.env.TWITCH_CLIENT_SECRET || "",
},
},
});

View File

@@ -25,7 +25,7 @@ export const forgetPassword = createAuthEndpoint(
async (ctx) => {
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
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", {
message: "Reset password isn't enabled",

View File

@@ -39,7 +39,7 @@ export const signInOAuth = createAuthEndpoint(
);
if (!provider) {
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,
},

View File

@@ -8,6 +8,7 @@ export async function createAuthorizationURL({
state,
codeVerifier,
scopes,
claims,
disablePkce,
redirectURI,
}: {
@@ -19,6 +20,7 @@ export async function createAuthorizationURL({
codeVerifier?: string;
scopes: string[];
disablePkce?: boolean;
claims?: string[];
}) {
const url = new URL(authorizationEndpoint);
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", 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;
}

View File

@@ -20,7 +20,11 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
accessTokenExpiresAt: data.expires_at
? new Date((Date.now() + data.expires_in) * 1000)
: undefined,
scopes: data.scope?.split(" ") || [],
scopes: data?.scope
? typeof data.scope === "string"
? data.scope.split(" ")
: data.scope
: [],
idToken: data.id_token,
};
}

View File

@@ -301,8 +301,9 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
}
const state = ctx.query.state;
const {
data: { callbackURL, currentURL, code },
data: { callbackURL, currentURL },
} = parsedState;
const code = ctx.query.code;
const errorURL =
parsedState.data?.currentURL || `${ctx.context.baseURL}/error`;
const storedState = await ctx.getSignedCookie(

View File

@@ -26,11 +26,10 @@ export const facebook = (options: FacebookOptions) => {
return await createAuthorizationURL({
id: "facebook",
options,
authorizationEndpoint: "https://www.facebook.com/v16.0/dialog/oauth",
authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth",
scopes: _scopes,
state,
redirectURI,
codeVerifier,
});
},
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
@@ -39,12 +38,12 @@ export const facebook = (options: FacebookOptions) => {
codeVerifier,
redirectURI: options.redirectURI || redirectURI,
options,
tokenEndpoint: "https://graph.facebook.com/v16.0/oauth/access_token",
tokenEndpoint: "https://graph.facebook.com/oauth/access_token",
});
},
async getUserInfo(token) {
const { data: profile, error } = await betterFetch<FacebookProfile>(
"https://graph.facebook.com/me",
"https://graph.facebook.com/me?fields=id,name,email,picture",
{
auth: {
type: "Bearer",

View File

@@ -1,5 +1,7 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider, ProviderOptions } from "../oauth2";
import { logger } from "../utils";
import { parseJWT } from "oslo/jwt";
import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
export interface TwitchProfile {
@@ -21,13 +23,15 @@ export interface TwitchProfile {
picture: string;
}
export interface TwitchOptions extends ProviderOptions {}
export interface TwitchOptions extends ProviderOptions {
claims?: string[];
}
export const twitch = (options: TwitchOptions) => {
return {
id: "twitch",
name: "Twitch",
createAuthorizationURL({ state, scopes, redirectURI }) {
const _scopes = options.scope || scopes || ["activity:write", "read"];
const _scopes = options.scope || scopes || ["user:read:email", "openid"];
return createAuthorizationURL({
id: "twitch",
redirectURI,
@@ -35,6 +39,11 @@ export const twitch = (options: TwitchOptions) => {
authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
scopes: _scopes,
state,
claims: options.claims || [
"email",
"email_verified",
"preferred_username",
],
});
},
validateAuthorizationCode: async ({ code, redirectURI }) => {
@@ -46,18 +55,12 @@ export const twitch = (options: TwitchOptions) => {
});
},
async getUserInfo(token) {
const { data: profile, error } = await betterFetch<TwitchProfile>(
"https://api.twitch.tv/helix/users",
{
method: "GET",
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
},
);
if (error) {
const idToken = token.idToken;
if (!idToken) {
logger.error("No idToken found in token");
return null;
}
const profile = parseJWT(idToken)?.payload as TwitchProfile;
return {
user: {
id: profile.sub,