1- import type {
2- AuthCodeData ,
3- ClientMetadata ,
4- LexiconPermissionSet ,
5- OAuthStorage ,
6- PARData ,
7- TokenData ,
1+ import {
2+ type AuthCodeData ,
3+ type ClientMetadata ,
4+ type OAuthStorage ,
5+ type LexiconPermissionSet ,
6+ type PARData ,
7+ type TokenData ,
8+ REFRESH_TOKEN_TTL ,
89} from "@getcirrus/oauth-provider" ;
910
1011/**
@@ -53,13 +54,11 @@ export class SqliteOAuthStorage implements OAuthStorage {
5354 scope TEXT NOT NULL,
5455 dpop_jkt TEXT,
5556 issued_at INTEGER NOT NULL,
56- expires_at INTEGER NOT NULL,
57+ access_expires_at INTEGER NOT NULL,
58+ refresh_expires_at INTEGER NOT NULL,
5759 revoked INTEGER NOT NULL DEFAULT 0
5860 );
5961
60- CREATE INDEX IF NOT EXISTS idx_tokens_refresh ON oauth_tokens(refresh_token);
61- CREATE INDEX IF NOT EXISTS idx_tokens_sub ON oauth_tokens(sub);
62- CREATE INDEX IF NOT EXISTS idx_tokens_expires ON oauth_tokens(expires_at);
6362
6463 -- Cached client metadata
6564 CREATE TABLE IF NOT EXISTS oauth_clients (
@@ -112,8 +111,10 @@ export class SqliteOAuthStorage implements OAuthStorage {
112111 CREATE INDEX IF NOT EXISTS idx_permission_sets_expires ON oauth_permission_sets(expires_at);
113112 ` ) ;
114113
115- // Migration: add columns for client auth metadata if missing
114+ // Migrations for older OAuth schema versions
116115 this . migrateClientTable ( ) ;
116+ this . migrateTokensTable ( ) ;
117+ this . ensureTokenIndexes ( ) ;
117118 }
118119
119120 private migrateClientTable ( ) : void {
@@ -132,14 +133,75 @@ export class SqliteOAuthStorage implements OAuthStorage {
132133 }
133134 }
134135
136+ private migrateTokensTable ( ) : void {
137+ const columns = this . sql
138+ . exec ( "PRAGMA table_info(oauth_tokens)" )
139+ . toArray ( )
140+ . map ( ( r ) => r . name as string ) ;
141+
142+ if ( columns . length === 0 ) return ;
143+ if (
144+ columns . includes ( "access_expires_at" ) &&
145+ columns . includes ( "refresh_expires_at" )
146+ ) {
147+ return ;
148+ }
149+
150+ this . sql . exec ( `
151+ CREATE TABLE oauth_tokens_new (
152+ access_token TEXT PRIMARY KEY,
153+ refresh_token TEXT NOT NULL UNIQUE,
154+ client_id TEXT NOT NULL,
155+ sub TEXT NOT NULL,
156+ scope TEXT NOT NULL,
157+ dpop_jkt TEXT,
158+ issued_at INTEGER NOT NULL,
159+ access_expires_at INTEGER NOT NULL,
160+ refresh_expires_at INTEGER NOT NULL,
161+ revoked INTEGER NOT NULL DEFAULT 0
162+ );
163+ ` ) ;
164+
165+ this . sql . exec (
166+ `INSERT INTO oauth_tokens_new (
167+ access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at, access_expires_at, refresh_expires_at, revoked
168+ )
169+ SELECT
170+ access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at,
171+ expires_at AS access_expires_at,
172+ MAX(expires_at, issued_at + ?) AS refresh_expires_at,
173+ revoked
174+ FROM oauth_tokens` ,
175+ REFRESH_TOKEN_TTL ,
176+ ) ;
177+
178+ this . sql . exec ( "DROP TABLE oauth_tokens" ) ;
179+ this . sql . exec ( "ALTER TABLE oauth_tokens_new RENAME TO oauth_tokens" ) ;
180+ }
181+
182+ private ensureTokenIndexes ( ) : void {
183+ this . sql . exec (
184+ "CREATE INDEX IF NOT EXISTS idx_tokens_refresh ON oauth_tokens(refresh_token)" ,
185+ ) ;
186+ this . sql . exec (
187+ "CREATE INDEX IF NOT EXISTS idx_tokens_sub ON oauth_tokens(sub)" ,
188+ ) ;
189+ this . sql . exec (
190+ "CREATE INDEX IF NOT EXISTS idx_tokens_access_expires ON oauth_tokens(access_expires_at)" ,
191+ ) ;
192+ this . sql . exec (
193+ "CREATE INDEX IF NOT EXISTS idx_tokens_refresh_expires ON oauth_tokens(refresh_expires_at)" ,
194+ ) ;
195+ }
196+
135197 /**
136198 * Clean up expired entries. Should be called periodically.
137199 */
138200 cleanup ( ) : void {
139201 const now = Date . now ( ) ;
140202 this . sql . exec ( "DELETE FROM oauth_auth_codes WHERE expires_at < ?" , now ) ;
141203 this . sql . exec (
142- "DELETE FROM oauth_tokens WHERE expires_at < ? AND revoked = 0 " ,
204+ "DELETE FROM oauth_tokens WHERE refresh_expires_at < ?" ,
143205 now ,
144206 ) ;
145207 this . sql . exec ( "DELETE FROM oauth_par_requests WHERE expires_at < ?" , now ) ;
@@ -219,24 +281,25 @@ export class SqliteOAuthStorage implements OAuthStorage {
219281 async saveTokens ( data : TokenData ) : Promise < void > {
220282 this . sql . exec (
221283 `INSERT INTO oauth_tokens
222- (access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at, expires_at , revoked)
223- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
284+ (access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at, access_expires_at, refresh_expires_at , revoked)
285+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? )` ,
224286 data . accessToken ,
225287 data . refreshToken ,
226288 data . clientId ,
227289 data . sub ,
228290 data . scope ,
229291 data . dpopJkt ?? null ,
230292 data . issuedAt ,
231- data . expiresAt ,
293+ data . accessExpiresAt ,
294+ data . refreshExpiresAt ,
232295 data . revoked ? 1 : 0 ,
233296 ) ;
234297 }
235298
236299 async getTokenByAccess ( accessToken : string ) : Promise < TokenData | null > {
237300 const rows = this . sql
238301 . exec (
239- `SELECT access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at, expires_at , revoked
302+ `SELECT access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at, access_expires_at, refresh_expires_at , revoked
240303 FROM oauth_tokens WHERE access_token = ?` ,
241304 accessToken ,
242305 )
@@ -246,9 +309,9 @@ export class SqliteOAuthStorage implements OAuthStorage {
246309
247310 const row = rows [ 0 ] ! ;
248311 const revoked = Boolean ( row . revoked ) ;
249- const expiresAt = row . expires_at as number ;
312+ const accessExpiresAt = row . access_expires_at as number ;
250313
251- if ( revoked || Date . now ( ) > expiresAt ) {
314+ if ( revoked || Date . now ( ) > accessExpiresAt ) {
252315 return null ;
253316 }
254317
@@ -260,15 +323,16 @@ export class SqliteOAuthStorage implements OAuthStorage {
260323 scope : row . scope as string ,
261324 dpopJkt : ( row . dpop_jkt as string ) ?? undefined ,
262325 issuedAt : row . issued_at as number ,
263- expiresAt,
326+ accessExpiresAt,
327+ refreshExpiresAt : row . refresh_expires_at as number ,
264328 revoked,
265329 } ;
266330 }
267331
268332 async getTokenByRefresh ( refreshToken : string ) : Promise < TokenData | null > {
269333 const rows = this . sql
270334 . exec (
271- `SELECT access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at, expires_at , revoked
335+ `SELECT access_token, refresh_token, client_id, sub, scope, dpop_jkt, issued_at, access_expires_at, refresh_expires_at , revoked
272336 FROM oauth_tokens WHERE refresh_token = ?` ,
273337 refreshToken ,
274338 )
@@ -279,7 +343,9 @@ export class SqliteOAuthStorage implements OAuthStorage {
279343 const row = rows [ 0 ] ! ;
280344 const revoked = Boolean ( row . revoked ) ;
281345
282- if ( revoked ) return null ;
346+ const refreshExpiresAt = row . refresh_expires_at as number ;
347+
348+ if ( revoked || Date . now ( ) > refreshExpiresAt ) return null ;
283349
284350 return {
285351 accessToken : row . access_token as string ,
@@ -289,7 +355,8 @@ export class SqliteOAuthStorage implements OAuthStorage {
289355 scope : row . scope as string ,
290356 dpopJkt : ( row . dpop_jkt as string ) ?? undefined ,
291357 issuedAt : row . issued_at as number ,
292- expiresAt : row . expires_at as number ,
358+ accessExpiresAt : row . access_expires_at as number ,
359+ refreshExpiresAt,
293360 revoked,
294361 } ;
295362 }
0 commit comments