refactor: append scope and google access type

This commit is contained in:
Bereket Engida
2024-10-24 11:31:24 +03:00
parent 36b4c729af
commit 3f09cb072f
17 changed files with 64 additions and 42 deletions

View File

@@ -10,7 +10,6 @@ import {
username, username,
} from "better-auth/plugins"; } from "better-auth/plugins";
import { reactInvitationEmail } from "./email/invitation"; import { reactInvitationEmail } from "./email/invitation";
import { LibsqlDialect } from "@libsql/kysely-libsql";
import { reactResetPasswordEmail } from "./email/rest-password"; import { reactResetPasswordEmail } from "./email/rest-password";
import { resend } from "./email/resend"; import { resend } from "./email/resend";
@@ -61,6 +60,7 @@ export const auth = betterAuth({
google: { google: {
clientId: process.env.GOOGLE_CLIENT_ID || "", clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
accessType: "offline",
}, },
discord: { discord: {
clientId: process.env.DISCORD_CLIENT_ID || "", clientId: process.env.DISCORD_CLIENT_ID || "",

View File

@@ -4,7 +4,6 @@ import { generateId } from "../../utils/id";
import { parseState } from "../../oauth2/state"; import { parseState } from "../../oauth2/state";
import { createAuthEndpoint } from "../call"; import { createAuthEndpoint } from "../call";
import { HIDE_METADATA } from "../../utils/hide-metadata"; import { HIDE_METADATA } from "../../utils/hide-metadata";
import { getAccountTokens } from "../../oauth2/get-account";
import { setSessionCookie } from "../../cookies"; import { setSessionCookie } from "../../cookies";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import type { OAuth2Tokens } from "../../oauth2"; import type { OAuth2Tokens } from "../../oauth2";
@@ -160,7 +159,10 @@ export const callbackOAuth = createAuthEndpoint(
accountId: user.id.toString(), accountId: user.id.toString(),
id: `${provider.id}:${user.id}`, id: `${provider.id}:${user.id}`,
userId: dbUser.user.id, userId: dbUser.user.id,
...getAccountTokens(tokens), accessToken: tokens.accessToken,
idToken: tokens.idToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.accessTokenExpiresAt,
}); });
} catch (e) { } catch (e) {
logger.error("Unable to link account", e); logger.error("Unable to link account", e);
@@ -168,7 +170,10 @@ export const callbackOAuth = createAuthEndpoint(
} }
} else { } else {
await c.context.internalAdapter.updateAccount(hasBeenLinked.id, { await c.context.internalAdapter.updateAccount(hasBeenLinked.id, {
...getAccountTokens(tokens), accessToken: tokens.accessToken,
idToken: tokens.idToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.accessTokenExpiresAt,
}); });
} }
} else { } else {
@@ -180,7 +185,10 @@ export const callbackOAuth = createAuthEndpoint(
emailVerified, emailVerified,
}, },
{ {
...getAccountTokens(tokens), accessToken: tokens.accessToken,
idToken: tokens.idToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.accessTokenExpiresAt,
providerId: provider.id, providerId: provider.id,
accountId: user.id.toString(), accountId: user.id.toString(),
}, },

View File

@@ -1,15 +0,0 @@
import type { OAuth2Tokens } from ".";
export function getAccountTokens(tokens: OAuth2Tokens) {
const accessToken = tokens.accessToken;
let refreshToken = tokens.refreshToken;
let accessTokenExpiresAt = undefined;
try {
accessTokenExpiresAt = tokens.accessTokenExpiresAt;
} catch {}
return {
accessToken,
refreshToken,
expiresAt: accessTokenExpiresAt,
};
}

View File

@@ -3,4 +3,3 @@ export * from "./validate-authorization-code";
export * from "./utils"; export * from "./utils";
export * from "./state"; export * from "./state";
export * from "./types"; export * from "./types";
export * from "./get-account";

View File

