Skip to content

Commit

Permalink
jwt => session
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexErrant committed Aug 8, 2023
1 parent d6fbd73 commit fcbfad4
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 45 deletions.
12 changes: 6 additions & 6 deletions cwa/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Context } from "hono"
import { jwtVerify, type JWTVerifyResult } from "jose"
import { type Brand, csrfHeaderName, jwtCookieName } from "shared"
import { type Brand, csrfHeaderName, sessionCookieName } from "shared"
import { getJwsSecret } from "./env"
import { type MediaTokenSecretBase64 } from "./privateToken"

Expand Down Expand Up @@ -69,16 +69,16 @@ export async function getUserId(
if (c.req.header(csrfHeaderName) == null) {
return toError(c.text(`Missing '${csrfHeaderName}' header`, 401))
}
const jwt = c.req.cookie(jwtCookieName)
if (jwt == null) {
return toError(c.text(`Missing '${jwtCookieName}' cookie.`, 401))
const session = c.req.cookie(sessionCookieName)
if (session == null) {
return toError(c.text(`Missing '${sessionCookieName}' cookie.`, 401))
} else {
let verifyResult: JWTVerifyResult
try {
verifyResult = await jwtVerify(jwt, getJwsSecret(c.env.jwsSecret))
verifyResult = await jwtVerify(session, getJwsSecret(c.env.jwsSecret))
} catch {
return toError(
c.text(`Failed to verify JWT in '${jwtCookieName}' cookie.`, 401)
c.text(`Failed to verify JWT in '${sessionCookieName}' cookie.`, 401)
)
}
if (verifyResult.payload.sub == null) {
Expand Down
8 changes: 4 additions & 4 deletions hub/src/routes/n/[nook]/submit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
createServerData$,
redirect,
} from "solid-start/server"
import { requireCsrfSignature, requireJwt, isInvalidCsrf } from "~/session"
import { requireCsrfSignature, requireSession, isInvalidCsrf } from "~/session"

export function routeData({ params }: RouteDataArgs) {
const nook = (): string => params.nook
Expand Down Expand Up @@ -65,8 +65,8 @@ export default function Submit(): JSX.Element {
if (Object.values(fieldErrors).some(Boolean)) {
throw new FormError("Some fields are invalid", { fieldErrors, fields })
}
const jwt = await requireJwt(request)
if (await isInvalidCsrf(csrfSignature, jwt.jti)) {
const session = await requireSession(request)
if (await isInvalidCsrf(csrfSignature, session.jti)) {
const searchParams = new URLSearchParams([
["redirectTo", new URL(request.url).pathname],
])
Expand All @@ -75,7 +75,7 @@ export default function Submit(): JSX.Element {

await insertPost({
id: ulidAsHex(),
authorId: jwt.sub,
authorId: session.sub,
title,
text,
nook,
Expand Down
6 changes: 3 additions & 3 deletions hub/src/routes/nooks/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "solid-start/server"
import {
requireCsrfSignature,
requireJwt,
requireSession,
isInvalidCsrf,
requireUserId,
} from "~/session"
Expand Down Expand Up @@ -73,8 +73,8 @@ export default function Submit(): JSX.Element {
throw new FormError("Some fields are invalid", { fieldErrors, fields })
}
const userId = await requireUserId(request)
const jwt = await requireJwt(request)
if (await isInvalidCsrf(csrfSignature, jwt.jti)) {
const session = await requireSession(request)
if (await isInvalidCsrf(csrfSignature, session.jti)) {
const searchParams = new URLSearchParams([
["redirectTo", new URL(request.url).pathname],
])
Expand Down
52 changes: 26 additions & 26 deletions hub/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
type Base64,
base64url,
csrfSignatureCookieName,
jwtCookieName,
sessionCookieName,
throwExp,
type UserId,
} from "shared"
Expand All @@ -22,7 +22,7 @@ export function setSessionStorage(x: {
jwsSecret = base64ToArray(x.jwsSecret)
csrfSecret = x.csrfSecret
hubInfoSecret = base64ToArray(x.hubInfoSecret)
const jwtCookieOpts: CookieOptions = {
const sessionCookieOpts: CookieOptions = {
secure: true,
secrets: [], // intentionally empty. This cookie should only store a signed JWT!
sameSite: "strict",
Expand All @@ -32,9 +32,9 @@ export function setSessionStorage(x: {
domain: import.meta.env.VITE_HUB_DOMAIN, // sadly, making cookies target specific subdomains from the main domain seems very hacky
// expires: "", // intentionally missing because docs say it's calculated off `maxAge` when missing https://github.com/solidjs/solid-start/blob/1b22cad87dd7bd74f73d807e1d60b886e753a6ee/packages/start/session/cookies.ts#L56-L57
}
jwtCookie = createPlainCookie(jwtCookieName, jwtCookieOpts)
destroyJwtCookie = createPlainCookie(jwtCookieName, {
...jwtCookieOpts,
sessionCookie = createPlainCookie(sessionCookieName, sessionCookieOpts)
destroySessionCookie = createPlainCookie(sessionCookieName, {
...sessionCookieOpts,
maxAge: undefined,
expires: new Date(0), // https://github.com/remix-run/remix/issues/5150 https://stackoverflow.com/q/5285940
})
Expand Down Expand Up @@ -119,9 +119,9 @@ export function setSessionStorage(x: {
}

// @ts-expect-error calls should throw null error if not setup
let jwtCookie = null as Cookie
let sessionCookie = null as Cookie
// @ts-expect-error calls should throw null error if not setup
let destroyJwtCookie = null as Cookie
let destroySessionCookie = null as Cookie
// @ts-expect-error calls should throw null error if not setup
let csrfSignatureCookie = null as Cookie
// @ts-expect-error calls should throw null error if not setup
Expand Down Expand Up @@ -175,30 +175,30 @@ export async function getOauthCodeVerifier(request: Request) {
return codeVerifier
}

export interface Jwt {
export interface Session {
sub: UserId
jti: string
}

export async function getJwt(request: Request): Promise<Jwt | null> {
const rawJwt = (await jwtCookie.parse(
export async function getSession(request: Request): Promise<Session | null> {
const rawSession = (await sessionCookie.parse(
request.headers.get("Cookie")
)) as string
let jwt: JWTVerifyResult | null = null
let session: JWTVerifyResult | null = null
try {
jwt = await jwtVerify(rawJwt, jwsSecret)
session = await jwtVerify(rawSession, jwsSecret)
} catch {}
return jwt == null
return session == null
? null
: {
sub: (jwt.payload.sub as UserId) ?? throwExp("`sub` is empty"),
jti: jwt.payload.jti ?? throwExp("`jti` is empty"),
sub: (session.payload.sub as UserId) ?? throwExp("`sub` is empty"),
jti: session.payload.jti ?? throwExp("`jti` is empty"),
}
}

export async function getUserId(request: Request) {
const jwt = await getJwt(request)
return jwt?.sub ?? null
const session = await getSession(request)
return session?.sub ?? null
}

export async function requireUserId(
Expand All @@ -225,21 +225,21 @@ export async function requireCsrfSignature(
return csrfSignature
}

export async function requireJwt(
export async function requireSession(
request: Request,
redirectTo: string = new URL(request.url).pathname
): Promise<Jwt> {
const jwt = await getJwt(request)
if (jwt == null) {
): Promise<Session> {
const session = await getSession(request)
if (session == null) {
const searchParams = new URLSearchParams([["redirectTo", redirectTo]])
throw redirect(`/login?${searchParams.toString()}`) as unknown
}
return jwt
return session
}

export async function logout() {
const headers = new Headers()
headers.append("Set-Cookie", await destroyJwtCookie.serialize("")) // lowTODO parallelize
headers.append("Set-Cookie", await destroySessionCookie.serialize("")) // lowTODO parallelize
headers.append("Set-Cookie", await destroyCsrfSignatureCookie.serialize("")) // lowTODO parallelize
return redirect("/login", {
headers,
Expand All @@ -252,8 +252,8 @@ export async function createUserSession(
): Promise<Response> {
const [csrf, csrfSignature] = await generateCsrf()
const headers = new Headers()
const jwt = await generateJwt(userId, csrf)
headers.append("Set-Cookie", await jwtCookie.serialize(jwt)) // lowTODO parallelize
const session = await generateSession(userId, csrf)
headers.append("Set-Cookie", await sessionCookie.serialize(session)) // lowTODO parallelize
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
// If you ever separate csrf from the session cookie https://security.stackexchange.com/a/220810 https://security.stackexchange.com/a/248434
// REST endpoints may need csrf https://security.stackexchange.com/q/166724
Expand Down Expand Up @@ -308,7 +308,7 @@ export async function getInfo(request: Request) {
: (jwt.payload.info as string) ?? throwExp("`info` is empty")
}

async function generateJwt(userId: string, csrf: string): Promise<string> {
async function generateSession(userId: string, csrf: string): Promise<string> {
return await new SignJWT({})
.setProtectedHeader({ alg })
.setSubject(userId)
Expand Down
10 changes: 5 additions & 5 deletions lrpc/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { jwtVerify } from "jose"
import { jwsSecret } from "./config.js"
import { parse } from "cookie"
import { csrfHeaderName, jwtCookieName } from "shared"
import { csrfHeaderName, sessionCookieName } from "shared"
import { type IncomingHttpHeaders } from "http"

export async function getUser(
headers: IncomingHttpHeaders
): Promise<string | undefined> {
if (headers.cookie != null) {
const cookies = parse(headers.cookie)
const jwtCookie = cookies[jwtCookieName]
if (jwtCookie != null && csrfHeaderName in headers) {
const sessionCookie = cookies[sessionCookieName]
if (sessionCookie != null && csrfHeaderName in headers) {
try {
const jwt = await jwtVerify(jwtCookie, jwsSecret)
return jwt.payload.sub
const session = await jwtVerify(sessionCookie, jwsSecret)
return session.payload.sub
} catch {}
}
}
Expand Down
2 changes: 1 addition & 1 deletion shared/src/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
export const hstsName = "Strict-Transport-Security"
export const hstsValue = "max-age=63072000; includeSubDomains; preload" // 2 years

export const jwtCookieName = "__Secure-jwt"
export const sessionCookieName = "__Secure-session"
export const csrfSignatureCookieName = "__Secure-csrf"
export const csrfHeaderName = "x-csrf" // https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers

0 comments on commit fcbfad4

Please sign in to comment.