@rapidraptor/auth-server 0.2.5 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/firebase/admin.d.ts.map +1 -1
- package/dist/firebase/admin.js +19 -2
- package/dist/firebase/admin.js.map +1 -1
- package/dist/middleware/authMiddleware.d.ts.map +1 -1
- package/dist/middleware/authMiddleware.js +11 -1
- package/dist/middleware/authMiddleware.js.map +1 -1
- package/dist/middleware/authMiddleware.test.js +3 -0
- package/dist/middleware/authMiddleware.test.js.map +1 -1
- package/dist/session/firestoreSync.d.ts.map +1 -1
- package/dist/session/firestoreSync.js +4 -3
- package/dist/session/firestoreSync.js.map +1 -1
- package/dist/session/firestoreSync.test.js +12 -6
- package/dist/session/firestoreSync.test.js.map +1 -1
- package/dist/session/sessionCache.test.js +13 -2
- package/dist/session/sessionCache.test.js.map +1 -1
- package/dist/session/sessionService.d.ts +37 -26
- package/dist/session/sessionService.d.ts.map +1 -1
- package/dist/session/sessionService.js +95 -83
- package/dist/session/sessionService.js.map +1 -1
- package/dist/session/sessionService.test.js +85 -119
- package/dist/session/sessionService.test.js.map +1 -1
- package/package.json +2 -2
|
@@ -9,8 +9,9 @@ export declare class TokenRevokedError extends Error {
|
|
|
9
9
|
constructor();
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Main session management service
|
|
13
|
-
*
|
|
12
|
+
* Main session management service. Sessions are stored in Firestore as user_sessions/{sessionId}
|
|
13
|
+
* (sessionId is a UUID). Lookup is by userId (from JWT sub); cache is keyed by userId.
|
|
14
|
+
* Uses cache-first strategy with Firestore fallback.
|
|
14
15
|
*/
|
|
15
16
|
export declare class SessionService {
|
|
16
17
|
private cache;
|
|
@@ -21,6 +22,13 @@ export declare class SessionService {
|
|
|
21
22
|
private logoutsCollectionName;
|
|
22
23
|
private logoutTtlMs;
|
|
23
24
|
constructor(cache: SessionCache, firestoreSync: FirestoreSync, firestore: Firestore, inactivityTimeout: number, collectionName?: string, logoutsCollectionName?: string, logoutTtlMs?: number);
|
|
25
|
+
/**
|
|
26
|
+
* Find active session for user.
|
|
27
|
+
* Sessions are stored as user_sessions/{sessionId}; we query by userId (from JWT sub) since
|
|
28
|
+
* sessionId is not sent by the client. orderBy lastActivityAt desc ensures a deterministic
|
|
29
|
+
* result when multiple active sessions exist for the same user (e.g. race or bug).
|
|
30
|
+
*/
|
|
31
|
+
private findActiveSessionByUserId;
|
|
24
32
|
/**
|
|
25
33
|
* Validate session and return detailed status (cache-first lookup with Firestore fallback)
|
|
26
34
|
* Returns explicit status instead of boolean to avoid requiring additional calls
|
|
@@ -33,64 +41,67 @@ export declare class SessionService {
|
|
|
33
41
|
*/
|
|
34
42
|
isSessionValid(userId: string): Promise<boolean>;
|
|
35
43
|
/**
|
|
36
|
-
* Check if session exists in Firestore (regardless of expiration)
|
|
37
|
-
*
|
|
44
|
+
* Check if a session document exists in Firestore for this user (regardless of expiration).
|
|
45
|
+
* Uses a query by userId since documents are keyed by sessionId.
|
|
38
46
|
*/
|
|
39
47
|
sessionExists(userId: string): Promise<boolean>;
|
|
40
48
|
/**
|
|
41
|
-
* Ensure session exists (idempotent
|
|
42
|
-
* Returns true if session was created, false if
|
|
43
|
-
*
|
|
49
|
+
* Ensure a session exists for the user (idempotent). Creates a new session only when none
|
|
50
|
+
* exists or there is a data integrity issue. Returns true if a session was created, false if
|
|
51
|
+
* one already existed and is valid.
|
|
44
52
|
*
|
|
45
|
-
* @param userId -
|
|
46
|
-
* @param tokenIssuedAt - Optional JWT
|
|
47
|
-
* @throws TokenRevokedError if token was issued before logout
|
|
48
|
-
* @throws Error if session is expired (user must logout and
|
|
53
|
+
* @param userId - User ID (from JWT sub)
|
|
54
|
+
* @param tokenIssuedAt - Optional JWT iat; if provided, we reject tokens issued before logout
|
|
55
|
+
* @throws TokenRevokedError if token was issued before logout
|
|
56
|
+
* @throws Error if session is expired (user must logout and log in again; we do not auto-recreate)
|
|
49
57
|
*/
|
|
50
58
|
ensureSession(userId: string, tokenIssuedAt?: Date): Promise<boolean>;
|
|
51
59
|
/**
|
|
52
|
-
* Create new session
|
|
60
|
+
* Create a new session. Session ID is an independent UUID (not derived from userId), so each
|
|
61
|
+
* login gets a distinct session and logout/re-login works correctly.
|
|
53
62
|
*/
|
|
54
63
|
createSession(userId: string): Promise<void>;
|
|
55
64
|
/**
|
|
56
|
-
* Update last activity timestamp
|
|
57
|
-
*
|
|
65
|
+
* Update last activity timestamp and extend expiration. Cache is updated immediately for
|
|
66
|
+
* fast reads; Firestore write is queued and throttled (see FirestoreSync).
|
|
58
67
|
*/
|
|
59
68
|
updateLastActivity(userId: string): Promise<void>;
|
|
60
69
|
/**
|
|
61
|
-
* Clear session (logout)
|
|
62
|
-
*
|
|
70
|
+
* Clear session (logout). Removes all session documents for this user and records the logout
|
|
71
|
+
* so tokens issued before this time can be rejected (see wasTokenIssuedBeforeLogout).
|
|
63
72
|
*/
|
|
64
73
|
clearSession(userId: string): Promise<void>;
|
|
65
74
|
/**
|
|
66
|
-
* Check if JWT
|
|
67
|
-
*
|
|
75
|
+
* Check if the JWT was issued before the user's last logout.
|
|
76
|
+
* Logout records are stored at user_logouts/{userId} (one per user). We use this to reject
|
|
77
|
+
* tokens that were issued before logout, since JWTs cannot be revoked directly.
|
|
78
|
+
* Returns true if token was issued before logout (token should be rejected).
|
|
68
79
|
*/
|
|
69
80
|
wasTokenIssuedBeforeLogout(userId: string, tokenIssuedAt: Date): Promise<boolean>;
|
|
70
81
|
/**
|
|
71
|
-
* Warmup cache from Firestore
|
|
72
|
-
*
|
|
73
|
-
* Also
|
|
82
|
+
* Warmup cache from Firestore on startup. Loads all non-expired sessions into the in-memory
|
|
83
|
+
* cache (keyed by userId). Document IDs in user_sessions are sessionIds; we cache by userId
|
|
84
|
+
* for lookup. Also performs lazy deletion of expired session documents.
|
|
74
85
|
*/
|
|
75
86
|
warmupCache(): Promise<void>;
|
|
76
87
|
/**
|
|
77
|
-
* Parse Firestore document
|
|
88
|
+
* Parse Firestore session document (timestamp fields may be Firestore Timestamp or Date) into SessionInfo.
|
|
78
89
|
*/
|
|
79
90
|
private parseFirestoreDocument;
|
|
80
91
|
/**
|
|
81
|
-
* Convert SessionInfo to Firestore document format
|
|
92
|
+
* Convert SessionInfo to Firestore document format for user_sessions collection.
|
|
82
93
|
*/
|
|
83
94
|
private toFirestoreDocument;
|
|
84
95
|
/**
|
|
85
|
-
*
|
|
96
|
+
* Normalize Firestore Timestamp or Date to JavaScript Date.
|
|
86
97
|
*/
|
|
87
98
|
private toDate;
|
|
88
99
|
/**
|
|
89
|
-
* Parse Firestore logout document
|
|
100
|
+
* Parse Firestore logout document (user_logouts collection) into typed fields.
|
|
90
101
|
*/
|
|
91
102
|
private parseLogoutDocument;
|
|
92
103
|
/**
|
|
93
|
-
* Convert logout info to Firestore document format
|
|
104
|
+
* Convert logout info to Firestore document format for user_logouts collection.
|
|
94
105
|
*/
|
|
95
106
|
private toLogoutDocument;
|
|
96
107
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionService.d.ts","sourceRoot":"","sources":["../../src/session/sessionService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sessionService.d.ts","sourceRoot":"","sources":["../../src/session/sessionService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAOnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAGnE;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;;CAK3C;AAED;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,WAAW,CAAS;gBAG1B,KAAK,EAAE,YAAY,EACnB,aAAa,EAAE,aAAa,EAC5B,SAAS,EAAE,SAAS,EACpB,iBAAiB,EAAE,MAAM,EACzB,cAAc,GAAE,MAAoD,EACpE,qBAAqB,GAAE,MAAmD,EAC1E,WAAW,GAAE,MAA+B;IAW9C;;;;;OAKG;YACW,yBAAyB;IAgBvC;;;;OAIG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAuCvE;;;OAGG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtD;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAcrD;;;;;;;;;OASG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAuB3E;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBlD;;;OAGG;IACG,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBvD;;;OAGG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BjD;;;;;OAKG;IACG,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBvF;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAgDlC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAU3B;;OAEG;IACH,OAAO,CAAC,MAAM;IAOd;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAY3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAWzB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
1
2
|
import { SessionValidationStatus } from '@rapidraptor/auth-shared';
|
|
2
3
|
import { DEFAULTS } from '@rapidraptor/auth-shared';
|
|
3
4
|
/**
|
|
@@ -10,8 +11,9 @@ export class TokenRevokedError extends Error {
|
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
|
-
* Main session management service
|
|
14
|
-
*
|
|
14
|
+
* Main session management service. Sessions are stored in Firestore as user_sessions/{sessionId}
|
|
15
|
+
* (sessionId is a UUID). Lookup is by userId (from JWT sub); cache is keyed by userId.
|
|
16
|
+
* Uses cache-first strategy with Firestore fallback.
|
|
15
17
|
*/
|
|
16
18
|
export class SessionService {
|
|
17
19
|
cache;
|
|
@@ -30,44 +32,59 @@ export class SessionService {
|
|
|
30
32
|
this.logoutsCollectionName = logoutsCollectionName;
|
|
31
33
|
this.logoutTtlMs = logoutTtlMs;
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Find active session for user.
|
|
37
|
+
* Sessions are stored as user_sessions/{sessionId}; we query by userId (from JWT sub) since
|
|
38
|
+
* sessionId is not sent by the client. orderBy lastActivityAt desc ensures a deterministic
|
|
39
|
+
* result when multiple active sessions exist for the same user (e.g. race or bug).
|
|
40
|
+
*/
|
|
41
|
+
async findActiveSessionByUserId(userId) {
|
|
42
|
+
const snapshot = await this.firestore
|
|
43
|
+
.collection(this.collectionName)
|
|
44
|
+
.where('userId', '==', userId)
|
|
45
|
+
.where('expiresAt', '>', new Date())
|
|
46
|
+
.orderBy('lastActivityAt', 'desc')
|
|
47
|
+
.limit(1)
|
|
48
|
+
.get();
|
|
49
|
+
if (snapshot.empty) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const data = snapshot.docs[0].data();
|
|
53
|
+
return this.parseFirestoreDocument(data);
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* Validate session and return detailed status (cache-first lookup with Firestore fallback)
|
|
35
57
|
* Returns explicit status instead of boolean to avoid requiring additional calls
|
|
36
58
|
* to determine why a session is invalid
|
|
37
59
|
*/
|
|
38
60
|
async validateSession(userId) {
|
|
39
|
-
//
|
|
61
|
+
// Cache is keyed by userId (one session per user in cache)
|
|
40
62
|
const cachedSession = this.cache.get(userId);
|
|
41
63
|
// Step 1: Check for userId mismatch (data integrity issue)
|
|
42
64
|
if (cachedSession && cachedSession.userId !== userId) {
|
|
43
|
-
// Data integrity issue - invalidate cache entry
|
|
44
65
|
this.cache.clear(userId);
|
|
45
66
|
return SessionValidationStatus.DATA_INTEGRITY_ERROR;
|
|
46
67
|
}
|
|
47
|
-
// Step 2: Check if cached session is valid
|
|
68
|
+
// Step 2: Check if cached session is valid and not expired
|
|
48
69
|
if (cachedSession && !this.cache.isExpired(userId)) {
|
|
49
|
-
// Cached session is valid and userId matches
|
|
50
70
|
return SessionValidationStatus.VALID;
|
|
51
71
|
}
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
// Step 2b: Cached session exists but is expired — return EXPIRED so caller can require re-login
|
|
73
|
+
if (cachedSession && this.cache.isExpired(userId)) {
|
|
74
|
+
return SessionValidationStatus.EXPIRED;
|
|
75
|
+
}
|
|
76
|
+
// Step 3: Cache miss — query Firestore by userId (sessions live under user_sessions/{sessionId})
|
|
77
|
+
const session = await this.findActiveSessionByUserId(userId);
|
|
78
|
+
if (!session) {
|
|
56
79
|
return SessionValidationStatus.NOT_FOUND;
|
|
57
80
|
}
|
|
58
|
-
// Parse Firestore document
|
|
59
|
-
const data = doc.data();
|
|
60
|
-
const session = this.parseFirestoreDocument(data);
|
|
61
|
-
// Verify session userId matches requested userId (data integrity check)
|
|
62
81
|
if (session.userId !== userId) {
|
|
63
|
-
// Data integrity issue - session document userId doesn't match document ID
|
|
64
82
|
return SessionValidationStatus.DATA_INTEGRITY_ERROR;
|
|
65
83
|
}
|
|
66
|
-
// Check expiration
|
|
67
84
|
if (new Date() > session.expiresAt) {
|
|
68
85
|
return SessionValidationStatus.EXPIRED;
|
|
69
86
|
}
|
|
70
|
-
//
|
|
87
|
+
// Repopulate cache for future requests
|
|
71
88
|
this.cache.set(userId, session);
|
|
72
89
|
return SessionValidationStatus.VALID;
|
|
73
90
|
}
|
|
@@ -80,161 +97,154 @@ export class SessionService {
|
|
|
80
97
|
return status === SessionValidationStatus.VALID;
|
|
81
98
|
}
|
|
82
99
|
/**
|
|
83
|
-
* Check if session exists in Firestore (regardless of expiration)
|
|
84
|
-
*
|
|
100
|
+
* Check if a session document exists in Firestore for this user (regardless of expiration).
|
|
101
|
+
* Uses a query by userId since documents are keyed by sessionId.
|
|
85
102
|
*/
|
|
86
103
|
async sessionExists(userId) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
const snapshot = await this.firestore
|
|
105
|
+
.collection(this.collectionName)
|
|
106
|
+
.where('userId', '==', userId)
|
|
107
|
+
.limit(1)
|
|
108
|
+
.get();
|
|
109
|
+
if (snapshot.empty) {
|
|
90
110
|
return false;
|
|
91
111
|
}
|
|
92
|
-
|
|
93
|
-
const data = doc.data();
|
|
112
|
+
const data = snapshot.docs[0].data();
|
|
94
113
|
return data.userId === userId;
|
|
95
114
|
}
|
|
96
115
|
/**
|
|
97
|
-
* Ensure session exists (idempotent
|
|
98
|
-
* Returns true if session was created, false if
|
|
99
|
-
*
|
|
116
|
+
* Ensure a session exists for the user (idempotent). Creates a new session only when none
|
|
117
|
+
* exists or there is a data integrity issue. Returns true if a session was created, false if
|
|
118
|
+
* one already existed and is valid.
|
|
100
119
|
*
|
|
101
|
-
* @param userId -
|
|
102
|
-
* @param tokenIssuedAt - Optional JWT
|
|
103
|
-
* @throws TokenRevokedError if token was issued before logout
|
|
104
|
-
* @throws Error if session is expired (user must logout and
|
|
120
|
+
* @param userId - User ID (from JWT sub)
|
|
121
|
+
* @param tokenIssuedAt - Optional JWT iat; if provided, we reject tokens issued before logout
|
|
122
|
+
* @throws TokenRevokedError if token was issued before logout
|
|
123
|
+
* @throws Error if session is expired (user must logout and log in again; we do not auto-recreate)
|
|
105
124
|
*/
|
|
106
125
|
async ensureSession(userId, tokenIssuedAt) {
|
|
107
|
-
// Check if token was issued before logout (if tokenIssuedAt provided)
|
|
108
126
|
if (tokenIssuedAt) {
|
|
109
127
|
const wasIssuedBeforeLogout = await this.wasTokenIssuedBeforeLogout(userId, tokenIssuedAt);
|
|
110
128
|
if (wasIssuedBeforeLogout) {
|
|
111
129
|
throw new TokenRevokedError();
|
|
112
130
|
}
|
|
113
131
|
}
|
|
114
|
-
// Check session validation status
|
|
115
132
|
const status = await this.validateSession(userId);
|
|
116
133
|
if (status === SessionValidationStatus.VALID) {
|
|
117
|
-
return false;
|
|
134
|
+
return false;
|
|
118
135
|
}
|
|
119
|
-
// If session is expired, don't recreate it - user must logout and relogin
|
|
120
136
|
if (status === SessionValidationStatus.EXPIRED) {
|
|
121
137
|
throw new Error('Session has expired. Please logout and login again.');
|
|
122
138
|
}
|
|
123
|
-
//
|
|
124
|
-
// Note: For DATA_INTEGRITY_ERROR, we recreate the session to fix the corruption
|
|
125
|
-
// createSession is idempotent (uses set() which overwrites)
|
|
139
|
+
// NOT_FOUND or DATA_INTEGRITY_ERROR: create a new session (new sessionId)
|
|
126
140
|
await this.createSession(userId);
|
|
127
|
-
return true;
|
|
141
|
+
return true;
|
|
128
142
|
}
|
|
129
143
|
/**
|
|
130
|
-
* Create new session
|
|
144
|
+
* Create a new session. Session ID is an independent UUID (not derived from userId), so each
|
|
145
|
+
* login gets a distinct session and logout/re-login works correctly.
|
|
131
146
|
*/
|
|
132
147
|
async createSession(userId) {
|
|
148
|
+
const sessionId = randomUUID();
|
|
133
149
|
const now = new Date();
|
|
134
150
|
const expiresAt = new Date(now.getTime() + this.inactivityTimeout);
|
|
135
151
|
const session = {
|
|
152
|
+
sessionId,
|
|
136
153
|
userId,
|
|
137
154
|
createdAt: now,
|
|
138
155
|
lastActivityAt: now,
|
|
139
156
|
expiresAt,
|
|
140
157
|
};
|
|
141
|
-
//
|
|
158
|
+
// Cache is keyed by userId for lookup; document in Firestore is keyed by sessionId
|
|
142
159
|
this.cache.set(userId, session);
|
|
143
|
-
|
|
144
|
-
const docRef = this.firestore.collection(this.collectionName).doc(userId);
|
|
160
|
+
const docRef = this.firestore.collection(this.collectionName).doc(sessionId);
|
|
145
161
|
await docRef.set(this.toFirestoreDocument(session));
|
|
146
162
|
}
|
|
147
163
|
/**
|
|
148
|
-
* Update last activity timestamp
|
|
149
|
-
*
|
|
164
|
+
* Update last activity timestamp and extend expiration. Cache is updated immediately for
|
|
165
|
+
* fast reads; Firestore write is queued and throttled (see FirestoreSync).
|
|
150
166
|
*/
|
|
151
167
|
async updateLastActivity(userId) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return; // Session doesn't exist or is expired
|
|
168
|
+
const status = await this.validateSession(userId);
|
|
169
|
+
if (status !== SessionValidationStatus.VALID) {
|
|
170
|
+
return; // Session doesn't exist or is expired; nothing to update
|
|
156
171
|
}
|
|
157
|
-
// Session is guaranteed to be in cache and valid at this point
|
|
158
172
|
const session = this.cache.get(userId);
|
|
159
173
|
if (!session) {
|
|
160
|
-
// Should not happen, but handle gracefully
|
|
161
174
|
return;
|
|
162
175
|
}
|
|
163
|
-
// Update cache immediately (fast path)
|
|
164
176
|
const updatedSession = {
|
|
165
177
|
...session,
|
|
166
178
|
lastActivityAt: new Date(),
|
|
167
179
|
expiresAt: new Date(Date.now() + this.inactivityTimeout),
|
|
168
180
|
};
|
|
169
181
|
this.cache.set(userId, updatedSession);
|
|
170
|
-
//
|
|
182
|
+
// Firestore write uses session.sessionId as document ID; throttled per user
|
|
171
183
|
this.firestoreSync.queueWrite(userId, updatedSession);
|
|
172
184
|
}
|
|
173
185
|
/**
|
|
174
|
-
* Clear session (logout)
|
|
175
|
-
*
|
|
186
|
+
* Clear session (logout). Removes all session documents for this user and records the logout
|
|
187
|
+
* so tokens issued before this time can be rejected (see wasTokenIssuedBeforeLogout).
|
|
176
188
|
*/
|
|
177
189
|
async clearSession(userId) {
|
|
178
|
-
// Clear cache
|
|
179
190
|
this.cache.clear(userId);
|
|
180
|
-
// Store logout timestamp to prevent re-authentication with old JWTs
|
|
181
|
-
// This addresses the JWT limitation: JWTs cannot be revoked, but we can track
|
|
182
|
-
// when a user logged out and reject tokens issued before that time
|
|
183
191
|
const now = new Date();
|
|
184
192
|
const expiresAt = new Date(now.getTime() + this.logoutTtlMs);
|
|
193
|
+
// Logout record is keyed by userId (per-user, not per-session); used for token revocation check
|
|
185
194
|
const logoutRef = this.firestore.collection(this.logoutsCollectionName).doc(userId);
|
|
186
195
|
await logoutRef.set(this.toLogoutDocument({
|
|
187
196
|
userId,
|
|
188
197
|
loggedOutAt: now,
|
|
189
198
|
expiresAt,
|
|
190
199
|
}));
|
|
191
|
-
//
|
|
192
|
-
const
|
|
193
|
-
|
|
200
|
+
// Sessions are stored as user_sessions/{sessionId}; query by userId and delete each document
|
|
201
|
+
const snapshot = await this.firestore
|
|
202
|
+
.collection(this.collectionName)
|
|
203
|
+
.where('userId', '==', userId)
|
|
204
|
+
.get();
|
|
205
|
+
for (const doc of snapshot.docs) {
|
|
206
|
+
await doc.ref.delete();
|
|
207
|
+
}
|
|
194
208
|
}
|
|
195
209
|
/**
|
|
196
|
-
* Check if JWT
|
|
197
|
-
*
|
|
210
|
+
* Check if the JWT was issued before the user's last logout.
|
|
211
|
+
* Logout records are stored at user_logouts/{userId} (one per user). We use this to reject
|
|
212
|
+
* tokens that were issued before logout, since JWTs cannot be revoked directly.
|
|
213
|
+
* Returns true if token was issued before logout (token should be rejected).
|
|
198
214
|
*/
|
|
199
215
|
async wasTokenIssuedBeforeLogout(userId, tokenIssuedAt) {
|
|
200
|
-
// Check logout timestamp
|
|
201
216
|
const logoutRef = this.firestore.collection(this.logoutsCollectionName).doc(userId);
|
|
202
217
|
const doc = await logoutRef.get();
|
|
203
218
|
if (!doc.exists) {
|
|
204
|
-
return false; // No logout recorded
|
|
219
|
+
return false; // No logout recorded — token is acceptable
|
|
205
220
|
}
|
|
206
221
|
const data = doc.data();
|
|
207
222
|
if (!data) {
|
|
208
223
|
return false;
|
|
209
224
|
}
|
|
210
|
-
// Parse logout document
|
|
211
225
|
const logoutInfo = this.parseLogoutDocument(data);
|
|
212
|
-
// Check if token was issued BEFORE logout
|
|
213
226
|
return tokenIssuedAt < logoutInfo.loggedOutAt;
|
|
214
227
|
}
|
|
215
228
|
/**
|
|
216
|
-
* Warmup cache from Firestore
|
|
217
|
-
*
|
|
218
|
-
* Also
|
|
229
|
+
* Warmup cache from Firestore on startup. Loads all non-expired sessions into the in-memory
|
|
230
|
+
* cache (keyed by userId). Document IDs in user_sessions are sessionIds; we cache by userId
|
|
231
|
+
* for lookup. Also performs lazy deletion of expired session documents.
|
|
219
232
|
*/
|
|
220
233
|
async warmupCache() {
|
|
221
234
|
const collection = this.firestore.collection(this.collectionName);
|
|
222
235
|
const now = new Date();
|
|
223
|
-
// Query active sessions
|
|
224
236
|
const snapshot = await collection.where('expiresAt', '>', now).get();
|
|
225
|
-
// Load into cache
|
|
226
237
|
for (const doc of snapshot.docs) {
|
|
227
238
|
const data = doc.data();
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
console.warn(`Skipping session with mismatched userId: document ID=${doc.id}, data.userId=${data.userId}`);
|
|
239
|
+
// Document ID must match sessionId in payload (data integrity)
|
|
240
|
+
if (data.sessionId !== doc.id) {
|
|
241
|
+
console.warn(`Skipping session with mismatched sessionId: document ID=${doc.id}, data.sessionId=${data.sessionId}`);
|
|
232
242
|
continue;
|
|
233
243
|
}
|
|
234
244
|
const session = this.parseFirestoreDocument(data);
|
|
235
245
|
this.cache.set(session.userId, session);
|
|
236
246
|
}
|
|
237
|
-
//
|
|
247
|
+
// Lazy cleanup: delete expired session documents in batches
|
|
238
248
|
const expiredSnapshot = await collection.where('expiresAt', '<=', now).get();
|
|
239
249
|
if (expiredSnapshot.empty) {
|
|
240
250
|
return; // No expired sessions to clean up
|
|
@@ -257,10 +267,11 @@ export class SessionService {
|
|
|
257
267
|
}
|
|
258
268
|
}
|
|
259
269
|
/**
|
|
260
|
-
* Parse Firestore document
|
|
270
|
+
* Parse Firestore session document (timestamp fields may be Firestore Timestamp or Date) into SessionInfo.
|
|
261
271
|
*/
|
|
262
272
|
parseFirestoreDocument(data) {
|
|
263
273
|
return {
|
|
274
|
+
sessionId: data.sessionId,
|
|
264
275
|
userId: data.userId,
|
|
265
276
|
createdAt: this.toDate(data.createdAt),
|
|
266
277
|
lastActivityAt: this.toDate(data.lastActivityAt),
|
|
@@ -268,10 +279,11 @@ export class SessionService {
|
|
|
268
279
|
};
|
|
269
280
|
}
|
|
270
281
|
/**
|
|
271
|
-
* Convert SessionInfo to Firestore document format
|
|
282
|
+
* Convert SessionInfo to Firestore document format for user_sessions collection.
|
|
272
283
|
*/
|
|
273
284
|
toFirestoreDocument(session) {
|
|
274
285
|
return {
|
|
286
|
+
sessionId: session.sessionId,
|
|
275
287
|
userId: session.userId,
|
|
276
288
|
createdAt: session.createdAt,
|
|
277
289
|
lastActivityAt: session.lastActivityAt,
|
|
@@ -279,7 +291,7 @@ export class SessionService {
|
|
|
279
291
|
};
|
|
280
292
|
}
|
|
281
293
|
/**
|
|
282
|
-
*
|
|
294
|
+
* Normalize Firestore Timestamp or Date to JavaScript Date.
|
|
283
295
|
*/
|
|
284
296
|
toDate(timestamp) {
|
|
285
297
|
if (timestamp instanceof Date) {
|
|
@@ -288,7 +300,7 @@ export class SessionService {
|
|
|
288
300
|
return timestamp.toDate();
|
|
289
301
|
}
|
|
290
302
|
/**
|
|
291
|
-
* Parse Firestore logout document
|
|
303
|
+
* Parse Firestore logout document (user_logouts collection) into typed fields.
|
|
292
304
|
*/
|
|
293
305
|
parseLogoutDocument(data) {
|
|
294
306
|
return {
|
|
@@ -298,7 +310,7 @@ export class SessionService {
|
|
|
298
310
|
};
|
|
299
311
|
}
|
|
300
312
|
/**
|
|
301
|
-
* Convert logout info to Firestore document format
|
|
313
|
+
* Convert logout info to Firestore document format for user_logouts collection.
|
|
302
314
|
*/
|
|
303
315
|
toLogoutDocument(logoutInfo) {
|
|
304
316
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionService.js","sourceRoot":"","sources":["../../src/session/sessionService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sessionService.js","sourceRoot":"","sources":["../../src/session/sessionService.ts"],"names":[],"mappings":"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"}
|