11const jwt = require ( 'jsonwebtoken' ) ;
22const crypto = require ( 'crypto' ) ;
33const { redis } = require ( '@urbackend/common' ) ;
4+ const {
5+ getRefreshSessionKey,
6+ getUserSessionsKey,
7+ getRefreshSession,
8+ persistRefreshSession,
9+ revokeSessionChain
10+ } = require ( '@urbackend/common' ) ;
411
512const ACCESS_TOKEN_EXPIRES_IN = process . env . PUBLIC_AUTH_ACCESS_TOKEN_TTL || '15m' ;
613const REFRESH_TOKEN_TTL_SECONDS = Number ( process . env . PUBLIC_AUTH_REFRESH_TOKEN_TTL_SECONDS || 7 * 24 * 60 * 60 ) ;
7- const REFRESH_SESSION_PREFIX = 'project' ;
814
915const getRefreshCookieOptions = ( ) => ( {
1016 httpOnly : true ,
@@ -19,8 +25,6 @@ const clearCookieOptions = () => ({
1925 sameSite : process . env . NODE_ENV === 'production' ? 'none' : 'lax'
2026} ) ;
2127
22- const getRefreshSessionKey = ( tokenId ) => `${ REFRESH_SESSION_PREFIX } :auth:refresh:session:${ tokenId } ` ;
23-
2428const hashRefreshToken = ( token ) => crypto . createHash ( 'sha256' ) . update ( token ) . digest ( 'hex' ) ;
2529
2630const toProjectIdString = ( projectId ) => projectId ?. toString ?. ( ) || String ( projectId ) ;
@@ -79,7 +83,7 @@ const parseRefreshToken = (rawToken) => {
7983 return { tokenId, tokenSecret } ;
8084} ;
8185
82- const saveRefreshSession = async ( { tokenId, rawToken, projectId, userId, rotatedFrom = null , isUsed = false , rotatedTo = null } ) => {
86+ const saveRefreshSession = async ( { tokenId, rawToken, projectId, userId, rotatedFrom = null , isUsed = false , rotatedTo = null , ip = null , userAgent = null } ) => {
8387 const nowIso = new Date ( ) . toISOString ( ) ;
8488 const session = {
8589 tokenId,
@@ -90,29 +94,17 @@ const saveRefreshSession = async ({ tokenId, rawToken, projectId, userId, rotate
9094 rotatedTo,
9195 isUsed,
9296 revokedAt : null ,
97+ ip,
98+ userAgent,
9399 createdAt : nowIso ,
94100 lastUsedAt : nowIso ,
95101 expiresAt : new Date ( Date . now ( ) + REFRESH_TOKEN_TTL_SECONDS * 1000 ) . toISOString ( )
96102 } ;
97103 await redis . set ( getRefreshSessionKey ( tokenId ) , JSON . stringify ( session ) , 'EX' , REFRESH_TOKEN_TTL_SECONDS ) ;
104+ await redis . sadd ( getUserSessionsKey ( projectId , userId ) , tokenId ) ;
98105 return session ;
99106} ;
100107
101- const getRefreshSession = async ( tokenId ) => {
102- const raw = await redis . get ( getRefreshSessionKey ( tokenId ) ) ;
103- if ( ! raw ) return null ;
104- try {
105- return JSON . parse ( raw ) ;
106- } catch {
107- return null ;
108- }
109- } ;
110-
111- const persistRefreshSession = async ( session ) => {
112- const ttl = Math . max ( 1 , Math . floor ( ( new Date ( session . expiresAt ) . getTime ( ) - Date . now ( ) ) / 1000 ) ) ;
113- await redis . set ( getRefreshSessionKey ( session . tokenId ) , JSON . stringify ( session ) , 'EX' , ttl ) ;
114- } ;
115-
116108const incrementRateCounter = async ( key , windowSeconds ) => {
117109 const count = await redis . incr ( key ) ;
118110 if ( count === 1 ) {
@@ -139,34 +131,31 @@ const assertRefreshRateLimits = async ({ req, tokenId, userId }) => {
139131 return { limited : false } ;
140132} ;
141133
142- const revokeSessionChain = async ( startTokenId ) => {
143- let currentTokenId = startTokenId ;
144- const visited = new Set ( ) ;
145-
146- while ( currentTokenId && ! visited . has ( currentTokenId ) ) {
147- visited . add ( currentTokenId ) ;
148- const session = await getRefreshSession ( currentTokenId ) ;
149- if ( ! session ) break ;
150- session . revokedAt = new Date ( ) . toISOString ( ) ;
151- session . lastUsedAt = new Date ( ) . toISOString ( ) ;
152- await persistRefreshSession ( session ) ;
153- currentTokenId = session . rotatedTo || null ;
154- }
155- } ;
134+
156135
157136const clearRefreshCookie = ( res ) => {
158137 res . clearCookie ( 'refreshToken' , clearCookieOptions ( ) ) ;
159138} ;
160139
161- const issueAuthTokens = async ( { project, userId, res, rotatedFrom = null } ) => {
140+ const issueAuthTokens = async ( { project, userId, req , res, rotatedFrom = null } ) => {
162141 const accessToken = signAccessToken ( project , userId ) ;
163142 const { tokenId, rawToken } = generateRefreshToken ( ) ;
143+
144+ let ip = null ;
145+ let userAgent = null ;
146+ if ( req ) {
147+ ip = readRequestIp ( req ) ;
148+ userAgent = req . headers ?. [ 'user-agent' ] || 'unknown' ;
149+ }
150+
164151 await saveRefreshSession ( {
165152 tokenId,
166153 rawToken,
167154 projectId : project . _id ,
168155 userId,
169- rotatedFrom
156+ rotatedFrom,
157+ ip,
158+ userAgent
170159 } ) ;
171160
172161 res . cookie ( 'refreshToken' , rawToken , getRefreshCookieOptions ( ) ) ;
@@ -181,12 +170,9 @@ const issueAuthTokens = async ({ project, userId, res, rotatedFrom = null }) =>
181170module . exports = {
182171 assertRefreshRateLimits,
183172 clearRefreshCookie,
184- getRefreshSession,
185173 hashRefreshToken,
186174 issueAuthTokens,
187175 parseRefreshToken,
188176 readRefreshTokenFromRequest,
189- revokeSessionChain,
190- shouldExposeRefreshToken,
191- persistRefreshSession
177+ shouldExposeRefreshToken
192178} ;
0 commit comments