@@ -1,6 +1,7 @@
import { sha256 } from "oslo/crypto"; import { sha256 } from "oslo/crypto";
import { base64url } from "oslo/encoding"; import { base64url } from "oslo/encoding";
import type { OAuth2Tokens } from "./types"; import type { OAuth2Tokens } from "./types";
import { getDate } from "../utils/date";
export async function generateCodeChallenge(codeVerifier: string) { export async function generateCodeChallenge(codeVerifier: string) {
const codeChallengeBytes = await sha256( const codeChallengeBytes = await sha256(
@@ -16,8 +17,8 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
tokenType: data.token_type, tokenType: data.token_type,
accessToken: data.access_token, accessToken: data.access_token,
refreshToken: data.refresh_token, refreshToken: data.refresh_token,
accessTokenExpiresAt: data.expires_at accessTokenExpiresAt: data.expires_in
? new Date((Date.now() + data.expires_in) * 1000) ? getDate(data.expires_in, "sec")
: undefined, : undefined,
scopes: data?.scope scopes: data?.scope
? typeof data.scope === "string" ? typeof data.scope === "string"

View File

@@ -6,11 +6,9 @@ import { betterFetch } from "@better-fetch/fetch";
import { generateState, parseState } from "../../oauth2/state"; import { generateState, parseState } from "../../oauth2/state";
import { generateCodeVerifier } from "oslo/oauth2"; import { generateCodeVerifier } from "oslo/oauth2";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { parseJWT } from "oslo/jwt"; import { parseJWT } from "oslo/jwt";
import { userSchema } from "../../db/schema"; import { userSchema } from "../../db/schema";
import { generateId } from "../../utils/id"; import { generateId } from "../../utils/id";
import { getAccountTokens } from "../../oauth2/get-account";
import { setSessionCookie } from "../../cookies"; import { setSessionCookie } from "../../cookies";
import { redirectURLMiddleware } from "../../api/middlewares/redirect"; import { redirectURLMiddleware } from "../../api/middlewares/redirect";
import { import {
@@ -421,20 +419,36 @@ export const genericOAuth = (options: GenericOAuthOptions) => {
accountId: user.data.id, accountId: user.data.id,
id: `${provider.providerId}:${user.data.id}`, id: `${provider.providerId}:${user.data.id}`,
userId: dbUser.user.id, userId: dbUser.user.id,
...getAccountTokens(tokens), accessToken: tokens.accessToken,
idToken: tokens.idToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.accessTokenExpiresAt,
}); });
} catch (e) { } catch (e) {
console.log(e); console.log(e);
throw ctx.redirect(`${errorURL}?error=failed_linking_account`); throw ctx.redirect(`${errorURL}?error=failed_linking_account`);
} }
} else {
await ctx.context.internalAdapter.updateAccount(
hasBeenLinked.id,
{
accessToken: tokens.accessToken,
idToken: tokens.idToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.accessTokenExpiresAt,
},
);
} }
} else { } else {
try { try {
await ctx.context.internalAdapter.createOAuthUser(user.data, { await ctx.context.internalAdapter.createOAuthUser(user.data, {
...getAccountTokens(tokens),
id: `${provider.providerId}:${user.data.id}`, id: `${provider.providerId}:${user.data.id}`,
providerId: provider.providerId, providerId: provider.providerId,
accountId: user.data.id, accountId: user.data.id,
accessToken: tokens.accessToken,
idToken: tokens.idToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.accessTokenExpiresAt,
}); });
} catch (e) { } catch (e) {
const url = new URL(errorURL); const url = new URL(errorURL);

View File

@@ -52,7 +52,8 @@ export const apple = (options: AppleOptions) => {
id: "apple", id: "apple",
name: "Apple", name: "Apple",
createAuthorizationURL({ state, scopes, redirectURI }) { createAuthorizationURL({ state, scopes, redirectURI }) {
const _scope = options.scope || scopes || ["email", "name", "openid"]; const _scope = scopes || ["email", "name", "openid"];
options.scope && _scope.push(...options.scope);
return new URL( return new URL(
`https://appleid.apple.com/auth/authorize?client_id=${ `https://appleid.apple.com/auth/authorize?client_id=${
options.clientId options.clientId

View File

@@ -81,7 +81,8 @@ export const discord = (options: DiscordOptions) => {
id: "discord", id: "discord",
name: "Discord", name: "Discord",
createAuthorizationURL({ state, scopes, redirectURI }) { createAuthorizationURL({ state, scopes, redirectURI }) {
const _scopes = options.scope || scopes || ["identify", "email"]; const _scopes = scopes || ["identify", "email"];
options.scope && _scopes.push(...options.scope);
return new URL( return new URL(
`https://discord.com/api/oauth2/authorize?scope=${_scopes.join( `https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
"+", "+",

View File

@@ -30,7 +30,8 @@ export const dropbox = (options: DropboxOptions) => {
codeVerifier, codeVerifier,
redirectURI, redirectURI,
}) => { }) => {
const _scopes = options.scope || scopes || ["account_info.read"]; const _scopes = scopes || ["account_info.read"];
options.scope && _scopes.push(...options.scope);
return await createAuthorizationURL({ return await createAuthorizationURL({
id: "dropbox", id: "dropbox",
options, options,

View File

@@ -22,7 +22,8 @@ export const facebook = (options: FacebookOptions) => {
id: "facebook", id: "facebook",
name: "Facebook", name: "Facebook",
async createAuthorizationURL({ state, scopes, redirectURI }) { async createAuthorizationURL({ state, scopes, redirectURI }) {
const _scopes = options.scope || scopes || ["email", "public_profile"]; const _scopes = scopes || ["email", "public_profile"];
options.scope && _scopes.push(...options.scope);
return await createAuthorizationURL({ return await createAuthorizationURL({
id: "facebook", id: "facebook",
options, options,

View File

@@ -58,7 +58,8 @@ export const github = (options: GithubOptions) => {
id: "github", id: "github",
name: "GitHub", name: "GitHub",
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
const _scopes = options.scope || scopes || ["user:email"]; const _scopes = scopes || ["user:email"];
options.scope && _scopes.push(...options.scope);
return createAuthorizationURL({ return createAuthorizationURL({
id: "github", id: "github",
options, options,

View File

@@ -31,13 +31,15 @@ export interface GoogleProfile {
sub: string; sub: string;
} }
export interface GoogleOptions extends ProviderOptions {} export interface GoogleOptions extends ProviderOptions {
accessType?: "offline" | "online";
}
export const google = (options: GoogleOptions) => { export const google = (options: GoogleOptions) => {
return { return {
id: "google", id: "google",
name: "Google", name: "Google",
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
if (!options.clientId || !options.clientSecret) { if (!options.clientId || !options.clientSecret) {
logger.error( logger.error(
"Client Id and Client Secret is required for Google. Make sure to provide them in the options.", "Client Id and Client Secret is required for Google. Make sure to provide them in the options.",
@@ -47,9 +49,10 @@ export const google = (options: GoogleOptions) => {
if (!codeVerifier) { if (!codeVerifier) {
throw new BetterAuthError("codeVerifier is required for Google"); throw new BetterAuthError("codeVerifier is required for Google");
} }
const _scopes = options.scope || scopes || ["email", "profile"]; const _scopes = scopes || ["email", "profile", "openid"];
options.scope && _scopes.push(...options.scope);
const url = createAuthorizationURL({ const url = await createAuthorizationURL({
id: "google", id: "google",
options, options,
authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth", authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
@@ -58,6 +61,8 @@ export const google = (options: GoogleOptions) => {
codeVerifier, codeVerifier,
redirectURI, redirectURI,
}); });
options.accessType &&
url.searchParams.set("access_type", options.accessType);
return url; return url;
}, },
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {

View File

@@ -27,7 +27,8 @@ export const linkedin = (options: LinkedInOptions) => {
id: "linkedin", id: "linkedin",
name: "Linkedin", name: "Linkedin",
createAuthorizationURL: async ({ state, scopes, redirectURI }) => { createAuthorizationURL: async ({ state, scopes, redirectURI }) => {
const _scopes = options.scope || scopes || ["profile", "email", "openid"]; const _scopes = scopes || ["profile", "email", "openid"];
options.scope && _scopes.push(...options.scope);
return await createAuthorizationURL({ return await createAuthorizationURL({
id: "linkedin", id: "linkedin",
options, options,

View File

@@ -37,8 +37,9 @@ export const microsoft = (options: MicrosoftOptions) => {
id: "microsoft", id: "microsoft",
name: "Microsoft EntraID", name: "Microsoft EntraID",
createAuthorizationURL(data) { createAuthorizationURL(data) {
const scopes = options.scope || const scopes = data.scopes || ["openid", "profile", "email", "User.Read"];
data.scopes || ["openid", "profile", "email", "User.Read"]; options.scope && scopes.push(...options.scope);
return createAuthorizationURL({ return createAuthorizationURL({
id: "microsoft", id: "microsoft",
options, options,

View File

@@ -18,7 +18,8 @@ export const spotify = (options: SpotifyOptions) => {
id: "spotify", id: "spotify",
name: "Spotify", name: "Spotify",
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
const _scopes = options.scope || scopes || ["user-read-email"]; const _scopes = scopes || ["user-read-email"];
options.scope && _scopes.push(...options.scope);
return createAuthorizationURL({ return createAuthorizationURL({
id: "spotify", id: "spotify",
options, options,

View File

@@ -31,7 +31,8 @@ export const twitch = (options: TwitchOptions) => {
id: "twitch", id: "twitch",
name: "Twitch", name: "Twitch",
createAuthorizationURL({ state, scopes, redirectURI }) { createAuthorizationURL({ state, scopes, redirectURI }) {
const _scopes = options.scope || scopes || ["user:read:email", "openid"]; const _scopes = scopes || ["user:read:email", "openid"];
options.scope && _scopes.push(...options.scope);
return createAuthorizationURL({ return createAuthorizationURL({
id: "twitch", id: "twitch",
redirectURI, redirectURI,

View File

@@ -98,7 +98,8 @@ export const twitter = (options: TwitterOption) => {
id: "twitter", id: "twitter",
name: "Twitter", name: "Twitter",
createAuthorizationURL(data) { createAuthorizationURL(data) {
const _scopes = options.scope || data.scopes || ["account_info.read"]; const _scopes = data.scopes || ["account_info.read"];
options.scope && _scopes.push(...options.scope);
return createAuthorizationURL({ return createAuthorizationURL({
id: "twitter", id: "twitter",
options, options,