-
Notifications
You must be signed in to change notification settings - Fork 454
Expand file tree
/
Copy pathstateful-session-store.ts
More file actions
176 lines (147 loc) · 4.59 KB
/
stateful-session-store.ts
File metadata and controls
176 lines (147 loc) · 4.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import { SessionData, SessionDataStore } from "../../types";
import * as cookies from "../cookies";
import {
AbstractSessionStore,
SessionCookieOptions
} from "./abstract-session-store";
import {
LEGACY_COOKIE_NAME,
normalizeStatefulSession
} from "./normalize-session";
// the value of the stateful session cookie containing a unique session ID to identify
// the current session
interface SessionCookieValue {
id: string;
}
interface StatefulSessionStoreOptions {
secret: string;
rolling?: boolean; // defaults to true
absoluteDuration?: number; // defaults to 3 days
inactivityDuration?: number; // defaults to 1 day
store: SessionDataStore;
cookieOptions?: SessionCookieOptions;
}
const generateId = () => {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
export class StatefulSessionStore extends AbstractSessionStore {
public store: SessionDataStore;
constructor({
secret,
store,
rolling,
absoluteDuration,
inactivityDuration,
cookieOptions
}: StatefulSessionStoreOptions) {
super({
secret,
rolling,
absoluteDuration,
inactivityDuration,
cookieOptions
});
this.store = store;
}
async get(reqCookies: cookies.RequestCookies) {
const cookie =
reqCookies.get(this.sessionCookieName) ||
reqCookies.get(LEGACY_COOKIE_NAME);
if (!cookie || !cookie.value) {
return null;
}
// we attempt to extract the session ID by decrypting the cookie value (assuming it's a JWE, v4+) first
// if that fails, we attempt to verify the cookie value as a signed cookie (legacy, v3-)
// if both fail, we return null
// this ensures that v3 sessions are respected and can be transparently rolled over to v4+ sessions
let sessionId: string | null = null;
try {
const { payload: sessionCookie } =
await cookies.decrypt<SessionCookieValue>(cookie.value, this.secret);
sessionId = sessionCookie.id;
} catch (e: any) {
// the session cookie could not be decrypted, try to verify if it's a legacy session
if (e.code === "ERR_JWE_INVALID") {
const legacySessionId = await cookies.verifySigned(
cookie.name,
cookie.value,
this.secret
);
if (!legacySessionId) {
return null;
}
sessionId = legacySessionId;
}
}
if (!sessionId) {
return null;
}
const session = await this.store.get(sessionId);
if (!session) {
return null;
}
return normalizeStatefulSession(session);
}
async set(
reqCookies: cookies.RequestCookies,
resCookies: cookies.ResponseCookies,
session: SessionData,
isNew: boolean = false
) {
// check if a session already exists. If so, maintain the existing session ID
let sessionId = null;
const cookieValue = reqCookies.get(this.sessionCookieName)?.value;
if (cookieValue) {
const { payload: sessionCookie } =
await cookies.decrypt<SessionCookieValue>(cookieValue, this.secret);
sessionId = sessionCookie.id;
}
// if this is a new session created by a new login we need to remove the old session
// from the store and regenerate the session ID to prevent session fixation.
if (sessionId && isNew) {
await this.store.delete(sessionId);
sessionId = generateId();
}
if (!sessionId) {
sessionId = generateId();
}
const jwe = await cookies.encrypt(
{
id: sessionId
},
this.secret
);
const maxAge = this.calculateMaxAge(session.internal.createdAt);
resCookies.set(this.sessionCookieName, jwe.toString(), {
...this.cookieConfig,
maxAge
});
await this.store.set(sessionId, session);
// to enable read-after-write in the same request for middleware
reqCookies.set(this.sessionCookieName, jwe.toString());
// Any existing v3 cookie can also be deleted once we have set a v4 cookie.
// In stateful sessions, we do not have to worry about chunking.
if (reqCookies.has(LEGACY_COOKIE_NAME)) {
resCookies.delete(LEGACY_COOKIE_NAME);
}
}
async delete(
reqCookies: cookies.RequestCookies,
resCookies: cookies.ResponseCookies
) {
const cookieValue = reqCookies.get(this.sessionCookieName)?.value;
await resCookies.delete(this.sessionCookieName);
if (!cookieValue) {
return;
}
const { payload: session } = await cookies.decrypt<SessionCookieValue>(
cookieValue,
this.secret
);
await this.store.delete(session.id);
}
}