@edgedb/auth-sveltekit
Advanced tools
Comparing version 0.3.0-canary.20240510T202738 to 0.3.0-canary.20240521T175926
import type { BuiltinOAuthProviderNames } from "@edgedb/auth-core"; | ||
import { WebAuthnClient } from "@edgedb/auth-core/webauthn"; | ||
export interface AuthOptions { | ||
@@ -8,4 +9,5 @@ baseUrl: string; | ||
passwordResetPath?: string; | ||
magicLinkFailurePath?: string; | ||
} | ||
type OptionalOptions = "passwordResetPath"; | ||
type OptionalOptions = "passwordResetPath" | "magicLinkFailurePath"; | ||
export type AuthConfig = Required<Omit<AuthOptions, OptionalOptions>> & Pick<AuthOptions, OptionalOptions> & { | ||
@@ -20,2 +22,3 @@ authRoute: string; | ||
passwordResetPath: string | undefined; | ||
magicLinkFailurePath: string | undefined; | ||
authRoute: string; | ||
@@ -27,2 +30,3 @@ }; | ||
protected readonly isSecure: boolean; | ||
readonly webAuthnClient: WebAuthnClient; | ||
/** @internal */ | ||
@@ -29,0 +33,0 @@ constructor(options: AuthOptions); |
@@ -0,1 +1,2 @@ | ||
import { WebAuthnClient } from "@edgedb/auth-core/webauthn"; | ||
export function getConfig(options) { | ||
@@ -10,2 +11,3 @@ const baseUrl = options.baseUrl.replace(/\/$/, ""); | ||
passwordResetPath: options.passwordResetPath, | ||
magicLinkFailurePath: options.magicLinkFailurePath, | ||
authRoute: `${baseUrl}/${authRoutesPath}`, | ||
@@ -20,2 +22,3 @@ }; | ||
isSecure; | ||
webAuthnClient; | ||
/** @internal */ | ||
@@ -25,2 +28,9 @@ constructor(options) { | ||
this.isSecure = this.config.baseUrl.startsWith("https"); | ||
this.webAuthnClient = new WebAuthnClient({ | ||
signupOptionsUrl: `${this.config.authRoute}/webauthn/signup/options`, | ||
signupUrl: `${this.config.authRoute}/webauthn/signup`, | ||
signinOptionsUrl: `${this.config.authRoute}/webauthn/signin/options`, | ||
signinUrl: `${this.config.authRoute}/webauthn/signin`, | ||
verifyUrl: `${this.config.authRoute}/webauthn/verify`, | ||
}); | ||
} | ||
@@ -27,0 +37,0 @@ getOAuthUrl(providerName) { |
import { type RequestEvent, type Handle } from "@sveltejs/kit"; | ||
import type { Client } from "edgedb"; | ||
import { Auth, type BuiltinOAuthProviderNames, type TokenData, type emailPasswordProviderName } from "@edgedb/auth-core"; | ||
import { Auth, type BuiltinOAuthProviderNames, type TokenData, type emailPasswordProviderName, type AuthenticationResponseJSON, type RegistrationResponseJSON } from "@edgedb/auth-core"; | ||
import { ClientAuth, type AuthOptions } from "./client.js"; | ||
@@ -37,2 +37,6 @@ export * from "@edgedb/auth-core/errors"; | ||
}>) => Promise<never>; | ||
onMagicLinkCallback(params: ParamsOrError<{ | ||
tokenData: TokenData; | ||
isSignUp: boolean; | ||
}>): Promise<Response>; | ||
onSignout?: () => Promise<never>; | ||
@@ -49,2 +53,6 @@ } | ||
private _session; | ||
private setVerifierCookie; | ||
private setAuthCookie; | ||
private deleteVerifierCookie; | ||
private deleteAuthCookie; | ||
get session(): AuthSession; | ||
@@ -54,4 +62,2 @@ /** @internal */ | ||
isPasswordResetTokenValid(resetToken: string): boolean | null; | ||
private setVerifierCookie; | ||
private setAuthCookie; | ||
getProvidersInfo(): Promise<{ | ||
@@ -91,2 +97,22 @@ oauth: { | ||
}>; | ||
magicLinkSignUp(data: { | ||
email: string; | ||
} | FormData): Promise<void>; | ||
magicLinkSend(data: { | ||
email: string; | ||
} | FormData): Promise<void>; | ||
webAuthnSignIn(data: { | ||
email: string; | ||
assertion: AuthenticationResponseJSON; | ||
}): Promise<{ | ||
tokenData: TokenData; | ||
}>; | ||
webAuthnSignUp(data: { | ||
email: string; | ||
credentials: RegistrationResponseJSON; | ||
verify_url: string; | ||
user_handle: string; | ||
}): Promise<{ | ||
tokenData: TokenData | null; | ||
}>; | ||
signout(): Promise<void>; | ||
@@ -93,0 +119,0 @@ } |
import { redirect, } from "@sveltejs/kit"; | ||
import { Auth, builtinOAuthProviderNames, BackendError, ConfigurationError, PKCEError, InvalidDataError, OAuthProviderFailureError, EdgeDBAuthError, } from "@edgedb/auth-core"; | ||
import { Auth, builtinOAuthProviderNames, BackendError, ConfigurationError, PKCEError, InvalidDataError, OAuthProviderFailureError, EdgeDBAuthError, MagicLinkFailureError, } from "@edgedb/auth-core"; | ||
import { ClientAuth, getConfig, } from "./client.js"; | ||
@@ -23,2 +23,27 @@ export * from "@edgedb/auth-core/errors"; | ||
} | ||
const BASE_COOKIE_CONFIG = { | ||
httpOnly: true, | ||
sameSite: "lax", | ||
path: "/", | ||
}; | ||
function setVerifierCookie(cookies, config, value) { | ||
const expires = new Date(Date.now() + 1000 * 60 * 24 * 7); // in 7 days | ||
cookies.set(config.pkceVerifierCookieName, value, { | ||
...BASE_COOKIE_CONFIG, | ||
expires, | ||
secure: config.baseUrl.startsWith("https"), | ||
}); | ||
} | ||
function setAuthCookie(cookies, config, value) { | ||
cookies.set(config.authCookieName, value, { | ||
...BASE_COOKIE_CONFIG, | ||
expires: Auth.getTokenExpiration(value) || undefined, | ||
secure: config.baseUrl.startsWith("https"), | ||
}); | ||
} | ||
function deleteCookie(cookies, name) { | ||
cookies.set(name, "", { | ||
path: "/", | ||
}); | ||
} | ||
export class ServerRequestAuth extends ClientAuth { | ||
@@ -29,2 +54,14 @@ client; | ||
_session; | ||
setVerifierCookie(verifier) { | ||
setVerifierCookie(this.cookies, this.config, verifier); | ||
} | ||
setAuthCookie(authToken) { | ||
setAuthCookie(this.cookies, this.config, authToken); | ||
} | ||
deleteVerifierCookie() { | ||
deleteCookie(this.cookies, this.config.pkceVerifierCookieName); | ||
} | ||
deleteAuthCookie() { | ||
deleteCookie(this.cookies, this.config.authCookieName); | ||
} | ||
get session() { | ||
@@ -46,22 +83,2 @@ if (!this._session) { | ||
} | ||
setVerifierCookie(verifier) { | ||
const expires = new Date(Date.now() + 1000 * 60 * 24 * 7); // In 7 days | ||
this.cookies.set(this.config.pkceVerifierCookieName, verifier, { | ||
httpOnly: true, | ||
sameSite: "lax", | ||
path: "/", | ||
expires, | ||
secure: this.isSecure, | ||
}); | ||
} | ||
setAuthCookie(authToken) { | ||
const expires = Auth.getTokenExpiration(authToken); | ||
this.cookies.set(this.config.authCookieName, authToken, { | ||
httpOnly: true, | ||
sameSite: "lax", | ||
path: "/", | ||
expires: expires ?? undefined, | ||
secure: this.isSecure, | ||
}); | ||
} | ||
async getProvidersInfo() { | ||
@@ -125,9 +142,40 @@ return (await this.core).getProvidersInfo(); | ||
this.setAuthCookie(tokenData.auth_token); | ||
this.cookies.delete(this.config.pkceVerifierCookieName, { | ||
path: "/", | ||
}); | ||
this.deleteVerifierCookie(); | ||
return { tokenData }; | ||
} | ||
async magicLinkSignUp(data) { | ||
if (!this.config.magicLinkFailurePath) { | ||
throw new ConfigurationError(`'magicLinkFailurePath' option not configured`); | ||
} | ||
const [email] = extractParams(data, ["email"], "email missing"); | ||
const { verifier } = await (await this.core).signupWithMagicLink(email, `${this.config.authRoute}/magiclink/callback?isSignUp=true`, new URL(this.config.magicLinkFailurePath, this.config.baseUrl).toString()); | ||
this.setVerifierCookie(verifier); | ||
} | ||
async magicLinkSend(data) { | ||
if (!this.config.magicLinkFailurePath) { | ||
throw new ConfigurationError(`'magicLinkFailurePath' option not configured`); | ||
} | ||
const [email] = extractParams(data, ["email"], "email missing"); | ||
const { verifier } = await (await this.core).signinWithMagicLink(email, `${this.config.authRoute}/magiclink/callback?isSignUp=true`, new URL(this.config.magicLinkFailurePath, this.config.baseUrl).toString()); | ||
this.setVerifierCookie(verifier); | ||
} | ||
async webAuthnSignIn(data) { | ||
const { email, assertion } = data; | ||
const tokenData = await (await this.core).signinWithWebAuthn(email, assertion); | ||
this.setAuthCookie(tokenData.auth_token); | ||
return { tokenData }; | ||
} | ||
async webAuthnSignUp(data) { | ||
const { email, credentials, verify_url, user_handle } = data; | ||
const result = await (await this.core).signupWithWebAuthn(email, credentials, verify_url, user_handle); | ||
this.setVerifierCookie(result.verifier); | ||
if (result.status === "complete") { | ||
const tokenData = result.tokenData; | ||
this.setAuthCookie(tokenData.auth_token); | ||
return { tokenData }; | ||
} | ||
return { tokenData: null }; | ||
} | ||
async signout() { | ||
this.cookies.delete(this.config.authCookieName, { path: "/" }); | ||
this.deleteAuthCookie(); | ||
} | ||
@@ -184,3 +232,3 @@ } | ||
} | ||
async function handleAuthRoutes({ onOAuthCallback, onBuiltinUICallback, onEmailVerify, onSignout, }, { url, cookies }, core, config) { | ||
async function handleAuthRoutes({ onOAuthCallback, onBuiltinUICallback, onEmailVerify, onMagicLinkCallback, onSignout, }, { url, cookies }, core, config) { | ||
const searchParams = url.searchParams; | ||
@@ -199,7 +247,4 @@ const path = url.pathname.split("/").slice(2).join("/"); | ||
const pkceSession = await core.then((core) => core.createPKCESession()); | ||
cookies.set(config.pkceVerifierCookieName, pkceSession.verifier, { | ||
httpOnly: true, | ||
path: "/", | ||
}); | ||
return redirect(303, pkceSession.getOAuthUrl(provider, redirectUrl, `${redirectUrl}?isSignUp=true`)); | ||
setVerifierCookie(cookies, config, pkceSession.verifier); | ||
return redirect(307, pkceSession.getOAuthUrl(provider, redirectUrl, `${redirectUrl}?isSignUp=true`)); | ||
} | ||
@@ -239,11 +284,4 @@ case "oauth/callback": { | ||
} | ||
cookies.set(config.authCookieName, tokenData.auth_token, { | ||
httpOnly: true, | ||
sameSite: "lax", | ||
path: "/", | ||
}); | ||
cookies.set(config.pkceVerifierCookieName, "", { | ||
maxAge: 0, | ||
path: "/", | ||
}); | ||
setAuthCookie(cookies, config, tokenData.auth_token); | ||
deleteCookie(cookies, config.pkceVerifierCookieName); | ||
return onOAuthCallback({ | ||
@@ -298,11 +336,4 @@ error: null, | ||
} | ||
cookies.set(config.authCookieName, tokenData.auth_token, { | ||
httpOnly: true, | ||
sameSite: "strict", | ||
path: "/", | ||
}); | ||
cookies.set(config.pkceVerifierCookieName, "", { | ||
maxAge: 0, | ||
path: "/", | ||
}); | ||
setAuthCookie(cookies, config, tokenData.auth_token); | ||
deleteCookie(cookies, config.pkceVerifierCookieName); | ||
return onBuiltinUICallback({ | ||
@@ -318,7 +349,4 @@ error: null, | ||
const pkceSession = await core.then((core) => core.createPKCESession()); | ||
cookies.set(config.pkceVerifierCookieName, pkceSession.verifier, { | ||
httpOnly: true, | ||
path: "/", | ||
}); | ||
return redirect(303, path.split("/").pop() === "signup" | ||
deleteCookie(cookies, config.pkceVerifierCookieName); | ||
return redirect(307, path.split("/").pop() === "signup" | ||
? pkceSession.getHostedUISignupUrl() | ||
@@ -354,7 +382,91 @@ : pkceSession.getHostedUISigninUrl()); | ||
} | ||
cookies.set(config.authCookieName, tokenData.auth_token, { | ||
httpOnly: true, | ||
sameSite: "strict", | ||
path: "/", | ||
setAuthCookie(cookies, config, tokenData.auth_token); | ||
return onEmailVerify({ | ||
error: null, | ||
tokenData, | ||
}); | ||
} | ||
case "magiclink/callback": { | ||
if (!onMagicLinkCallback) { | ||
throw new ConfigurationError(`'onMagicLinkCallback' auth route handler not configured`); | ||
} | ||
const error = searchParams.get("error"); | ||
if (error) { | ||
const desc = searchParams.get("error_description"); | ||
return onMagicLinkCallback({ | ||
error: new MagicLinkFailureError(error + (desc ? `: ${desc}` : "")), | ||
}); | ||
} | ||
const code = searchParams.get("code"); | ||
const isSignUp = searchParams.get("isSignUp") === "true"; | ||
const verifier = cookies.get(config.pkceVerifierCookieName); | ||
if (!code) { | ||
return onMagicLinkCallback({ | ||
error: new PKCEError("no pkce code in response"), | ||
}); | ||
} | ||
if (!verifier) { | ||
return onMagicLinkCallback({ | ||
error: new PKCEError("no pkce verifier cookie found"), | ||
}); | ||
} | ||
let tokenData; | ||
try { | ||
tokenData = await (await core).getToken(code, verifier); | ||
} | ||
catch (err) { | ||
return onMagicLinkCallback({ | ||
error: err instanceof Error ? err : new Error(String(err)), | ||
}); | ||
} | ||
setAuthCookie(cookies, config, tokenData.auth_token); | ||
deleteCookie(cookies, config.pkceVerifierCookieName); | ||
return onMagicLinkCallback({ | ||
error: null, | ||
tokenData, | ||
isSignUp, | ||
}); | ||
} | ||
case "webauthn/signup/options": { | ||
const email = searchParams.get("email"); | ||
if (!email) { | ||
throw new InvalidDataError("email missing"); | ||
} | ||
return redirect(307, (await core).getWebAuthnSignupOptionsUrl(email)); | ||
} | ||
case "webauthn/signin/options": { | ||
const email = searchParams.get("email"); | ||
if (!email) { | ||
throw new InvalidDataError("email missing"); | ||
} | ||
return redirect(307, (await core).getWebAuthnSigninOptionsUrl(email)); | ||
} | ||
case "webauthn/verify": { | ||
if (!onEmailVerify) { | ||
throw new ConfigurationError(`'onEmailVerify' auth route handler not configured`); | ||
} | ||
const verificationToken = searchParams.get("verification_token"); | ||
const verifier = cookies.get(config.pkceVerifierCookieName); | ||
if (!verificationToken) { | ||
return onEmailVerify({ | ||
error: new PKCEError("no verification_token in response"), | ||
}); | ||
} | ||
if (!verifier) { | ||
return onEmailVerify({ | ||
error: new PKCEError("no pkce verifier cookie found"), | ||
verificationToken, | ||
}); | ||
} | ||
let tokenData; | ||
try { | ||
tokenData = await (await core).verifyWebAuthnSignup(verificationToken, verifier); | ||
} | ||
catch (err) { | ||
return onEmailVerify({ | ||
error: err instanceof Error ? err : new Error(String(err)), | ||
verificationToken, | ||
}); | ||
} | ||
setAuthCookie(cookies, config, tokenData.auth_token); | ||
return onEmailVerify({ | ||
@@ -369,3 +481,3 @@ error: null, | ||
} | ||
cookies.delete(config.authCookieName, { path: "/" }); | ||
deleteCookie(cookies, config.authCookieName); | ||
return onSignout(); | ||
@@ -372,0 +484,0 @@ } |
{ | ||
"name": "@edgedb/auth-sveltekit", | ||
"description": "Helper library to integrate the EdgeDB Auth extension with Sveltekit.", | ||
"version": "0.3.0-canary.20240510T202738", | ||
"version": "0.3.0-canary.20240521T175926", | ||
"author": "EdgeDB <info@edgedb.com>", | ||
@@ -6,0 +6,0 @@ "type": "module", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
35809
683