From 64fd02217d0587d52d5dbadb39cec5547f5da16c Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 11 Dec 2024 21:25:57 -0500 Subject: [PATCH] Add refresh token (#44) * feat: extract refresh function * Version Packages (#43) Co-authored-by: github-actions[bot] * refresh token tweaks --------- Co-authored-by: Timo Strackfeldt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- examples/client/cloudflare-api/api.ts | 17 +++---- packages/openauth/src/client.ts | 68 ++++++++++++++++++--------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/examples/client/cloudflare-api/api.ts b/examples/client/cloudflare-api/api.ts index 076ac17f..e4d05841 100644 --- a/examples/client/cloudflare-api/api.ts +++ b/examples/client/cloudflare-api/api.ts @@ -46,7 +46,8 @@ export default { }, ) const resp = Response.json(verified.subject) - setSession(resp, verified.access, verified.refresh) + if (verified.tokens) + setSession(resp, verified.tokens.access, verified.tokens.refresh) return resp } catch (e) { console.error(e) @@ -58,21 +59,17 @@ export default { }, } -function setSession( - response: Response, - accessToken?: string, - refreshToken?: string, -) { - if (accessToken) { +function setSession(response: Response, access: string, refresh: string) { + if (access) { response.headers.append( "Set-Cookie", - `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`, + `access_token=${access}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`, ) } - if (refreshToken) { + if (refresh) { response.headers.append( "Set-Cookie", - `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`, + `refresh_token=${refresh}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`, ) } } diff --git a/packages/openauth/src/client.ts b/packages/openauth/src/client.ts index b4228702..d7230ca3 100644 --- a/packages/openauth/src/client.ts +++ b/packages/openauth/src/client.ts @@ -1,4 +1,10 @@ -import { createLocalJWKSet, errors, JSONWebKeySet, jwtVerify } from "jose" +import { + createLocalJWKSet, + errors, + JSONWebKeySet, + jwtVerify, + decodeJwt, +} from "jose" import { SubjectSchema } from "./session.js" import type { v1 } from "@standard-schema/spec" import { @@ -109,6 +115,39 @@ export function createClient(input: { refresh: json.refresh_token as string, } }, + async refresh( + refresh: string, + opts?: { + access?: string + }, + ) { + if (!opts?.access) { + const decoded = decodeJwt(refresh) + // allow 30s window for expiration + if (decoded.exp < Date.now() / 1000 + 30) { + return + } + } + const tokens = await f(issuer + "/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refresh, + }).toString(), + }) + const json = (await tokens.json()) as any + if (!tokens.ok) { + console.error(json) + throw new InvalidRefreshTokenError() + } + return { + access: json.access_token as string, + refresh: json.refresh_token as string, + } + }, async verify( subjects: T, token: string, @@ -151,30 +190,17 @@ export function createClient(input: { } } catch (e) { if (e instanceof errors.JWTExpired && options?.refresh) { - const wk = await getIssuer() - const tokens = await f(wk.token_endpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: options.refresh, - }).toString(), - }) - const json = (await tokens.json()) as any - if (!tokens.ok) { - console.error(json) - throw new InvalidRefreshTokenError() - } - const verified = await result.verify(subjects, json.access_token, { - refresh: json.refresh_token, + const tokens = await this.refresh(options.refresh) + + const verified = await result.verify(subjects, tokens.access, { + refresh: tokens.refresh, issuer, fetch: options?.fetch, }) + verified.tokens = { - access: json.access_token, - refresh: json.refresh_token, + access: tokens.access, + refresh: tokens.refresh, } return verified }