/**
*
* :::warning
* `@auth/qwik` is currently experimental. The API _will_ change in the future.
* :::
*
* Qwik Auth is the official Qwik integration for Auth.js.
* It provides a simple way to add authentication to your Qwik app in a few lines of code.
*
* ## Installation
* ```bash npm2yarn
* npm install @auth/qwik
* ```
*
* ## Usage
*
* ### Provider Configuration
*
* Create a `plugin@auth.ts` file in the routes folder
*
* import { QwikAuth$ } from "@auth/qwik"
* import GitHub from "@auth/qwik/providers/github"
*
* export const {
* onRequest, useSession, useSignIn, useSignOut
* } = QwikAuth$(() => ({ providers: [GitHub] }))
*
* ## Signing in and signing out
*
* Sign In via form
*
* import { component$ } from '@builder.io/qwik';
* import { Form } from '@builder.io/qwik-city';
* import { useSignIn } from '~/routes/plugin@auth';
*
* export default component$(() => {
* const signIn = useSignIn();
* return (
*
* );
* });
*
* Sign In via code
*
* import { component$ } from '@builder.io/qwik';
* import { useSignIn } from '~/routes/plugin@auth';
*
* export default component$(() => {
* const signIn = useSignIn();
* return (
*
* );
* });
*
* Sign out via form
*
* import { component$ } from '@builder.io/qwik';
* import { Form } from '@builder.io/qwik-city';
* import { useSignOut } from '~/routes/plugin@auth';
*
* export default component$(() => {
* const signOut = useSignOut();
* return (
*
* );
* });
*
* Sign out via code
*
* import { component$ } from '@builder.io/qwik';
* import { useSignOut } from '~/routes/plugin@auth';
*
* export default component$(() => {
* const signOut = useSignOut();
* return ;
* });
*
* ## Managing the session
*
* import { component$ } from '@builder.io/qwik';
* import { useSession } from '~/routes/plugin@auth';
*
* export default component$(() => {
* const session = useSession();
* return
{session.value?.user?.email}
;
* });
*
* ## Authorization
*
* Session data can be accessed via the route event.sharedMap.
* So a route can be protected and redirect using something like this located in a layout.tsx or page index.tsx:
*
* export const onRequest: RequestHandler = (event) => {
* const session = event.sharedMap.get("session")
* if (!session || new Date(session.expires) < new Date()) {
* throw event.redirect(302, `/auth/signin?redirectTo=${event.url.pathname}`);
* }
* };
*
* ```
*
* @module @auth/qwik
*/
import type { AuthConfig } from "@auth/core"
import {
Auth,
isAuthAction,
skipCSRFCheck,
customFetch as _customFetch,
} from "@auth/core"
import { AuthAction, Session } from "@auth/core/types"
import { implicit$FirstArg, type QRL } from "@builder.io/qwik"
import {
globalAction$,
routeLoader$,
z,
zod$,
type RequestEventCommon,
} from "@builder.io/qwik-city"
import { EnvGetter } from "@builder.io/qwik-city/middleware/request-handler"
import { isServer } from "@builder.io/qwik/build"
import { parseString, splitCookiesString } from "set-cookie-parser"
export const customFetch = isServer ? _customFetch : undefined
export { AuthError, CredentialsSignin } from "@auth/core/errors"
export type {
Account,
DefaultSession,
Profile,
Session,
User,
} from "@auth/core/types"
/** Configure the {@link QwikAuth$} method. */
export interface QwikAuthConfig extends Omit {}
export type GetSessionResult = Promise<{ data: Session | null; cookie: any }>
/** @internal */
export function QwikAuthQrl(
authOptions: QRL<(ev: RequestEventCommon) => QwikAuthConfig>
) {
const useSignIn = globalAction$(
async (
{ providerId, redirectTo: _redirectTo, options, authorizationParams },
req
) => {
if (!isServer) return
const { redirectTo = _redirectTo ?? defaultRedirectTo(req), ...rest } =
options ?? {}
const isCredentials = providerId === "credentials"
const authOpts = await authOptions(req)
setEnvDefaults(req.env, authOpts)
const body = new URLSearchParams({ callbackUrl: redirectTo })
Object.entries(rest).forEach(([key, value]) => {
body.set(key, String(value))
})
const baseSignInUrl = `/auth/${isCredentials ? "callback" : "signin"}${
providerId ? `/${providerId}` : ""
}`
const signInUrl = `${baseSignInUrl}?${new URLSearchParams(
authorizationParams
)}`
const data = await authAction(body, req, signInUrl, authOpts)
if (data.url) {
throw req.redirect(301, data.url)
}
},
zod$({
providerId: z.string().optional(),
redirectTo: z.string().optional(),
options: z
.object({
redirectTo: z.string(),
})
.passthrough()
.partial()
.optional(),
authorizationParams: z
.union([z.string(), z.custom(), z.record(z.string())])
.optional(),
})
)
const useSignOut = globalAction$(
async ({ redirectTo }, req) => {
if (!isServer) return
redirectTo ??= defaultRedirectTo(req)
const authOpts = await authOptions(req)
setEnvDefaults(req.env, authOpts)
const body = new URLSearchParams({ callbackUrl: redirectTo })
await authAction(body, req, `/auth/signout`, authOpts)
},
zod$({
redirectTo: z.string().optional(),
})
)
const useSession = routeLoader$((req) => {
return req.sharedMap.get("session") as Session | null
})
const onRequest = async (req: RequestEventCommon) => {
if (isServer) {
const prefix: string = "/auth"
const action = req.url.pathname
.slice(prefix.length + 1)
.split("/")[0] as AuthAction
const authOpts = await authOptions(req)
setEnvDefaults(req.env, authOpts)
if (isAuthAction(action) && req.url.pathname.startsWith(prefix + "/")) {
const res = (await Auth(req.request, authOpts)) as Response
const cookie = res.headers.get("set-cookie")
if (cookie) {
req.headers.set("set-cookie", cookie)
res.headers.delete("set-cookie")
fixCookies(req)
}
throw req.send(res)
} else {
const { data, cookie } = await getSessionData(req.request, authOpts)
req.sharedMap.set("session", data)
if (cookie) {
req.headers.set("set-cookie", cookie)
fixCookies(req)
}
}
}
}
return { useSignIn, useSignOut, useSession, onRequest }
}
/**
* Initialize Qwik Auth.
*
* @example
* ```ts title="plugin@auth.ts"
* import { QwikAuth } from "@auth/qwik"
* import GitHub from "@auth/qwik/providers/github"
*
* export const {
* onRequest, useSession, useSignIn, useSignOut
* } = QwikAuth$(() => ({ providers: [GitHub] }))
* ```
*/
export const QwikAuth$ = /*#__PURE__*/ implicit$FirstArg(QwikAuthQrl)
async function authAction(
body: URLSearchParams | undefined,
req: RequestEventCommon,
path: string,
authOptions: QwikAuthConfig
) {
if (!isServer) return
const request = new Request(new URL(path, req.request.url), {
method: req.request.method,
headers: req.request.headers,
body: body,
})
request.headers.set("content-type", "application/x-www-form-urlencoded")
const res = (await Auth(request, authOptions)) as Response
const cookies: string[] = []
res.headers.forEach((value, key) => {
if (key === "set-cookie") {
// while browsers would support setting multiple cookies, the fetch implementation does not, so we join them later.
cookies.push(value)
} else if (!req.headers.has(key)) {
req.headers.set(key, value)
}
})
if (cookies.length > 0) {
req.headers.set("set-cookie", cookies.join(", "))
}
fixCookies(req)
try {
return await res.json()
} catch {
return await res.text()
}
}
const fixCookies = (req: RequestEventCommon) => {
req.headers.set("set-cookie", req.headers.get("set-cookie") || "")
const cookie = req.headers.get("set-cookie")
if (cookie) {
req.headers.delete("set-cookie")
splitCookiesString(cookie).forEach((cookie) => {
const { name, value, ...rest } = parseString(cookie)
req.cookie.set(name, value, rest as any)
})
}
}
const defaultRedirectTo = (req: RequestEventCommon) => {
req.url.searchParams.delete("qaction")
return req.url.href
}
async function getSessionData(
req: Request,
options: AuthConfig
): GetSessionResult {
options.secret ??= process.env.AUTH_SECRET
options.trustHost ??= true
const url = new URL("/auth/session", req.url)
const response = (await Auth(
new Request(url, { headers: req.headers }),
options
)) as Response
const { status = 200 } = response
const data = await response.json()
const cookie = response.headers.get("set-cookie")
if (!data || !Object.keys(data).length) {
return { data: null, cookie }
}
if (status === 200) {
return { data, cookie }
}
throw new Error(data.message)
}
export const setEnvDefaults = (env: EnvGetter, config: AuthConfig) => {
if (!isServer) return
config.basePath = "/auth"
if (!config.secret?.length) {
config.secret = []
const secret = env.get("AUTH_SECRET")
if (secret) {
config.secret.push(secret)
}
for (const i of [1, 2, 3]) {
const secret = env.get(`AUTH_SECRET_${i}`)
if (secret) {
config.secret.unshift(secret)
}
}
}
config.redirectProxyUrl ??= env.get("AUTH_REDIRECT_PROXY_URL")
config.trustHost ??= !!(
env.get("AUTH_URL") ??
env.get("AUTH_TRUST_HOST") ??
env.get("VERCEL") ??
env.get("CF_PAGES") ??
env.get("NODE_ENV") !== "production"
)
config.skipCSRFCheck = skipCSRFCheck
config.providers = config.providers.map((p) => {
const finalProvider = typeof p === "function" ? p({}) : p
const ID = finalProvider.id.toUpperCase().replace(/-/g, "_")
if (finalProvider.type === "oauth" || finalProvider.type === "oidc") {
finalProvider.clientId ??= env.get(`AUTH_${ID}_ID`)
finalProvider.clientSecret ??= env.get(`AUTH_${ID}_SECRET`)
if (finalProvider.type === "oidc") {
finalProvider.issuer ??= env.get(`AUTH_${ID}_ISSUER`)
}
} else if (finalProvider.type === "email") {
finalProvider.apiKey ??= env.get(`AUTH_${ID}_KEY`)
}
return finalProvider
})
}