This repository was archived by the owner on Dec 12, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathindex.ts
More file actions
170 lines (142 loc) · 5.9 KB
/
index.ts
File metadata and controls
170 lines (142 loc) · 5.9 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
import { deleteCookie, eventHandler, H3Event, parseCookies, setCookie } from 'h3'
import { nanoid } from 'nanoid'
import dayjs from 'dayjs'
import equal from 'fast-deep-equal'
import { SameSiteOptions, Session, SessionOptions } from '../../../../types'
import { dropStorageSession, getStorageSession, setStorageSession } from './storage'
import { processSessionIp, getHashedIpAddress } from './ipPinning'
import { SessionExpired } from './exceptions'
import { useRuntimeConfig } from '#imports'
const SESSION_COOKIE_NAME = 'sessionId'
const safeSetCookie = (event: H3Event, name: string, value: string, createdAt: Date) => {
const sessionOptions = useRuntimeConfig().session.session as SessionOptions
const expirationDate = sessionOptions.expiryInSeconds !== false
? new Date(createdAt.getTime() + sessionOptions.expiryInSeconds * 1000)
: undefined
setCookie(event, name, value, {
// Set cookie expiration date to now + expiryInSeconds
expires: expirationDate,
// Wether to send cookie via HTTPs to mitigate man-in-the-middle attacks
secure: sessionOptions.cookieSecure,
// Wether to send cookie via HTTP requests and not allowing access of cookie from JS to mitigate XSS attacks
httpOnly: sessionOptions.cookieHttpOnly,
// Do not send cookies on many cross-site requests to mitigates CSRF and cross-site attacks, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax
sameSite: sessionOptions.cookieSameSite as SameSiteOptions,
// Set cookie for subdomain
domain: sessionOptions.domain || undefined
})
}
const checkSessionExpirationTime = (session: Session, sessionExpiryInSeconds: number) => {
const now = dayjs()
if (now.diff(dayjs(session.createdAt), 'seconds') > sessionExpiryInSeconds) {
throw new SessionExpired()
}
}
/**
* Get the current session id.
*
* The session id may be set only on the cookie or only on the context or both. This is because when the
* session was just created, the session cookie is not yet set on the request (only on the response!). To
* still function in this scenario the session-middleware sets the cookie on the response and the `sessionId` on the `event.context`.
*
* This method extracts the session id and ensures that if the id on cookie and context match if both exist.
* @param event H3Event Event passing through middleware
*/
const getCurrentSessionId = (event: H3Event) => {
const sessionIdRequest = parseCookies(event).sessionId
const sessionIdContext = event.context.sessionId
if (sessionIdContext && sessionIdRequest && sessionIdContext !== sessionIdRequest) {
return null
}
return sessionIdRequest || sessionIdContext || null
}
export const deleteSession = async (event: H3Event) => {
const currentSessionId = getCurrentSessionId(event)
if (currentSessionId) {
await dropStorageSession(currentSessionId)
}
deleteCookie(event, SESSION_COOKIE_NAME)
}
const newSession = async (event: H3Event) => {
const runtimeConfig = useRuntimeConfig()
const sessionOptions = runtimeConfig.session.session as SessionOptions
const now = new Date()
// (Re-)Set cookie
const sessionId = nanoid(sessionOptions.idLength)
safeSetCookie(event, SESSION_COOKIE_NAME, sessionId, now)
// Store session data in storage
const session: Session = {
id: sessionId,
createdAt: now,
ip: sessionOptions.ipPinning ? await getHashedIpAddress(event) : undefined
}
await setStorageSession(sessionId, session)
return session
}
const getSession = async (event: H3Event): Promise<null | Session> => {
// 1. Does the sessionId cookie exist on the request?
const existingSessionId = getCurrentSessionId(event)
if (!existingSessionId) {
return null
}
// 2. Does the session exist in our storage?
const session = await getStorageSession(existingSessionId)
if (!isSession(session)) {
return null
}
const runtimeConfig = useRuntimeConfig()
const sessionOptions = runtimeConfig.session.session as SessionOptions
const sessionExpiryInSeconds = sessionOptions.expiryInSeconds
try {
// 3. Is the session not expired?
if (sessionExpiryInSeconds !== false) {
checkSessionExpirationTime(session, sessionExpiryInSeconds)
}
// 4. Check for IP pinning logic
if (sessionOptions.ipPinning) {
await processSessionIp(event, session)
}
} catch {
await deleteSession(event) // Cleanup old session data to avoid leaks
return null
}
return session
}
const updateSessionExpirationDate = (session: Session, event: H3Event) => {
const now = new Date()
safeSetCookie(event, SESSION_COOKIE_NAME, session.id, now)
return { ...session, createdAt: now }
}
function isSession (shape: unknown): shape is Session {
return typeof shape === 'object' && !!shape && 'id' in shape && 'createdAt' in shape
}
const ensureSession = async (event: H3Event) => {
const sessionOptions = useRuntimeConfig().session.session as SessionOptions
let session = await getSession(event)
if (!session) {
session = await newSession(event)
} else if (sessionOptions.rolling) {
session = updateSessionExpirationDate(session, event)
}
event.context.sessionId = session.id
event.context.session = session
return session
}
export default eventHandler(async (event: H3Event) => {
const sessionOptions = useRuntimeConfig().session.session as SessionOptions
// 1. Ensure that a session is present by either loading or creating one
const session = await ensureSession(event)
// 2. Save current state of the session
const source = { ...session }
// 3. Setup a hook that saves any changed made to the session by the subsequent endpoints & middlewares
event.res.on('finish', async () => {
// Session id may not exist if session was deleted
const session = await getSession(event)
if (!session) {
return
}
if (sessionOptions.resave || !equal(event.context.session, source)) {
await setStorageSession(session.id, event.context.session)
}
})
})