@a-type/auth
Advanced tools
Comparing version 0.3.7 to 0.3.8
@@ -268,2 +268,10 @@ import { AuthError } from './error.js'; | ||
} | ||
async function handleRefreshSessionRequest(req) { | ||
const accessToken = sessions.getAccessToken(req); | ||
const refreshToken = sessions.getRefreshToken(req); | ||
if (!accessToken || !refreshToken) { | ||
throw new AuthError('Invalid session', 401); | ||
} | ||
return sessions.refreshSession(accessToken, refreshToken); | ||
} | ||
return { | ||
@@ -279,4 +287,5 @@ handleOAuthLoginRequest, | ||
handleSessionRequest, | ||
handleRefreshSessionRequest, | ||
}; | ||
} | ||
//# sourceMappingURL=handlers.js.map |
import { parse } from 'cookie'; | ||
import { SignJWT, jwtVerify, errors } from 'jose'; | ||
import { SignJWT, jwtVerify, decodeJwt, errors, compactVerify, } from 'jose'; | ||
import { AuthError } from './error.js'; | ||
import { randomUUID } from 'crypto'; | ||
export const defaultShortNames = { | ||
@@ -13,3 +14,3 @@ userId: 'sub', | ||
}; | ||
this.getSession = async (req) => { | ||
this.getAccessToken = (req) => { | ||
var _a; | ||
@@ -22,2 +23,11 @@ const cookieHeader = (_a = req.headers.get('cookie')) !== null && _a !== void 0 ? _a : ''; | ||
} | ||
return cookieValue; | ||
}; | ||
this.getRefreshToken = (req) => { | ||
return req.headers.get(this.refreshTokenHeader); | ||
}; | ||
this.getSession = async (req) => { | ||
const cookieValue = this.getAccessToken(req); | ||
if (!cookieValue) | ||
return null; | ||
// read the JWT from the cookie | ||
@@ -30,6 +40,3 @@ try { | ||
// convert the JWT claims to a session object | ||
const session = Object.fromEntries(Object.entries(jwt.payload).map(([key, value]) => [ | ||
this.getLongName(key), | ||
value, | ||
])); | ||
const session = this.readSessionFromPayload(jwt.payload); | ||
// in dev mode, validate session has the right keys | ||
@@ -60,3 +67,40 @@ if (this.options.mode === 'development') { | ||
}; | ||
this.updateSession = async (session) => { | ||
/** | ||
* Refresh the session by re-signing the JWT with a new expiration time. | ||
* Requires a valid refresh token. | ||
*/ | ||
this.refreshSession = async (accessToken, refreshToken) => { | ||
const refreshData = await jwtVerify(refreshToken, this.secret, { | ||
issuer: this.options.issuer, | ||
audience: this.options.audience, | ||
}); | ||
// verify the signature of the token | ||
await compactVerify(accessToken, this.secret); | ||
const accessData = decodeJwt(accessToken); | ||
if (refreshData.payload.jti !== accessData.jti) { | ||
throw new AuthError('Invalid refresh token', 400); | ||
} | ||
const session = this.readSessionFromPayload(accessData); | ||
return this.updateSession(session, { sendRefreshToken: true }); | ||
}; | ||
this.updateSession = async (session, { sendRefreshToken } = { sendRefreshToken: false }) => { | ||
const jti = randomUUID(); | ||
const accessTokenBuilder = this.getAccessTokenBuilder(session, jti); | ||
const jwt = await accessTokenBuilder.sign(this.secret); | ||
const headers = { | ||
'Set-Cookie': `${this.options.cookieName}=${jwt}; Path=/; HttpOnly; SameSite=Strict`, | ||
}; | ||
if (sendRefreshToken) { | ||
const refreshTokenBuilder = this.getRefreshTokenBuilder(jti); | ||
const refreshToken = await refreshTokenBuilder.sign(this.secret); | ||
headers[this.refreshTokenHeader] = refreshToken; | ||
} | ||
return headers; | ||
}; | ||
this.clearSession = () => { | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
}; | ||
}; | ||
this.getAccessTokenBuilder = (session, jti) => { | ||
var _a; | ||
@@ -70,3 +114,4 @@ const builder = new SignJWT(Object.fromEntries(Object.entries(session).map(([key, value]) => [ | ||
.setExpirationTime((_a = this.options.expiration) !== null && _a !== void 0 ? _a : '12h') | ||
.setSubject(session.userId); | ||
.setSubject(session.userId) | ||
.setJti(jti); | ||
if (this.options.issuer) { | ||
@@ -78,11 +123,18 @@ builder.setIssuer(this.options.issuer); | ||
} | ||
const jwt = await builder.sign(this.secret); | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=${jwt}; Path=/; HttpOnly; SameSite=Strict`, | ||
}; | ||
return builder; | ||
}; | ||
this.clearSession = () => { | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
}; | ||
this.getRefreshTokenBuilder = (jti) => { | ||
const refreshTokenBuilder = new SignJWT({ | ||
jti, | ||
}) | ||
.setProtectedHeader({ alg: 'HS256' }) | ||
.setIssuedAt() | ||
.setExpirationTime('7d'); | ||
if (this.options.issuer) { | ||
refreshTokenBuilder.setIssuer(this.options.issuer); | ||
} | ||
if (this.options.audience) { | ||
refreshTokenBuilder.setAudience(this.options.audience); | ||
} | ||
return refreshTokenBuilder; | ||
}; | ||
@@ -95,2 +147,5 @@ this.getShortName = (key) => { | ||
}; | ||
this.readSessionFromPayload = (jwt) => { | ||
return Object.fromEntries(Object.entries(jwt).map(([key, value]) => [this.getLongName(key), value])); | ||
}; | ||
this.secret = new TextEncoder().encode(options.secret); | ||
@@ -104,3 +159,7 @@ this.shortNamesBackwards = Object.fromEntries(Object.entries(options.shortNames).map(([key, value]) => [value, key])); | ||
} | ||
get refreshTokenHeader() { | ||
var _a; | ||
return (_a = this.options.refreshTokenHeader) !== null && _a !== void 0 ? _a : 'x-refresh-token'; | ||
} | ||
} | ||
//# sourceMappingURL=session.js.map |
@@ -26,2 +26,3 @@ import { AuthDB } from './db.js'; | ||
handleSessionRequest: (req: Request) => Promise<Response>; | ||
handleRefreshSessionRequest: (req: Request) => Promise<Record<string, string>>; | ||
}; |
@@ -17,2 +17,3 @@ export interface Session { | ||
cookieName: string; | ||
refreshTokenHeader?: string; | ||
shortNames: ShortNames; | ||
@@ -26,7 +27,26 @@ mode?: 'production' | 'development'; | ||
createSession: (userId: string) => Promise<Session>; | ||
getSession: (req: Request) => Promise<Session | null>; | ||
updateSession: (session: Session) => Promise<Record<string, string>>; | ||
getAccessToken: (req: { | ||
headers: Headers; | ||
}) => string | null; | ||
getRefreshToken: (req: { | ||
headers: Headers; | ||
}) => string | null; | ||
getSession: (req: { | ||
headers: Headers; | ||
}) => Promise<Session | null>; | ||
/** | ||
* Refresh the session by re-signing the JWT with a new expiration time. | ||
* Requires a valid refresh token. | ||
*/ | ||
refreshSession: (accessToken: string, refreshToken: string) => Promise<Record<string, string>>; | ||
updateSession: (session: Session, { sendRefreshToken }?: { | ||
sendRefreshToken: boolean; | ||
}) => Promise<Record<string, string>>; | ||
clearSession: () => Record<string, string>; | ||
private getAccessTokenBuilder; | ||
private getRefreshTokenBuilder; | ||
private getShortName; | ||
private getLongName; | ||
private readSessionFromPayload; | ||
private get refreshTokenHeader(); | ||
} |
{ | ||
"name": "@a-type/auth", | ||
"version": "0.3.7", | ||
"version": "0.3.8", | ||
"description": "My personal auth request handlers", | ||
@@ -30,3 +30,4 @@ "module": "dist/esm/index.js", | ||
"@types/simple-oauth2": "^5.0.7", | ||
"typescript": "^5.0.2" | ||
"typescript": "^5.0.2", | ||
"vitest": "1.3.1" | ||
}, | ||
@@ -40,4 +41,5 @@ "keywords": [], | ||
"ci:version": "pnpm changeset version", | ||
"ci:publish": "pnpm changeset publish --access=public" | ||
"ci:publish": "pnpm changeset publish --access=public", | ||
"test": "vitest" | ||
} | ||
} |
@@ -339,2 +339,13 @@ import { AuthDB } from './db.js'; | ||
async function handleRefreshSessionRequest(req: Request) { | ||
const accessToken = sessions.getAccessToken(req); | ||
const refreshToken = sessions.getRefreshToken(req); | ||
if (!accessToken || !refreshToken) { | ||
throw new AuthError('Invalid session', 401); | ||
} | ||
return sessions.refreshSession(accessToken, refreshToken); | ||
} | ||
return { | ||
@@ -350,3 +361,4 @@ handleOAuthLoginRequest, | ||
handleSessionRequest, | ||
handleRefreshSessionRequest, | ||
}; | ||
} |
import { parse } from 'cookie'; | ||
import { SignJWT, jwtVerify, errors } from 'jose'; | ||
import { | ||
SignJWT, | ||
jwtVerify, | ||
decodeJwt, | ||
errors, | ||
JWTPayload, | ||
compactVerify, | ||
} from 'jose'; | ||
import { AuthError } from './error.js'; | ||
import { randomUUID } from 'crypto'; | ||
@@ -25,2 +33,3 @@ export interface Session { | ||
cookieName: string; | ||
refreshTokenHeader?: string; | ||
shortNames: ShortNames; | ||
@@ -49,3 +58,3 @@ mode?: 'production' | 'development'; | ||
getSession = async (req: Request) => { | ||
getAccessToken = (req: { headers: Headers }) => { | ||
const cookieHeader = req.headers.get('cookie') ?? ''; | ||
@@ -57,2 +66,13 @@ const cookies = parse(cookieHeader); | ||
} | ||
return cookieValue; | ||
}; | ||
getRefreshToken = (req: { headers: Headers }) => { | ||
return req.headers.get(this.refreshTokenHeader); | ||
}; | ||
getSession = async (req: { headers: Headers }) => { | ||
const cookieValue = this.getAccessToken(req); | ||
if (!cookieValue) return null; | ||
// read the JWT from the cookie | ||
@@ -65,8 +85,3 @@ try { | ||
// convert the JWT claims to a session object | ||
const session: Session = Object.fromEntries( | ||
Object.entries(jwt.payload).map(([key, value]) => [ | ||
this.getLongName(key), | ||
value, | ||
]), | ||
) as any; | ||
const session: Session = this.readSessionFromPayload(jwt.payload); | ||
// in dev mode, validate session has the right keys | ||
@@ -98,3 +113,57 @@ if (this.options.mode === 'development') { | ||
updateSession = async (session: Session): Promise<Record<string, string>> => { | ||
/** | ||
* Refresh the session by re-signing the JWT with a new expiration time. | ||
* Requires a valid refresh token. | ||
*/ | ||
refreshSession = async ( | ||
accessToken: string, | ||
refreshToken: string, | ||
): Promise<Record<string, string>> => { | ||
const refreshData = await jwtVerify(refreshToken, this.secret, { | ||
issuer: this.options.issuer, | ||
audience: this.options.audience, | ||
}); | ||
// verify the signature of the token | ||
await compactVerify(accessToken, this.secret); | ||
const accessData = decodeJwt(accessToken); | ||
if (refreshData.payload.jti !== accessData.jti) { | ||
throw new AuthError('Invalid refresh token', 400); | ||
} | ||
const session = this.readSessionFromPayload(accessData); | ||
return this.updateSession(session, { sendRefreshToken: true }); | ||
}; | ||
updateSession = async ( | ||
session: Session, | ||
{ sendRefreshToken } = { sendRefreshToken: false }, | ||
): Promise<Record<string, string>> => { | ||
const jti = randomUUID(); | ||
const accessTokenBuilder = this.getAccessTokenBuilder(session, jti); | ||
const jwt = await accessTokenBuilder.sign(this.secret); | ||
const headers: Record<string, string> = { | ||
'Set-Cookie': `${this.options.cookieName}=${jwt}; Path=/; HttpOnly; SameSite=Strict`, | ||
}; | ||
if (sendRefreshToken) { | ||
const refreshTokenBuilder = this.getRefreshTokenBuilder(jti); | ||
const refreshToken = await refreshTokenBuilder.sign(this.secret); | ||
headers[this.refreshTokenHeader] = refreshToken; | ||
} | ||
return headers; | ||
}; | ||
clearSession = (): Record<string, string> => { | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
}; | ||
}; | ||
private getAccessTokenBuilder = (session: Session, jti: string) => { | ||
const builder = new SignJWT( | ||
@@ -111,3 +180,4 @@ Object.fromEntries( | ||
.setExpirationTime(this.options.expiration ?? '12h') | ||
.setSubject(session.userId); | ||
.setSubject(session.userId) | ||
.setJti(jti); | ||
@@ -120,13 +190,21 @@ if (this.options.issuer) { | ||
} | ||
const jwt = await builder.sign(this.secret); | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=${jwt}; Path=/; HttpOnly; SameSite=Strict`, | ||
}; | ||
return builder; | ||
}; | ||
clearSession = (): Record<string, string> => { | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
}; | ||
private getRefreshTokenBuilder = (jti: string) => { | ||
const refreshTokenBuilder = new SignJWT({ | ||
jti, | ||
}) | ||
.setProtectedHeader({ alg: 'HS256' }) | ||
.setIssuedAt() | ||
.setExpirationTime('7d'); | ||
if (this.options.issuer) { | ||
refreshTokenBuilder.setIssuer(this.options.issuer); | ||
} | ||
if (this.options.audience) { | ||
refreshTokenBuilder.setAudience(this.options.audience); | ||
} | ||
return refreshTokenBuilder; | ||
}; | ||
@@ -140,2 +218,12 @@ | ||
}; | ||
private readSessionFromPayload = (jwt: JWTPayload): Session => { | ||
return Object.fromEntries( | ||
Object.entries(jwt).map(([key, value]) => [this.getLongName(key), value]), | ||
) as any; | ||
}; | ||
private get refreshTokenHeader() { | ||
return this.options.refreshTokenHeader ?? 'x-refresh-token'; | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
96373
50
1930
7