1- import { Injectable } from '@nestjs/common' ;
1+ import { Injectable , Inject } from '@nestjs/common' ;
22import { JwtService } from '@nestjs/jwt' ;
3- import { randomBytes } from 'crypto' ;
3+ import { randomBytes , createHmac } from 'crypto' ;
44import { SiwsService } from './siws.service' ;
5+ import { SUPABASE_CLIENT } from '../services/supabase.provider' ;
6+ import { SupabaseClient } from '@supabase/supabase-js' ;
7+ import { env } from '../config/env.config' ;
58
69/** In-memory nonce store (use Redis in production for multi-instance). */
710const nonces = new Map <
@@ -16,6 +19,7 @@ export class AuthService {
1619 constructor (
1720 private readonly jwtService : JwtService ,
1821 private readonly siwsService : SiwsService ,
22+ @Inject ( SUPABASE_CLIENT ) private readonly client : SupabaseClient ,
1923 ) { }
2024
2125 /**
@@ -72,8 +76,127 @@ export class AuthService {
7276 throw new Error ( 'Invalid signature' ) ;
7377 }
7478
79+ // issue both access + refresh tokens and persist refresh token hash
80+ return await this . issueTokens ( address ) ;
81+ }
82+
83+ private signAccessToken ( address : string ) {
7584 const payload = { address } ;
76- const accessToken = this . jwtService . sign ( payload ) ;
77- return { accessToken } ;
85+ return this . jwtService . sign ( payload , { expiresIn : env . jwt . expiresIn } ) ;
86+ }
87+
88+ private signRefreshToken ( address : string ) {
89+ const payload = { address, type : 'refresh' } ;
90+ return this . jwtService . sign ( payload , {
91+ expiresIn : env . jwt . refreshExpiresIn ,
92+ } ) ;
93+ }
94+
95+ private hashToken ( token : string ) {
96+ return createHmac ( 'sha256' , env . jwt . secret ) . update ( token ) . digest ( 'hex' ) ;
97+ }
98+
99+ private async storeRefreshHash (
100+ userAddress : string ,
101+ tokenHash : string ,
102+ expiresAtIso : string ,
103+ ) {
104+ // Upsert a refresh token row: allow multiple active tokens per user if desired.
105+ const { error } = await this . client . from ( 'refresh_tokens' ) . insert (
106+ [
107+ {
108+ user_address : userAddress ,
109+ token_hash : tokenHash ,
110+ created_at : new Date ( ) . toISOString ( ) ,
111+ last_used_at : new Date ( ) . toISOString ( ) ,
112+ expires_at : expiresAtIso ,
113+ revoked : false ,
114+ } ,
115+ ] ,
116+ { upsert : false } ,
117+ ) ;
118+
119+ if ( error ) {
120+ throw new Error ( 'Failed to persist refresh token' ) ;
121+ }
122+ }
123+
124+ async issueTokens ( address : string ) : Promise < { accessToken : string ; refreshToken : string } > {
125+ const accessToken = this . signAccessToken ( address ) ;
126+ const refreshToken = this . signRefreshToken ( address ) ;
127+
128+ // compute expiry for refresh token record
129+ const now = new Date ( ) ;
130+ // Parse refreshExpiresIn like '30d' or '7d' or seconds; for simplicity support days only
131+ const match = String ( env . jwt . refreshExpiresIn ) . match ( / ( \d + ) d $ / ) ;
132+ let expiresAt = new Date ( now . getTime ( ) ) ;
133+ if ( match ) {
134+ expiresAt . setDate ( expiresAt . getDate ( ) + parseInt ( match [ 1 ] , 10 ) ) ;
135+ } else {
136+ // fallback: 30 days
137+ expiresAt . setDate ( expiresAt . getDate ( ) + 30 ) ;
138+ }
139+
140+ const tokenHash = this . hashToken ( refreshToken ) ;
141+ await this . storeRefreshHash ( address , tokenHash , expiresAt . toISOString ( ) ) ;
142+
143+ return { accessToken, refreshToken } ;
144+ }
145+
146+ /**
147+ * Refresh flow: validate provided refresh token, rotate and issue new tokens.
148+ */
149+ async refresh ( refreshToken : string ) : Promise < { accessToken : string ; refreshToken : string } > {
150+ if ( ! refreshToken ) throw new Error ( 'refresh token required' ) ;
151+
152+ // verify token signature and expiry
153+ let payload : any ;
154+ try {
155+ payload = this . jwtService . verify ( refreshToken ) ;
156+ } catch ( err ) {
157+ throw new Error ( 'Invalid refresh token' ) ;
158+ }
159+
160+ if ( payload . type !== 'refresh' || ! payload . address ) {
161+ throw new Error ( 'Invalid refresh token payload' ) ;
162+ }
163+
164+ const tokenHash = this . hashToken ( refreshToken ) ;
165+
166+ // lookup token hash in DB
167+ const { data, error } = await this . client
168+ . from ( 'refresh_tokens' )
169+ . select ( '*' )
170+ . eq ( 'token_hash' , tokenHash )
171+ . eq ( 'revoked' , false )
172+ . limit ( 1 )
173+ . maybeSingle ( ) ;
174+
175+ if ( error || ! data ) {
176+ throw new Error ( 'Refresh token not found or revoked' ) ;
177+ }
178+
179+ // optional: check expires_at
180+ if ( data . expires_at && new Date ( data . expires_at ) < new Date ( ) ) {
181+ throw new Error ( 'Refresh token expired' ) ;
182+ }
183+
184+ // rotate: create new refresh token and replace stored hash
185+ const address = payload . address as string ;
186+ const newAccess = this . signAccessToken ( address ) ;
187+ const newRefresh = this . signRefreshToken ( address ) ;
188+ const newHash = this . hashToken ( newRefresh ) ;
189+
190+ const nowIso = new Date ( ) . toISOString ( ) ;
191+ const { error : updateErr } = await this . client
192+ . from ( 'refresh_tokens' )
193+ . update ( { token_hash : newHash , last_used_at : nowIso , updated_at : nowIso } )
194+ . eq ( 'token_hash' , tokenHash ) ;
195+
196+ if ( updateErr ) {
197+ throw new Error ( 'Failed to rotate refresh token' ) ;
198+ }
199+
200+ return { accessToken : newAccess , refreshToken : newRefresh } ;
78201 }
79202}
0 commit comments