Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
interface CloudflareEnv {
JWT_SECRET: string
GITHUB_CLIENT_ID: string
GITHUB_CLIENT_SECRET: string
GITHUB_CALLBACK_URL: string
DEV?: string
}

declare global {
namespace App {
interface Locals {
runtime: {
env: CloudflareEnv
}
}
}
}

export {}
40 changes: 21 additions & 19 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import * as jose from 'jose'

const getSecret = () => {
const secret = import.meta.env.JWT_SECRET
type Env = Record<string, any>

const getSecret = (env: Env) => {
const secret = env.JWT_SECRET
if (!secret) {
throw new Error('JWT_SECRET is not set')
}
return new TextEncoder().encode(secret)
}

const getTokenSecret = () => {
const secret = import.meta.env.JWT_SECRET
const getTokenSecret = (env: Env) => {
const secret = env.JWT_SECRET
if (!secret) {
throw new Error('JWT_SECRET is not set')
}
Expand All @@ -32,47 +34,47 @@ export interface GitHubTokens {
scope: string
}

export async function createToken(user: SessionUser): Promise<string> {
const secret = getSecret()
export async function createToken(user: SessionUser, env: Env): Promise<string> {
const secret = getSecret(env)
return await new jose.SignJWT({ ...user })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(secret)
}

export async function verifyToken(token: string): Promise<SessionUser | null> {
export async function verifyToken(token: string, env: Env): Promise<SessionUser | null> {
try {
const secret = getSecret()
const secret = getSecret(env)
const { payload } = await jose.jwtVerify(token, secret)
return payload as unknown as SessionUser
} catch {
return null
}
}

export async function createTokensCookie(tokens: GitHubTokens): Promise<string> {
const secret = getTokenSecret()
export async function createTokensCookie(tokens: GitHubTokens, env: Env): Promise<string> {
const secret = getTokenSecret(env)
return await new jose.SignJWT({ ...tokens })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('90d')
.sign(secret)
}

export async function verifyTokensCookie(token: string): Promise<GitHubTokens | null> {
export async function verifyTokensCookie(token: string, env: Env): Promise<GitHubTokens | null> {
try {
const secret = getTokenSecret()
const secret = getTokenSecret(env)
const { payload } = await jose.jwtVerify(token, secret)
return payload as unknown as GitHubTokens
} catch {
return null
}
}

export async function refreshAccessToken(refreshToken: string): Promise<GitHubTokens | null> {
const clientId = import.meta.env.GITHUB_CLIENT_ID
const clientSecret = import.meta.env.GITHUB_CLIENT_SECRET
export async function refreshAccessToken(refreshToken: string, env: Env): Promise<GitHubTokens | null> {
const clientId = env.GITHUB_CLIENT_ID
const clientSecret = env.GITHUB_CLIENT_SECRET

if (!clientId || !clientSecret) {
return null
Expand Down Expand Up @@ -123,8 +125,8 @@ export function getTokensCookieName(): string {
return TOKEN_COOKIE_NAME
}

export function getCookieOptions() {
const isDev = import.meta.env.DEV
export function getCookieOptions(env: Env) {
const isDev = env.DEV
return {
httpOnly: true,
secure: !isDev,
Expand All @@ -134,8 +136,8 @@ export function getCookieOptions() {
}
}

export function getTokensCookieOptions() {
const isDev = import.meta.env.DEV
export function getTokensCookieOptions(env: Env) {
const isDev = env.DEV
return {
httpOnly: true,
secure: !isDev,
Expand Down
5 changes: 3 additions & 2 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { verifyToken, getCookieName, type SessionUser } from './lib/auth'
export const onRequest = defineMiddleware(async (context, next) => {
const cookieName = getCookieName()
const token = context.cookies.get(cookieName)?.value
const env = context.locals.runtime?.env

if (token) {
const user = await verifyToken(token)
if (token && env) {
const user = await verifyToken(token, env)
if (user) {
context.locals.user = user
}
Expand Down
17 changes: 9 additions & 8 deletions src/pages/api/auth/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
type GitHubTokens,
} from '../../../lib/auth'

export const GET: APIRoute = async ({ url, redirect, cookies }) => {
const clientId = import.meta.env.GITHUB_CLIENT_ID
const clientSecret = import.meta.env.GITHUB_CLIENT_SECRET
const callbackUrl = import.meta.env.GITHUB_CALLBACK_URL
export const GET: APIRoute = async ({ url, redirect, cookies, locals }) => {
const env = locals.runtime.env
const clientId = env.GITHUB_CLIENT_ID
const clientSecret = env.GITHUB_CLIENT_SECRET
const callbackUrl = env.GITHUB_CALLBACK_URL

if (!clientId || !clientSecret || !callbackUrl) {
return new Response('GitHub OAuth not configured', { status: 500 })
Expand Down Expand Up @@ -67,8 +68,8 @@ export const GET: APIRoute = async ({ url, redirect, cookies }) => {
name: githubUser.name,
}

const userToken = await createToken(sessionUser)
cookies.set(getCookieName(), userToken, getCookieOptions())
const userToken = await createToken(sessionUser, env)
cookies.set(getCookieName(), userToken, getCookieOptions(env))

if (refreshToken) {
const tokens: GitHubTokens = {
Expand All @@ -77,8 +78,8 @@ export const GET: APIRoute = async ({ url, redirect, cookies }) => {
expires_at: Date.now() + (tokenData.expires_in || 3600) * 1000,
scope: tokenData.scope || '',
}
const tokensCookie = await createTokensCookie(tokens)
cookies.set(getTokensCookieName(), tokensCookie, getTokensCookieOptions())
const tokensCookie = await createTokensCookie(tokens, env)
cookies.set(getTokensCookieName(), tokensCookie, getTokensCookieOptions(env))
}

return redirect('/')
Expand Down
10 changes: 5 additions & 5 deletions src/pages/api/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export const prerender = false

import type { APIRoute } from 'astro'
import { createToken, getCookieName, getCookieOptions, type SessionUser } from '../../../lib/auth'

export const GET: APIRoute = async ({ url, redirect }) => {
const clientId = import.meta.env.GITHUB_CLIENT_ID
const clientSecret = import.meta.env.GITHUB_CLIENT_SECRET
const callbackUrl = import.meta.env.GITHUB_CALLBACK_URL
export const GET: APIRoute = async ({ redirect, locals }) => {
const env = locals.runtime.env
const clientId = env.GITHUB_CLIENT_ID
const clientSecret = env.GITHUB_CLIENT_SECRET
const callbackUrl = env.GITHUB_CALLBACK_URL

if (!clientId || !clientSecret || !callbackUrl) {
return new Response('GitHub OAuth not configured', { status: 500 })
Expand Down
13 changes: 7 additions & 6 deletions src/pages/api/auth/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
getTokensCookieOptions,
} from '../../../lib/auth'

export const GET: APIRoute = async ({ cookies }) => {
export const GET: APIRoute = async ({ cookies, locals }) => {
const env = locals.runtime.env
const sessionToken = cookies.get(getCookieName())?.value
const tokensToken = cookies.get(getTokensCookieName())?.value

Expand All @@ -22,26 +23,26 @@ export const GET: APIRoute = async ({ cookies }) => {
})
}

const user = await verifyToken(sessionToken)
const user = await verifyToken(sessionToken, env)
if (!user) {
return new Response(JSON.stringify({ user: null, accessToken: null }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}

let tokens = await verifyTokensCookie(tokensToken)
let tokens = await verifyTokensCookie(tokensToken, env)

if (tokens) {
const isExpired = Date.now() >= tokens.expires_at
const shouldRefresh = tokens.refresh_token && isExpired

if (shouldRefresh) {
const newTokens = await refreshAccessToken(tokens.refresh_token)
const newTokens = await refreshAccessToken(tokens.refresh_token, env)
if (newTokens) {
tokens = newTokens
const newTokensCookie = await createTokensCookie(newTokens)
cookies.set(getTokensCookieName(), newTokensCookie, getTokensCookieOptions())
const newTokensCookie = await createTokensCookie(newTokens, env)
cookies.set(getTokensCookieName(), newTokensCookie, getTokensCookieOptions(env))
}
}
}
Expand Down
Loading