@a-type/auth
Advanced tools
Comparing version 0.4.0 to 0.4.1
@@ -12,2 +12,13 @@ import { AuthError } from './error.js'; | ||
} | ||
function toRedirect(destination, session) { | ||
// add search params to destination | ||
const url = new URL(destination); | ||
for (const [key, value] of session.searchParams) { | ||
url.searchParams.append(key, value); | ||
} | ||
return new Response(null, { | ||
status: 302, | ||
headers: Object.assign({ location: url.toString() }, session.headers), | ||
}); | ||
} | ||
function handleOAuthLoginRequest(req, opts) { | ||
@@ -31,3 +42,3 @@ var _a; | ||
async function handleOAuthCallbackRequest(req, opts) { | ||
var _a, _b, _c, _d, _e, _f; | ||
var _a, _b, _c, _d; | ||
const url = new URL(req.url); | ||
@@ -80,11 +91,6 @@ const code = url.searchParams.get('code'); | ||
const session = await sessions.createSession(userId); | ||
const clientDomain = (_e = (_d = req.headers.get('origin')) !== null && _d !== void 0 ? _d : req.headers.get('host')) !== null && _e !== void 0 ? _e : undefined; | ||
const { headers: sessionHeaders } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: Object.assign(Object.assign({}, sessionHeaders), { location: (_f = url.searchParams.get('returnTo')) !== null && _f !== void 0 ? _f : defaultReturnTo }), | ||
}); | ||
return toRedirect((_d = url.searchParams.get('returnTo')) !== null && _d !== void 0 ? _d : defaultReturnTo, sessionUpdate); | ||
} | ||
@@ -95,5 +101,6 @@ async function handleLogoutRequest(req) { | ||
const returnTo = (_a = url.searchParams.get('returnTo')) !== null && _a !== void 0 ? _a : defaultReturnTo; | ||
const { headers } = sessions.clearSession(); | ||
return new Response(null, { | ||
status: 302, | ||
headers: Object.assign(Object.assign({}, sessions.clearSession()), { location: returnTo }), | ||
headers: Object.assign(Object.assign({}, headers), { location: returnTo }), | ||
}); | ||
@@ -136,3 +143,3 @@ } | ||
async function handleVerifyEmailRequest(req) { | ||
var _a, _b, _c, _d, _e; | ||
var _a, _b, _c; | ||
const url = new URL(req.url); | ||
@@ -181,14 +188,9 @@ const code = url.searchParams.get('code'); | ||
const session = await sessions.createSession(userId); | ||
const clientDomain = (_d = (_c = req.headers.get('origin')) !== null && _c !== void 0 ? _c : req.headers.get('host')) !== null && _d !== void 0 ? _d : undefined; | ||
const { headers } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: Object.assign(Object.assign({}, headers), { location: (_e = url.searchParams.get('returnTo')) !== null && _e !== void 0 ? _e : defaultReturnTo }), | ||
}); | ||
return toRedirect((_c = url.searchParams.get('returnTo')) !== null && _c !== void 0 ? _c : defaultReturnTo, sessionUpdate); | ||
} | ||
async function handleEmailLoginRequest(req) { | ||
var _a, _b, _c, _d; | ||
var _a, _b; | ||
const formData = await req.formData(); | ||
@@ -210,11 +212,6 @@ const email = formData.get('email'); | ||
const session = await sessions.createSession(user.id); | ||
const clientDomain = (_c = (_b = req.headers.get('origin')) !== null && _b !== void 0 ? _b : req.headers.get('host')) !== null && _c !== void 0 ? _c : undefined; | ||
const { headers } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: Object.assign(Object.assign({}, headers), { location: (_d = params.returnTo) !== null && _d !== void 0 ? _d : defaultReturnTo }), | ||
}); | ||
return toRedirect((_b = params.returnTo) !== null && _b !== void 0 ? _b : defaultReturnTo, sessionUpdate); | ||
} | ||
@@ -248,3 +245,3 @@ async function handleResetPasswordRequest(req) { | ||
async function handleVerifyPasswordResetRequest(req) { | ||
var _a, _b, _c, _d; | ||
var _a, _b; | ||
const url = new URL(req.url); | ||
@@ -268,11 +265,6 @@ const code = url.searchParams.get('code'); | ||
const session = await sessions.createSession(user.id); | ||
const clientDomain = (_c = (_b = req.headers.get('origin')) !== null && _b !== void 0 ? _b : req.headers.get('host')) !== null && _c !== void 0 ? _c : undefined; | ||
const { headers } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: Object.assign(Object.assign({}, headers), { location: (_d = url.searchParams.get('returnTo')) !== null && _d !== void 0 ? _d : defaultReturnTo }), | ||
}); | ||
return toRedirect((_b = url.searchParams.get('returnTo')) !== null && _b !== void 0 ? _b : defaultReturnTo, sessionUpdate); | ||
} | ||
@@ -303,5 +295,6 @@ async function handleSessionRequest(req) { | ||
} | ||
const { headers } = await sessions.refreshSession(accessToken, refreshToken); | ||
const { headers, searchParams } = await sessions.refreshSession(accessToken, refreshToken); | ||
return new Response(JSON.stringify({ | ||
ok: true, | ||
refreshToken: searchParams.get('refreshToken'), | ||
}), { | ||
@@ -308,0 +301,0 @@ status: 200, |
@@ -82,3 +82,3 @@ import { parse, serialize } from 'cookie'; | ||
}; | ||
this.updateSession = async (session, { sendRefreshToken, clientDomain = this.options.audience, } = { sendRefreshToken: false }) => { | ||
this.updateSession = async (session, { sendRefreshToken, } = { sendRefreshToken: false }) => { | ||
const jti = randomUUID(); | ||
@@ -95,19 +95,11 @@ const accessTokenBuilder = this.getAccessTokenBuilder(session, jti); | ||
}; | ||
const searchParams = new URLSearchParams(); | ||
if (sendRefreshToken) { | ||
const refreshTokenBuilder = this.getRefreshTokenBuilder(jti); | ||
const refreshToken = await refreshTokenBuilder.sign(this.secret); | ||
// construct a short lived cookie for the client domain | ||
// which is client accessible. this lets us pass the refresh token | ||
// to the client so it can store it itself, even if the response | ||
// is a document. | ||
const refreshCookie = serialize(this.refreshCookieName, refreshToken, { | ||
httpOnly: false, | ||
sameSite: 'strict', | ||
path: '/', | ||
domain: clientDomain, | ||
}); | ||
headers['Set-Cookie'] += '; ' + refreshCookie; | ||
searchParams.set(this.refreshParam, refreshToken); | ||
} | ||
return { | ||
headers, | ||
searchParams, | ||
}; | ||
@@ -117,3 +109,5 @@ }; | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
headers: { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
}, | ||
}; | ||
@@ -172,7 +166,7 @@ }; | ||
} | ||
get refreshCookieName() { | ||
get refreshParam() { | ||
var _a; | ||
return ((_a = this.options.refreshCookieName) !== null && _a !== void 0 ? _a : `${this.options.cookieName}-refresh`); | ||
return (_a = this.options.refreshParam) !== null && _a !== void 0 ? _a : `refreshToken`; | ||
} | ||
} | ||
//# sourceMappingURL=session.js.map |
@@ -20,22 +20,4 @@ import { it, describe, vi, beforeAll, expect } from 'vitest'; | ||
}); | ||
it('should send a refresh token in a cookie for the client domain', async () => { | ||
const { headers } = await sessions.updateSession({ userId: '123' }, { | ||
sendRefreshToken: true, | ||
clientDomain: 'client.com', | ||
}); | ||
const cookies = parse(headers['Set-Cookie']); | ||
const authToken = cookies['session']; | ||
const refreshToken = cookies['session-refresh']; | ||
expect(authToken).toBeTruthy(); | ||
expect(refreshToken).toBeTruthy(); | ||
// cookie should be for the client domain | ||
expect(headers['Set-Cookie']).toMatch(/session-refresh=(.*);\s+Domain=client.com;\s+Path=\/;/); | ||
// without specifying client domain, it defaults to audience | ||
const { headers: headers2 } = await sessions.updateSession({ userId: '123' }, { | ||
sendRefreshToken: true, | ||
}); | ||
expect(headers2['Set-Cookie']).toMatch(/session-refresh=(.*);\s+Domain=example.com;\s+Path=\/;/); | ||
}); | ||
it('should refresh an expired JWT', async () => { | ||
const { headers } = await sessions.updateSession({ userId: '123' }, { | ||
const { headers, searchParams } = await sessions.updateSession({ userId: '123' }, { | ||
sendRefreshToken: true, | ||
@@ -45,3 +27,3 @@ }); | ||
const authToken = cookies['session']; | ||
const refreshToken = cookies['session-refresh']; | ||
const refreshToken = searchParams.get('refreshToken'); | ||
// verify that the token is accepted | ||
@@ -66,6 +48,6 @@ let req = { | ||
// verify that the refresh token is accepted | ||
const { headers: newSessionHeaders } = await sessions.refreshSession(authToken, refreshToken); | ||
const { headers: newSessionHeaders, searchParams: newSessionSearchParams } = await sessions.refreshSession(authToken, refreshToken); | ||
const newCookies = parse(newSessionHeaders['Set-Cookie']); | ||
const newAuthToken = newCookies['session']; | ||
const newRefreshToken = newCookies['session-refresh']; | ||
const newRefreshToken = newSessionSearchParams.get('refreshToken'); | ||
// verify that the new token is accepted | ||
@@ -84,3 +66,3 @@ req = { | ||
it('should not allow refreshing any old token', async () => { | ||
const { headers } = await sessions.updateSession({ userId: '123' }, { | ||
const { headers, searchParams } = await sessions.updateSession({ userId: '123' }, { | ||
sendRefreshToken: true, | ||
@@ -98,3 +80,3 @@ }); | ||
const jti = verifiedAuth.payload.jti; | ||
const refreshToken = cookies['session-refresh']; | ||
const refreshToken = searchParams.get('refreshToken'); | ||
// sign a JWT with some other signature | ||
@@ -101,0 +83,0 @@ const badToken = await new SignJWT({}) |
@@ -17,3 +17,3 @@ export interface Session { | ||
cookieName: string; | ||
refreshCookieName?: string; | ||
refreshParam?: string; | ||
shortNames: ShortNames; | ||
@@ -44,10 +44,15 @@ mode?: 'production' | 'development'; | ||
headers: Record<string, string>; | ||
searchParams: URLSearchParams; | ||
}>; | ||
updateSession: (session: Session, { sendRefreshToken, clientDomain, }?: { | ||
updateSession: (session: Session, { sendRefreshToken, }?: { | ||
sendRefreshToken?: boolean | undefined; | ||
clientDomain?: string | undefined; | ||
}) => Promise<{ | ||
headers: Record<string, string>; | ||
searchParams: URLSearchParams; | ||
}>; | ||
clearSession: () => Record<string, string>; | ||
clearSession: () => { | ||
headers: { | ||
'Set-Cookie': string; | ||
}; | ||
}; | ||
private getAccessTokenBuilder; | ||
@@ -58,3 +63,3 @@ private getRefreshTokenBuilder; | ||
private readSessionFromPayload; | ||
private get refreshCookieName(); | ||
private get refreshParam(); | ||
} |
{ | ||
"name": "@a-type/auth", | ||
"version": "0.4.0", | ||
"version": "0.4.1", | ||
"description": "My personal auth request handlers", | ||
@@ -5,0 +5,0 @@ "module": "dist/esm/index.js", |
@@ -35,2 +35,20 @@ import { AuthDB } from './db.js'; | ||
function toRedirect( | ||
destination: string, | ||
session: { headers: Record<string, string>; searchParams: URLSearchParams }, | ||
) { | ||
// add search params to destination | ||
const url = new URL(destination); | ||
for (const [key, value] of session.searchParams) { | ||
url.searchParams.append(key, value); | ||
} | ||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
location: url.toString(), | ||
...session.headers, | ||
}, | ||
}); | ||
} | ||
function handleOAuthLoginRequest(req: Request, opts: { provider: string }) { | ||
@@ -114,15 +132,9 @@ const url = new URL(req.url); | ||
const session = await sessions.createSession(userId); | ||
const clientDomain = | ||
req.headers.get('origin') ?? req.headers.get('host') ?? undefined; | ||
const { headers: sessionHeaders } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
...sessionHeaders, | ||
location: url.searchParams.get('returnTo') ?? defaultReturnTo, | ||
}, | ||
}); | ||
return toRedirect( | ||
url.searchParams.get('returnTo') ?? defaultReturnTo, | ||
sessionUpdate, | ||
); | ||
} | ||
@@ -133,6 +145,7 @@ | ||
const returnTo = url.searchParams.get('returnTo') ?? defaultReturnTo; | ||
const { headers } = sessions.clearSession(); | ||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
...sessions.clearSession(), | ||
...headers, | ||
location: returnTo, | ||
@@ -225,15 +238,9 @@ }, | ||
const session = await sessions.createSession(userId); | ||
const clientDomain = | ||
req.headers.get('origin') ?? req.headers.get('host') ?? undefined; | ||
const { headers } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
...headers, | ||
location: url.searchParams.get('returnTo') ?? defaultReturnTo, | ||
}, | ||
}); | ||
return toRedirect( | ||
url.searchParams.get('returnTo') ?? defaultReturnTo, | ||
sessionUpdate, | ||
); | ||
} | ||
@@ -264,15 +271,6 @@ | ||
const session = await sessions.createSession(user.id); | ||
const clientDomain = | ||
req.headers.get('origin') ?? req.headers.get('host') ?? undefined; | ||
const { headers } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
...headers, | ||
location: params.returnTo ?? defaultReturnTo, | ||
}, | ||
}); | ||
return toRedirect(params.returnTo ?? defaultReturnTo, sessionUpdate); | ||
} | ||
@@ -328,15 +326,9 @@ | ||
const session = await sessions.createSession(user.id); | ||
const clientDomain = | ||
req.headers.get('origin') ?? req.headers.get('host') ?? undefined; | ||
const { headers } = await sessions.updateSession(session, { | ||
const sessionUpdate = await sessions.updateSession(session, { | ||
sendRefreshToken: true, | ||
clientDomain, | ||
}); | ||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
...headers, | ||
location: url.searchParams.get('returnTo') ?? defaultReturnTo, | ||
}, | ||
}); | ||
return toRedirect( | ||
url.searchParams.get('returnTo') ?? defaultReturnTo, | ||
sessionUpdate, | ||
); | ||
} | ||
@@ -374,3 +366,3 @@ | ||
const { headers } = await sessions.refreshSession( | ||
const { headers, searchParams } = await sessions.refreshSession( | ||
accessToken, | ||
@@ -383,2 +375,3 @@ refreshToken, | ||
ok: true, | ||
refreshToken: searchParams.get('refreshToken'), | ||
}), | ||
@@ -385,0 +378,0 @@ { |
@@ -23,38 +23,4 @@ import { it, describe, vi, beforeAll, expect } from 'vitest'; | ||
it('should send a refresh token in a cookie for the client domain', async () => { | ||
const { headers } = await sessions.updateSession( | ||
{ userId: '123' }, | ||
{ | ||
sendRefreshToken: true, | ||
clientDomain: 'client.com', | ||
}, | ||
); | ||
const cookies = parse(headers['Set-Cookie']); | ||
const authToken = cookies['session']; | ||
const refreshToken = cookies['session-refresh']; | ||
expect(authToken).toBeTruthy(); | ||
expect(refreshToken).toBeTruthy(); | ||
// cookie should be for the client domain | ||
expect(headers['Set-Cookie']).toMatch( | ||
/session-refresh=(.*);\s+Domain=client.com;\s+Path=\/;/, | ||
); | ||
// without specifying client domain, it defaults to audience | ||
const { headers: headers2 } = await sessions.updateSession( | ||
{ userId: '123' }, | ||
{ | ||
sendRefreshToken: true, | ||
}, | ||
); | ||
expect(headers2['Set-Cookie']).toMatch( | ||
/session-refresh=(.*);\s+Domain=example.com;\s+Path=\/;/, | ||
); | ||
}); | ||
it('should refresh an expired JWT', async () => { | ||
const { headers } = await sessions.updateSession( | ||
const { headers, searchParams } = await sessions.updateSession( | ||
{ userId: '123' }, | ||
@@ -68,3 +34,3 @@ { | ||
const authToken = cookies['session']; | ||
const refreshToken = cookies['session-refresh']; | ||
const refreshToken = searchParams.get('refreshToken')!; | ||
@@ -95,10 +61,8 @@ // verify that the token is accepted | ||
// verify that the refresh token is accepted | ||
const { headers: newSessionHeaders } = await sessions.refreshSession( | ||
authToken, | ||
refreshToken, | ||
); | ||
const { headers: newSessionHeaders, searchParams: newSessionSearchParams } = | ||
await sessions.refreshSession(authToken, refreshToken); | ||
const newCookies = parse(newSessionHeaders['Set-Cookie']); | ||
const newAuthToken = newCookies['session']; | ||
const newRefreshToken = newCookies['session-refresh']; | ||
const newRefreshToken = newSessionSearchParams.get('refreshToken')!; | ||
@@ -121,3 +85,3 @@ // verify that the new token is accepted | ||
it('should not allow refreshing any old token', async () => { | ||
const { headers } = await sessions.updateSession( | ||
const { headers, searchParams } = await sessions.updateSession( | ||
{ userId: '123' }, | ||
@@ -143,3 +107,3 @@ { | ||
const jti = verifiedAuth.payload.jti!; | ||
const refreshToken = cookies['session-refresh']; | ||
const refreshToken = searchParams.get('refreshToken')!; | ||
@@ -146,0 +110,0 @@ // sign a JWT with some other signature |
@@ -33,3 +33,3 @@ import { parse, serialize } from 'cookie'; | ||
cookieName: string; | ||
refreshCookieName?: string; | ||
refreshParam?: string; | ||
shortNames: ShortNames; | ||
@@ -116,8 +116,3 @@ mode?: 'production' | 'development'; | ||
*/ | ||
refreshSession = async ( | ||
accessToken: string, | ||
refreshToken: string, | ||
): Promise<{ | ||
headers: Record<string, string>; | ||
}> => { | ||
refreshSession = async (accessToken: string, refreshToken: string) => { | ||
const refreshData = await jwtVerify(refreshToken, this.secret, { | ||
@@ -146,10 +141,6 @@ issuer: this.options.issuer, | ||
sendRefreshToken, | ||
clientDomain = this.options.audience, | ||
}: { | ||
sendRefreshToken?: boolean; | ||
clientDomain?: string; | ||
} = { sendRefreshToken: false }, | ||
): Promise<{ | ||
headers: Record<string, string>; | ||
}> => { | ||
) => { | ||
const jti = randomUUID(); | ||
@@ -167,2 +158,3 @@ const accessTokenBuilder = this.getAccessTokenBuilder(session, jti); | ||
}; | ||
const searchParams = new URLSearchParams(); | ||
@@ -172,13 +164,3 @@ if (sendRefreshToken) { | ||
const refreshToken = await refreshTokenBuilder.sign(this.secret); | ||
// construct a short lived cookie for the client domain | ||
// which is client accessible. this lets us pass the refresh token | ||
// to the client so it can store it itself, even if the response | ||
// is a document. | ||
const refreshCookie = serialize(this.refreshCookieName, refreshToken, { | ||
httpOnly: false, | ||
sameSite: 'strict', | ||
path: '/', | ||
domain: clientDomain, | ||
}); | ||
headers['Set-Cookie'] += '; ' + refreshCookie; | ||
searchParams.set(this.refreshParam, refreshToken); | ||
} | ||
@@ -188,8 +170,11 @@ | ||
headers, | ||
searchParams, | ||
}; | ||
}; | ||
clearSession = (): Record<string, string> => { | ||
clearSession = () => { | ||
return { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
headers: { | ||
'Set-Cookie': `${this.options.cookieName}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`, | ||
}, | ||
}; | ||
@@ -253,7 +238,5 @@ }; | ||
private get refreshCookieName() { | ||
return ( | ||
this.options.refreshCookieName ?? `${this.options.cookieName}-refresh` | ||
); | ||
private get refreshParam() { | ||
return this.options.refreshParam ?? `refreshToken`; | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
99709
2019