cloudflare-auth
Advanced tools
Comparing version 1.3.1 to 1.4.0
@@ -1,4 +0,3 @@ | ||
import { authConfig } from 'lib/constants'; | ||
import { middlewareGuard } from 'cloudflare-auth'; | ||
export const onRequest = [middlewareGuard(authConfig)]; | ||
export const onRequest = [middlewareGuard]; |
import { html, htmlResponse } from 'cloudflare-htmx'; | ||
import { getJWTPayload, logout } from 'cloudflare-auth'; | ||
import { authConfig } from '@lib/constants'; | ||
import { getJWTPayload, logout, Env } from 'cloudflare-auth'; | ||
export const onRequestPost: PagesFunction = async ({ request }) => { | ||
return logout(authConfig); | ||
export const onRequestPost: PagesFunction<Env> = async ({ env }) => { | ||
return logout(env, '/'); | ||
}; | ||
export const onRequestGet: PagesFunction = async ({ request }) => { | ||
const payload = await getJWTPayload(authConfig, request); | ||
export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => { | ||
const payload = await getJWTPayload(request, env); | ||
return htmlResponse( | ||
@@ -12,0 +11,0 @@ html` |
import { Env, loginWithToken } from 'cloudflare-auth'; | ||
import Toast from '@components/Toast'; | ||
import { html, htmlResponse } from 'cloudflare-htmx'; | ||
import { authConfig } from '@lib/constants'; | ||
@@ -14,3 +13,3 @@ export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => { | ||
try { | ||
const magicLink = await loginWithToken(email, env, url.origin, authConfig); | ||
const magicLink = await loginWithToken(email, env, url.origin, true); | ||
return htmlResponse(Toast(html` <a class="link text-primary" href="${magicLink}">Click here to login</a> `, 'alert-success', false)); | ||
@@ -17,0 +16,0 @@ } catch (e) { |
@@ -1,7 +0,6 @@ | ||
import { authConfig } from '@lib/constants'; | ||
import { isAuthorised, Env } from 'cloudflare-auth'; | ||
import { html, htmlResponse } from 'cloudflare-htmx'; | ||
export const onRequestGet: PagesFunction<Env> = async ({ request }) => { | ||
const authorised = await isAuthorised(authConfig, request); | ||
export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => { | ||
const authorised = await isAuthorised(request, env); | ||
if (authorised) { | ||
@@ -8,0 +7,0 @@ return htmlResponse(html`<h1 class="text-3xl text-primary">You are authorised!</h1>`); |
@@ -1,2 +0,1 @@ | ||
import { authConfig } from '@lib/constants'; | ||
import { verify, Env } from 'cloudflare-auth'; | ||
@@ -8,3 +7,3 @@ | ||
try { | ||
return await verify(token!, env, authConfig); | ||
return await verify(token!, env, '/dash'); | ||
} catch { | ||
@@ -11,0 +10,0 @@ return Response.redirect(url.origin, 301); |
@@ -9,3 +9,4 @@ { | ||
"tailwind": "tailwindcss build -i ./tailwind.css -o ./static/css/output.css --watch", | ||
"db:init": "wrangler d1 execute auth --local --file=./schema.sql" | ||
"db:init:local": "wrangler d1 execute auth --local --file=./schema.sql", | ||
"db:init": "wrangler d1 execute auth --file=./schema.sql" | ||
}, | ||
@@ -12,0 +13,0 @@ "devDependencies": { |
@@ -1,4 +0,2 @@ | ||
import { AuthConfig } from 'cloudflare-auth'; | ||
export const authConfig: AuthConfig = { | ||
export const authConfig = { | ||
secretKey: 'this_is_your_secretKey', | ||
@@ -5,0 +3,0 @@ issuer: 'urn:continuata:issuer', |
{ | ||
"name": "cloudflare-auth", | ||
"version": "1.3.1", | ||
"version": "1.4.0", | ||
"description": "JWT implementation for Cloudflare using D1", | ||
@@ -5,0 +5,0 @@ "main": "src/index.ts", |
@@ -16,5 +16,3 @@ # cloudflare-auth - Simple Auth for Cloudflare Pages | ||
import { AuthConfig } from 'cloudflare-auth'; | ||
export const authConfig: AuthConfig = { | ||
export const authConfig = { | ||
secretKey: 'this_is_your_secretKey', | ||
@@ -21,0 +19,0 @@ issuer: 'urn:continuata:issuer', |
import { Env } from './interfaces'; | ||
import type { AuthConfig } from './interfaces'; | ||
@@ -38,4 +37,3 @@ export const sendEmail = async (urlOrigin: string, payload: any) => { | ||
link: string, | ||
env: Env, | ||
config: AuthConfig | ||
env: Env | ||
) => { | ||
@@ -52,4 +50,4 @@ return await sendEmail(urlOrigin, { | ||
from: { | ||
email: config.adminEmail, | ||
name: config.adminName, | ||
email: env.ADMIN_EMAIL, | ||
name: env.ADMIN_NAME, | ||
}, | ||
@@ -70,4 +68,3 @@ subject: 'Verify your email address', | ||
link: string, | ||
env: Env, | ||
config: AuthConfig | ||
env: Env | ||
) => { | ||
@@ -84,4 +81,4 @@ return await sendEmail(urlOrigin, { | ||
from: { | ||
email: config.adminEmail, | ||
name: config.adminName, | ||
email: env.ADMIN_EMAIL, | ||
name: env.ADMIN_NAME, | ||
}, | ||
@@ -88,0 +85,0 @@ subject: 'Confirm login', |
import * as jose from 'jose'; | ||
export type UserRole = 'user' | 'admin' | 'super'; | ||
export interface Env { | ||
@@ -8,4 +10,11 @@ DB: D1Database; | ||
DKIM_PRIVATE_KEY: string; | ||
COOKIE_NAME: string; | ||
SECRET_KEY: string; | ||
ISSUER: string; | ||
AUDIENCE: string; | ||
EXPIRY: string; | ||
ADMIN_EMAIL: string; | ||
ADMIN_NAME: string; | ||
SALT: string; | ||
} | ||
export interface UserToken { | ||
@@ -23,3 +32,3 @@ email: string; | ||
created_at?: string; | ||
role: 'user' | 'admin' | 'super'; | ||
role: UserRole; | ||
active?: 0 | 1; | ||
@@ -34,16 +43,2 @@ verified?: 0 | 1; | ||
export interface AuthConfig { | ||
secretKey: string; | ||
issuer: string; | ||
audience: string; | ||
expiry: string; | ||
cookieName: string; | ||
redirectTo: string; | ||
loginPath: string; | ||
allowUserSignup: boolean; | ||
salt: string; | ||
adminEmail: string; | ||
adminName: string; | ||
} | ||
export interface JWTPayload extends jose.JWTPayload { | ||
@@ -50,0 +45,0 @@ uid: string; |
@@ -11,4 +11,3 @@ import { Kysely } from 'kysely'; | ||
password: string, | ||
env: CloudflareAuth.Env, | ||
config: CloudflareAuth.AuthConfig | ||
env: CloudflareAuth.Env | ||
) => { | ||
@@ -26,3 +25,3 @@ const db = new Kysely<CloudflareAuth.Database>({ | ||
} | ||
const hashedPassword = await hashPassword(password, config); | ||
const hashedPassword = await hashPassword(password, env); | ||
if (hashedPassword !== users_row.password) { | ||
@@ -38,3 +37,3 @@ throw new Error('Invalid password'); | ||
urlOrigin: string, | ||
config: CloudflareAuth.AuthConfig | ||
allowUserSignup = false | ||
) => { | ||
@@ -49,3 +48,3 @@ const db = new Kysely<CloudflareAuth.Database>({ | ||
.executeTakeFirst(); | ||
if (!users_row && !config.allowUserSignup) { | ||
if (!users_row && !allowUserSignup) { | ||
throw new Error('User not found'); | ||
@@ -55,3 +54,3 @@ } | ||
const magicLink = `${urlOrigin}/verify?token=${token}`; | ||
await sendLoginMagicLinkEmail(urlOrigin, email, magicLink, env, config); | ||
await sendLoginMagicLinkEmail(urlOrigin, email, magicLink, env); | ||
return magicLink; | ||
@@ -63,3 +62,3 @@ }; | ||
env: CloudflareAuth.Env, | ||
config: CloudflareAuth.AuthConfig | ||
redirectTo: string | ||
): Promise<Response> => { | ||
@@ -88,8 +87,8 @@ const db = new Kysely<CloudflareAuth.Database>({ | ||
const jwt = await generateJWT(user.uid, email, config); | ||
const accessCookie = `${config.cookieName}=${jwt}; path=/; max-age=${config.expiry}; SameSite=Lax; HttpOnly; Secure`; | ||
const jwt = await generateJWT(user.uid, email, env); | ||
const accessCookie = `${env.COOKIE_NAME}=${jwt}; path=/; max-age=${env.EXPIRY}; SameSite=Lax; HttpOnly; Secure`; | ||
return new Response(null, { | ||
status: 301, | ||
headers: { | ||
Location: config.redirectTo, | ||
Location: redirectTo, | ||
'Set-Cookie': accessCookie, | ||
@@ -101,9 +100,10 @@ }, | ||
export const logout = async ( | ||
config: CloudflareAuth.AuthConfig | ||
env: CloudflareAuth.Env, | ||
loginPath: string | ||
): Promise<Response> => { | ||
const accessCookie = `${config.cookieName}=''; path=/; max-age=-1; SameSite=Lax; HttpOnly; Secure`; | ||
const accessCookie = `${env.COOKIE_NAME}=''; path=/; max-age=-1; SameSite=Lax; HttpOnly; Secure`; | ||
return new Response(null, { | ||
status: 301, | ||
headers: { | ||
Location: config.loginPath, | ||
Location: loginPath, | ||
'Set-Cookie': accessCookie, | ||
@@ -110,0 +110,0 @@ }, |
@@ -11,4 +11,3 @@ import { Kysely } from 'kysely'; | ||
password: string, | ||
env: CloudflareAuth.Env, | ||
config: CloudflareAuth.AuthConfig | ||
env: CloudflareAuth.Env | ||
) => { | ||
@@ -18,3 +17,3 @@ const db = new Kysely<CloudflareAuth.Database>({ | ||
}); | ||
const hashedPassword = await hashPassword(password, config); | ||
const hashedPassword = await hashPassword(password, env); | ||
const uid = uuid(); | ||
@@ -31,3 +30,3 @@ await db | ||
env: CloudflareAuth.Env, | ||
config: CloudflareAuth.AuthConfig | ||
redirectTo: string | ||
): Promise<Response> => { | ||
@@ -62,8 +61,8 @@ const db = new Kysely<CloudflareAuth.Database>({ | ||
const jwt = await generateJWT(user.uid, email, config); | ||
const accessCookie = `${config.cookieName}=${jwt}; path=/; max-age=${config.expiry}; SameSite=Lax; HttpOnly; Secure`; | ||
const jwt = await generateJWT(user.uid, email, env); | ||
const accessCookie = `${env.COOKIE_NAME}=${jwt}; path=/; max-age=${env.EXPIRY}; SameSite=Lax; HttpOnly; Secure`; | ||
return new Response(null, { | ||
status: 301, | ||
headers: { | ||
Location: config.redirectTo, | ||
Location: redirectTo, | ||
'Set-Cookie': accessCookie, | ||
@@ -70,0 +69,0 @@ }, |
@@ -11,3 +11,3 @@ import * as jose from 'jose'; | ||
env: CloudflareAuth.Env, | ||
role: string = 'user' | ||
role: CloudflareAuth.UserRole = 'user' | ||
) => { | ||
@@ -38,3 +38,3 @@ const token = uuid(); | ||
.insertInto('users') | ||
.values({ uid, email, verified: 1, role: 'user' }) | ||
.values({ uid, email, verified: 1, role }) | ||
.execute(); | ||
@@ -48,5 +48,5 @@ } | ||
email: string, | ||
config: CloudflareAuth.AuthConfig | ||
env: CloudflareAuth.Env | ||
) => { | ||
const secret = new TextEncoder().encode(config.secretKey); | ||
const secret = new TextEncoder().encode(env.SECRET_KEY); | ||
const alg = 'HS256'; | ||
@@ -56,5 +56,5 @@ return await new jose.SignJWT({ uid, email }) | ||
.setIssuedAt() | ||
.setIssuer(config.issuer) | ||
.setAudience(config.audience) | ||
.setExpirationTime(config.expiry) | ||
.setIssuer(env.ISSUER) | ||
.setAudience(env.AUDIENCE) | ||
.setExpirationTime(env.EXPIRY) | ||
.sign(secret); | ||
@@ -65,3 +65,3 @@ }; | ||
password: string, | ||
config: CloudflareAuth.AuthConfig | ||
env: CloudflareAuth.Env | ||
) => { | ||
@@ -72,5 +72,5 @@ const hashedPassword = await crypto.subtle.digest( | ||
}, | ||
new TextEncoder().encode(password + config.salt) | ||
new TextEncoder().encode(password + env.SALT) | ||
); | ||
return String(hashedPassword); | ||
}; |
@@ -6,31 +6,33 @@ import * as jose from 'jose'; | ||
export const middlewareGuard = | ||
(authConfig: CloudflareAuth.AuthConfig): PagesFunction => | ||
async ({ request, next }) => { | ||
const cookie = parse(request.headers.get('Cookie') || ''); | ||
const jwt = cookie[authConfig.cookieName]; | ||
const secret = new TextEncoder().encode(authConfig.secretKey); | ||
try { | ||
await jose.jwtVerify(jwt, secret, { | ||
issuer: authConfig.issuer, | ||
audience: authConfig.audience, | ||
}); | ||
return next(); | ||
} catch { | ||
const url = new URL(request.url); | ||
return Response.redirect(url.origin, 301); | ||
} | ||
}; | ||
export const middlewareGuard: PagesFunction<CloudflareAuth.Env> = async ({ | ||
request, | ||
next, | ||
env, | ||
}) => { | ||
const cookie = parse(request.headers.get('Cookie') || ''); | ||
const jwt = cookie[env.COOKIE_NAME]; | ||
const secret = new TextEncoder().encode(env.SECRET_KEY); | ||
try { | ||
await jose.jwtVerify(jwt, secret, { | ||
issuer: env.ISSUER, | ||
audience: env.AUDIENCE, | ||
}); | ||
return next(); | ||
} catch { | ||
const url = new URL(request.url); | ||
return Response.redirect(url.origin, 301); | ||
} | ||
}; | ||
export const isAuthorised = async ( | ||
authConfig: CloudflareAuth.AuthConfig, | ||
request: Request | ||
request: Request, | ||
env: CloudflareAuth.Env | ||
): Promise<boolean> => { | ||
const cookie = parse(request.headers.get('Cookie') || ''); | ||
const jwt = cookie[authConfig.cookieName]; | ||
const secret = new TextEncoder().encode(authConfig.secretKey); | ||
const jwt = cookie[env.COOKIE_NAME]; | ||
const secret = new TextEncoder().encode(env.SECRET_KEY); | ||
try { | ||
await jose.jwtVerify(jwt, secret, { | ||
issuer: authConfig.issuer, | ||
audience: authConfig.audience, | ||
issuer: env.ISSUER, | ||
audience: env.AUDIENCE, | ||
}); | ||
@@ -44,8 +46,8 @@ return true; | ||
export const getJWTPayload = async ( | ||
authConfig: CloudflareAuth.AuthConfig, | ||
request: Request | ||
request: Request, | ||
env: CloudflareAuth.Env | ||
): Promise<CloudflareAuth.JWTPayload> => { | ||
const cookie = parse(request.headers.get('Cookie') || ''); | ||
const jwt = cookie[authConfig.cookieName]; | ||
const jwt = cookie[env.COOKIE_NAME]; | ||
return jose.decodeJwt(jwt) as CloudflareAuth.JWTPayload; | ||
}; |
Sorry, the diff of this file is not supported yet
35
352006
10343
118