@prsm/auth 1.0.0
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/README.md +226 -0
- package/index.d.ts +19 -0
- package/package.json +76 -0
- package/src/__tests__/auth.test.js +1171 -0
- package/src/__tests__/impersonation-test-setup.js +208 -0
- package/src/__tests__/impersonation.test.js +473 -0
- package/src/__tests__/oauth-test-setup.js +136 -0
- package/src/__tests__/oauth.test.js +400 -0
- package/src/__tests__/prsm.test.js +215 -0
- package/src/__tests__/test-setup.js +385 -0
- package/src/__tests__/totp.test.js +158 -0
- package/src/__tests__/two-factor-test-setup.js +331 -0
- package/src/__tests__/two-factor.test.js +396 -0
- package/src/activity-logger.js +228 -0
- package/src/auth-context.js +120 -0
- package/src/auth-functions.js +520 -0
- package/src/auth-manager.js +1371 -0
- package/src/errors.js +173 -0
- package/src/hooks.js +41 -0
- package/src/index.js +23 -0
- package/src/invalidation.js +166 -0
- package/src/middleware.js +33 -0
- package/src/providers/azure-provider.js +114 -0
- package/src/providers/base-provider.js +152 -0
- package/src/providers/github-provider.js +86 -0
- package/src/providers/google-provider.js +76 -0
- package/src/providers/index.js +4 -0
- package/src/queries.js +543 -0
- package/src/schema.js +261 -0
- package/src/totp.js +221 -0
- package/src/two-factor/index.js +3 -0
- package/src/two-factor/otp-provider.js +128 -0
- package/src/two-factor/totp-provider.js +98 -0
- package/src/two-factor/two-factor-manager.js +676 -0
- package/src/types.js +399 -0
- package/src/user-roles.js +128 -0
- package/src/util.js +32 -0
- package/types/activity-logger.d.ts +73 -0
- package/types/auth-context.d.ts +88 -0
- package/types/auth-functions.d.ts +151 -0
- package/types/auth-manager.d.ts +365 -0
- package/types/errors.d.ts +108 -0
- package/types/hooks.d.ts +30 -0
- package/types/index.d.ts +13 -0
- package/types/invalidation.d.ts +40 -0
- package/types/middleware.d.ts +11 -0
- package/types/providers/azure-provider.d.ts +35 -0
- package/types/providers/base-provider.d.ts +52 -0
- package/types/providers/github-provider.d.ts +29 -0
- package/types/providers/google-provider.d.ts +29 -0
- package/types/providers/index.d.ts +4 -0
- package/types/queries.d.ts +287 -0
- package/types/schema.d.ts +37 -0
- package/types/totp.d.ts +72 -0
- package/types/two-factor/index.d.ts +3 -0
- package/types/two-factor/otp-provider.d.ts +57 -0
- package/types/two-factor/totp-provider.d.ts +58 -0
- package/types/two-factor/two-factor-manager.d.ts +191 -0
- package/types/types.d.ts +688 -0
- package/types/user-roles.d.ts +47 -0
- package/types/util.d.ts +3 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as authFunctions from "./auth-functions.js"
|
|
2
|
+
import { AuthQueries } from "./queries.js"
|
|
3
|
+
import { ActivityLogger } from "./activity-logger.js"
|
|
4
|
+
import { AuthRole, AuthStatus, TwoFactorMechanism } from "./types.js"
|
|
5
|
+
import { UserNotFoundError } from "./errors.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import("./types.js").AuthConfig} AuthConfig
|
|
9
|
+
* @typedef {import("./types.js").TokenCallback} TokenCallback
|
|
10
|
+
* @typedef {import("./types.js").AuthAccount} AuthAccount
|
|
11
|
+
* @typedef {import("./types.js").UserIdentifier} UserIdentifier
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {AuthQueries} queries
|
|
16
|
+
* @param {UserIdentifier} identifier
|
|
17
|
+
* @returns {Promise<AuthAccount | null>}
|
|
18
|
+
*/
|
|
19
|
+
async function resolveAccount(queries, identifier) {
|
|
20
|
+
if (identifier.accountId !== undefined) return queries.findAccountById(identifier.accountId)
|
|
21
|
+
if (identifier.email !== undefined) return queries.findAccountByEmail(identifier.email)
|
|
22
|
+
if (identifier.userId !== undefined) return queries.findAccountByUserId(identifier.userId)
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a requestless auth context for scripts, workers, cron jobs, and admin
|
|
28
|
+
* tasks. The same object doubles as the binding surface for the @prsm/devtools
|
|
29
|
+
* admin panel: it exposes read methods (listAccounts, getAccount, getStats,
|
|
30
|
+
* recent activity, roles) and control actions (role/status/force-logout/etc),
|
|
31
|
+
* all duck-typed so devtools needs no @prsm/auth dependency.
|
|
32
|
+
* @param {AuthConfig} config
|
|
33
|
+
*/
|
|
34
|
+
export function createAuthContext(config) {
|
|
35
|
+
const queries = new AuthQueries(config)
|
|
36
|
+
const activityLogger = new ActivityLogger(config)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
// user management (requestless equivalents of the req.auth admin methods)
|
|
40
|
+
createUser: (credentials, userId, callback) => authFunctions.createUser(config, credentials, userId, callback),
|
|
41
|
+
register: (email, password, userId, callback) => authFunctions.register(config, email, password, userId, callback),
|
|
42
|
+
deleteUserBy: (identifier) => authFunctions.deleteUserBy(config, identifier),
|
|
43
|
+
addRoleForUserBy: (identifier, role) => authFunctions.addRoleForUserBy(config, identifier, role),
|
|
44
|
+
removeRoleForUserBy: (identifier, role) => authFunctions.removeRoleForUserBy(config, identifier, role),
|
|
45
|
+
hasRoleForUserBy: (identifier, role) => authFunctions.hasRoleForUserBy(config, identifier, role),
|
|
46
|
+
changePasswordForUserBy: (identifier, password) => authFunctions.changePasswordForUserBy(config, identifier, password),
|
|
47
|
+
setStatusForUserBy: (identifier, status) => authFunctions.setStatusForUserBy(config, identifier, status),
|
|
48
|
+
initiatePasswordResetForUserBy: (identifier, expiresAfter, callback) => authFunctions.initiatePasswordResetForUserBy(config, identifier, expiresAfter, callback),
|
|
49
|
+
resetPassword: (email, expiresAfter, maxOpenRequests, callback) => authFunctions.resetPassword(config, email, expiresAfter, maxOpenRequests, callback),
|
|
50
|
+
confirmResetPassword: (token, password) => authFunctions.confirmResetPassword(config, token, password),
|
|
51
|
+
userExistsByEmail: (email) => authFunctions.userExistsByEmail(config, email),
|
|
52
|
+
forceLogoutForUserBy: (identifier) => authFunctions.forceLogoutForUserBy(config, identifier),
|
|
53
|
+
|
|
54
|
+
// introspection surface for @prsm/devtools
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {{ limit?: number, offset?: number, search?: string }} [opts]
|
|
58
|
+
* @returns {Promise<{ accounts: AuthAccount[], total: number }>}
|
|
59
|
+
*/
|
|
60
|
+
async listAccounts(opts = {}) {
|
|
61
|
+
const [accounts, total] = await Promise.all([queries.listAccounts(opts), queries.countAccounts(opts.search)])
|
|
62
|
+
return { accounts, total }
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {UserIdentifier} identifier
|
|
67
|
+
* @returns {Promise<AuthAccount>}
|
|
68
|
+
*/
|
|
69
|
+
async getAccount(identifier) {
|
|
70
|
+
const account = await resolveAccount(queries, identifier)
|
|
71
|
+
if (!account) throw new UserNotFoundError()
|
|
72
|
+
return account
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {number} accountId
|
|
77
|
+
*/
|
|
78
|
+
getProvidersForAccount: (accountId) => queries.findProvidersByAccountId(accountId),
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {number} accountId
|
|
82
|
+
*/
|
|
83
|
+
getTwoFactorMethods: (accountId) => queries.findTwoFactorMethodsByAccountId(accountId),
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The role name -> bit map devtools renders, defaulting to AuthRole.
|
|
87
|
+
* @returns {Record<string, number>}
|
|
88
|
+
*/
|
|
89
|
+
getRoles: () => config.roles || AuthRole,
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The status code -> name map devtools renders for account status.
|
|
93
|
+
* @returns {Record<string, number>}
|
|
94
|
+
*/
|
|
95
|
+
getStatuses: () => AuthStatus,
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The 2FA mechanism code -> name map devtools renders.
|
|
99
|
+
* @returns {Record<string, number>}
|
|
100
|
+
*/
|
|
101
|
+
getMechanisms: () => TwoFactorMechanism,
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @returns {Promise<ReturnType<typeof import("./schema.js").getAuthTableStats>>}
|
|
105
|
+
*/
|
|
106
|
+
getStats: () => import("./schema.js").then((m) => m.getAuthTableStats(config)),
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {number} [limit]
|
|
110
|
+
* @param {number} [accountId]
|
|
111
|
+
*/
|
|
112
|
+
getRecentActivity: (limit, accountId) => activityLogger.getRecentActivity(limit, accountId),
|
|
113
|
+
|
|
114
|
+
getActivityStats: () => activityLogger.getActivityStats(),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @typedef {ReturnType<typeof createAuthContext>} AuthContext
|
|
120
|
+
*/
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import hash from "@prsm/hash"
|
|
2
|
+
import ms from "@prsm/ms"
|
|
3
|
+
import { AuthQueries } from "./queries.js"
|
|
4
|
+
import { validateEmail } from "./util.js"
|
|
5
|
+
import { EmailTakenError, InvalidPasswordError, UserNotFoundError, EmailNotVerifiedError, ResetDisabledError, TooManyResetsError, ResetNotFoundError, ResetExpiredError, InvalidTokenError } from "./errors.js"
|
|
6
|
+
import { AuthStatus } from "./types.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import("./types.js").AuthConfig} AuthConfig
|
|
10
|
+
* @typedef {import("./types.js").AuthAccount} AuthAccount
|
|
11
|
+
* @typedef {import("./types.js").TokenCallback} TokenCallback
|
|
12
|
+
* @typedef {import("./types.js").UserIdentifier} UserIdentifier
|
|
13
|
+
* @typedef {import("./types.js").AuthenticateRequestResult} AuthenticateRequestResult
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} cookieHeader
|
|
18
|
+
* @returns {Record<string, string>}
|
|
19
|
+
*/
|
|
20
|
+
function parseCookies(cookieHeader) {
|
|
21
|
+
/** @type {Record<string, string>} */
|
|
22
|
+
const cookies = {}
|
|
23
|
+
if (!cookieHeader) return cookies
|
|
24
|
+
|
|
25
|
+
for (const pair of cookieHeader.split(";")) {
|
|
26
|
+
const idx = pair.indexOf("=")
|
|
27
|
+
if (idx === -1) continue
|
|
28
|
+
const key = pair.slice(0, idx).trim()
|
|
29
|
+
const value = pair.slice(idx + 1).trim()
|
|
30
|
+
if (key) cookies[key] = decodeURIComponent(value)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return cookies
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the account for an incoming request via session or remember-me cookie.
|
|
38
|
+
* @param {AuthConfig} config
|
|
39
|
+
* @param {import("http").IncomingMessage} req
|
|
40
|
+
* @param {(req: any, res: any, next: () => void) => void} [sessionMiddleware]
|
|
41
|
+
* @returns {Promise<AuthenticateRequestResult>}
|
|
42
|
+
*/
|
|
43
|
+
export async function authenticateRequest(config, req, sessionMiddleware) {
|
|
44
|
+
const queries = new AuthQueries(config)
|
|
45
|
+
|
|
46
|
+
if (sessionMiddleware) {
|
|
47
|
+
await new Promise(resolve => {
|
|
48
|
+
sessionMiddleware(req, {}, resolve)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const session = req.session
|
|
53
|
+
if (session?.auth?.loggedIn && session.auth.accountId) {
|
|
54
|
+
const account = await queries.findAccountById(session.auth.accountId)
|
|
55
|
+
if (account && account.status === AuthStatus.Normal) {
|
|
56
|
+
return { account, source: "session" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cookies = parseCookies(req.headers.cookie || "")
|
|
61
|
+
const cookieName = config.rememberCookieName || "remember_token"
|
|
62
|
+
const token = cookies[cookieName]
|
|
63
|
+
|
|
64
|
+
if (!token) {
|
|
65
|
+
return { account: null, source: null }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const remember = await queries.findRememberToken(token)
|
|
69
|
+
if (!remember || new Date() > remember.expires) {
|
|
70
|
+
return { account: null, source: null }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const account = await queries.findAccountById(remember.account_id)
|
|
74
|
+
if (!account || account.status !== AuthStatus.Normal) {
|
|
75
|
+
return { account: null, source: null }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { account, source: "remember" }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} password
|
|
83
|
+
* @param {AuthConfig} config
|
|
84
|
+
* @throws {InvalidPasswordError}
|
|
85
|
+
*/
|
|
86
|
+
function validatePassword(password, config) {
|
|
87
|
+
const minLength = config.minPasswordLength || 8
|
|
88
|
+
const maxLength = config.maxPasswordLength || 64
|
|
89
|
+
|
|
90
|
+
if (typeof password !== "string") {
|
|
91
|
+
throw new InvalidPasswordError()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (password.length < minLength) {
|
|
95
|
+
throw new InvalidPasswordError()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (password.length > maxLength) {
|
|
99
|
+
throw new InvalidPasswordError()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
function generateAutoUserId() {
|
|
107
|
+
return crypto.randomUUID()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {AuthQueries} queries
|
|
112
|
+
* @param {UserIdentifier} identifier
|
|
113
|
+
* @returns {Promise<AuthAccount | null>}
|
|
114
|
+
*/
|
|
115
|
+
async function findAccountByIdentifier(queries, identifier) {
|
|
116
|
+
if (identifier.accountId !== undefined) {
|
|
117
|
+
return await queries.findAccountById(identifier.accountId)
|
|
118
|
+
} else if (identifier.email !== undefined) {
|
|
119
|
+
return await queries.findAccountByEmail(identifier.email)
|
|
120
|
+
} else if (identifier.userId !== undefined) {
|
|
121
|
+
return await queries.findAccountByUserId(identifier.userId)
|
|
122
|
+
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {AuthQueries} queries
|
|
128
|
+
* @param {AuthAccount} account
|
|
129
|
+
* @param {string} email
|
|
130
|
+
* @param {TokenCallback} callback
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async function createConfirmationToken(queries, account, email, callback) {
|
|
134
|
+
const token = await hash.encode(email)
|
|
135
|
+
const expires = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 1 week
|
|
136
|
+
|
|
137
|
+
await queries.createConfirmation({
|
|
138
|
+
accountId: account.id,
|
|
139
|
+
token,
|
|
140
|
+
email,
|
|
141
|
+
expires,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (callback) {
|
|
145
|
+
callback(token)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create a new local account. When a callback is provided the account starts
|
|
151
|
+
* unverified and a confirmation token is generated.
|
|
152
|
+
* @param {AuthConfig} config
|
|
153
|
+
* @param {{ email: string, password: string }} credentials
|
|
154
|
+
* @param {string | number} [userId]
|
|
155
|
+
* @param {TokenCallback} [callback]
|
|
156
|
+
* @returns {Promise<AuthAccount>}
|
|
157
|
+
* @throws {EmailTakenError}
|
|
158
|
+
*/
|
|
159
|
+
export async function createUser(config, credentials, userId, callback) {
|
|
160
|
+
validateEmail(credentials.email)
|
|
161
|
+
validatePassword(credentials.password, config)
|
|
162
|
+
|
|
163
|
+
const queries = new AuthQueries(config)
|
|
164
|
+
|
|
165
|
+
const existing = await queries.findAccountByEmail(credentials.email)
|
|
166
|
+
if (existing) {
|
|
167
|
+
throw new EmailTakenError()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const finalUserId = userId || generateAutoUserId()
|
|
171
|
+
const hashedPassword = await hash.encode(credentials.password)
|
|
172
|
+
const verified = typeof callback !== "function"
|
|
173
|
+
|
|
174
|
+
const account = await queries.createAccount({
|
|
175
|
+
userId: finalUserId,
|
|
176
|
+
email: credentials.email,
|
|
177
|
+
password: hashedPassword,
|
|
178
|
+
verified,
|
|
179
|
+
status: AuthStatus.Normal,
|
|
180
|
+
rolemask: 0,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if (!verified && callback) {
|
|
184
|
+
await createConfirmationToken(queries, account, credentials.email, callback)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return account
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Register a new local account. When a callback is provided the account starts
|
|
192
|
+
* unverified and a confirmation token is generated.
|
|
193
|
+
* @param {AuthConfig} config
|
|
194
|
+
* @param {string} email
|
|
195
|
+
* @param {string} password
|
|
196
|
+
* @param {string | number} [userId]
|
|
197
|
+
* @param {TokenCallback} [callback]
|
|
198
|
+
* @returns {Promise<AuthAccount>}
|
|
199
|
+
* @throws {EmailTakenError}
|
|
200
|
+
*/
|
|
201
|
+
export async function register(config, email, password, userId, callback) {
|
|
202
|
+
validateEmail(email)
|
|
203
|
+
validatePassword(password, config)
|
|
204
|
+
|
|
205
|
+
const queries = new AuthQueries(config)
|
|
206
|
+
|
|
207
|
+
const existing = await queries.findAccountByEmail(email)
|
|
208
|
+
if (existing) {
|
|
209
|
+
throw new EmailTakenError()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const finalUserId = userId || generateAutoUserId()
|
|
213
|
+
const hashedPassword = await hash.encode(password)
|
|
214
|
+
const verified = typeof callback !== "function"
|
|
215
|
+
|
|
216
|
+
const account = await queries.createAccount({
|
|
217
|
+
userId: finalUserId,
|
|
218
|
+
email,
|
|
219
|
+
password: hashedPassword,
|
|
220
|
+
verified,
|
|
221
|
+
status: AuthStatus.Normal,
|
|
222
|
+
rolemask: 0,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
if (!verified && callback) {
|
|
226
|
+
await createConfirmationToken(queries, account, email, callback)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return account
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Delete the account matched by the identifier.
|
|
234
|
+
* @param {AuthConfig} config
|
|
235
|
+
* @param {UserIdentifier} identifier
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
* @throws {UserNotFoundError}
|
|
238
|
+
*/
|
|
239
|
+
export async function deleteUserBy(config, identifier) {
|
|
240
|
+
const queries = new AuthQueries(config)
|
|
241
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
242
|
+
|
|
243
|
+
if (!account) {
|
|
244
|
+
throw new UserNotFoundError()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await queries.deleteAccount(account.id)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Add a role bit to the account's rolemask.
|
|
252
|
+
* @param {AuthConfig} config
|
|
253
|
+
* @param {UserIdentifier} identifier
|
|
254
|
+
* @param {number} role
|
|
255
|
+
* @returns {Promise<void>}
|
|
256
|
+
* @throws {UserNotFoundError}
|
|
257
|
+
*/
|
|
258
|
+
export async function addRoleForUserBy(config, identifier, role) {
|
|
259
|
+
const queries = new AuthQueries(config)
|
|
260
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
261
|
+
|
|
262
|
+
if (!account) {
|
|
263
|
+
throw new UserNotFoundError()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const rolemask = account.rolemask | role
|
|
267
|
+
await queries.updateAccount(account.id, { rolemask })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Remove a role bit from the account's rolemask.
|
|
272
|
+
* @param {AuthConfig} config
|
|
273
|
+
* @param {UserIdentifier} identifier
|
|
274
|
+
* @param {number} role
|
|
275
|
+
* @returns {Promise<void>}
|
|
276
|
+
* @throws {UserNotFoundError}
|
|
277
|
+
*/
|
|
278
|
+
export async function removeRoleForUserBy(config, identifier, role) {
|
|
279
|
+
const queries = new AuthQueries(config)
|
|
280
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
281
|
+
|
|
282
|
+
if (!account) {
|
|
283
|
+
throw new UserNotFoundError()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const rolemask = account.rolemask & ~role
|
|
287
|
+
await queries.updateAccount(account.id, { rolemask })
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check whether the account has every bit in the given role mask.
|
|
292
|
+
* @param {AuthConfig} config
|
|
293
|
+
* @param {UserIdentifier} identifier
|
|
294
|
+
* @param {number} role
|
|
295
|
+
* @returns {Promise<boolean>}
|
|
296
|
+
* @throws {UserNotFoundError}
|
|
297
|
+
*/
|
|
298
|
+
export async function hasRoleForUserBy(config, identifier, role) {
|
|
299
|
+
const queries = new AuthQueries(config)
|
|
300
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
301
|
+
|
|
302
|
+
if (!account) {
|
|
303
|
+
throw new UserNotFoundError()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return (account.rolemask & role) === role
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Change the password for the account matched by the identifier.
|
|
311
|
+
* @param {AuthConfig} config
|
|
312
|
+
* @param {UserIdentifier} identifier
|
|
313
|
+
* @param {string} password
|
|
314
|
+
* @returns {Promise<void>}
|
|
315
|
+
* @throws {UserNotFoundError}
|
|
316
|
+
* @throws {InvalidPasswordError}
|
|
317
|
+
*/
|
|
318
|
+
export async function changePasswordForUserBy(config, identifier, password) {
|
|
319
|
+
validatePassword(password, config)
|
|
320
|
+
|
|
321
|
+
const queries = new AuthQueries(config)
|
|
322
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
323
|
+
|
|
324
|
+
if (!account) {
|
|
325
|
+
throw new UserNotFoundError()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await queries.updateAccount(account.id, {
|
|
329
|
+
password: await hash.encode(password),
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Set the status code for the account matched by the identifier.
|
|
335
|
+
* @param {AuthConfig} config
|
|
336
|
+
* @param {UserIdentifier} identifier
|
|
337
|
+
* @param {number} status
|
|
338
|
+
* @returns {Promise<void>}
|
|
339
|
+
* @throws {UserNotFoundError}
|
|
340
|
+
*/
|
|
341
|
+
export async function setStatusForUserBy(config, identifier, status) {
|
|
342
|
+
const queries = new AuthQueries(config)
|
|
343
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
344
|
+
|
|
345
|
+
if (!account) {
|
|
346
|
+
throw new UserNotFoundError()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await queries.updateAccount(account.id, { status })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create a password reset token for the account matched by the identifier.
|
|
354
|
+
* @param {AuthConfig} config
|
|
355
|
+
* @param {UserIdentifier} identifier
|
|
356
|
+
* @param {string | number | null} [expiresAfter]
|
|
357
|
+
* @param {TokenCallback} [callback]
|
|
358
|
+
* @returns {Promise<void>}
|
|
359
|
+
* @throws {UserNotFoundError}
|
|
360
|
+
* @throws {EmailNotVerifiedError}
|
|
361
|
+
*/
|
|
362
|
+
export async function initiatePasswordResetForUserBy(config, identifier, expiresAfter = null, callback) {
|
|
363
|
+
const queries = new AuthQueries(config)
|
|
364
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
365
|
+
|
|
366
|
+
if (!account) {
|
|
367
|
+
throw new UserNotFoundError()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!account.verified) {
|
|
371
|
+
throw new EmailNotVerifiedError()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const expiry = !expiresAfter ? ms("6h") : ms(expiresAfter)
|
|
375
|
+
const token = await hash.encode(account.email)
|
|
376
|
+
const expires = new Date(Date.now() + expiry)
|
|
377
|
+
|
|
378
|
+
await queries.createResetToken({
|
|
379
|
+
accountId: account.id,
|
|
380
|
+
token,
|
|
381
|
+
expires,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
if (callback) {
|
|
385
|
+
callback(token)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Request a password reset by email, subject to the open-request limit.
|
|
391
|
+
* @param {AuthConfig} config
|
|
392
|
+
* @param {string} email
|
|
393
|
+
* @param {string | number | null} [expiresAfter]
|
|
394
|
+
* @param {number | null} [maxOpenRequests]
|
|
395
|
+
* @param {TokenCallback} [callback]
|
|
396
|
+
* @returns {Promise<void>}
|
|
397
|
+
* @throws {EmailNotVerifiedError}
|
|
398
|
+
* @throws {ResetDisabledError}
|
|
399
|
+
* @throws {TooManyResetsError}
|
|
400
|
+
*/
|
|
401
|
+
export async function resetPassword(config, email, expiresAfter = null, maxOpenRequests = null, callback) {
|
|
402
|
+
validateEmail(email)
|
|
403
|
+
|
|
404
|
+
const expiry = !expiresAfter ? ms("6h") : ms(expiresAfter)
|
|
405
|
+
const maxRequests = maxOpenRequests === null ? 2 : Math.max(1, maxOpenRequests)
|
|
406
|
+
|
|
407
|
+
const queries = new AuthQueries(config)
|
|
408
|
+
const account = await queries.findAccountByEmail(email)
|
|
409
|
+
|
|
410
|
+
if (!account || !account.verified) {
|
|
411
|
+
throw new EmailNotVerifiedError()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!account.resettable) {
|
|
415
|
+
throw new ResetDisabledError()
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const openRequests = await queries.countActiveResetTokensForAccount(account.id)
|
|
419
|
+
|
|
420
|
+
if (openRequests >= maxRequests) {
|
|
421
|
+
throw new TooManyResetsError()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const token = await hash.encode(email)
|
|
425
|
+
const expires = new Date(Date.now() + expiry)
|
|
426
|
+
|
|
427
|
+
await queries.createResetToken({
|
|
428
|
+
accountId: account.id,
|
|
429
|
+
token,
|
|
430
|
+
expires,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
if (callback) {
|
|
434
|
+
callback(token)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Confirm a password reset token and apply the new password.
|
|
440
|
+
* @param {AuthConfig} config
|
|
441
|
+
* @param {string} token
|
|
442
|
+
* @param {string} password
|
|
443
|
+
* @returns {Promise<{ accountId: number, email: string }>}
|
|
444
|
+
* @throws {ResetNotFoundError}
|
|
445
|
+
* @throws {ResetExpiredError}
|
|
446
|
+
* @throws {UserNotFoundError}
|
|
447
|
+
* @throws {ResetDisabledError}
|
|
448
|
+
* @throws {InvalidPasswordError}
|
|
449
|
+
* @throws {InvalidTokenError}
|
|
450
|
+
*/
|
|
451
|
+
export async function confirmResetPassword(config, token, password) {
|
|
452
|
+
const queries = new AuthQueries(config)
|
|
453
|
+
const reset = await queries.findResetToken(token)
|
|
454
|
+
|
|
455
|
+
if (!reset) {
|
|
456
|
+
throw new ResetNotFoundError()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (new Date(reset.expires) < new Date()) {
|
|
460
|
+
throw new ResetExpiredError()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const account = await queries.findAccountById(reset.account_id)
|
|
464
|
+
if (!account) {
|
|
465
|
+
throw new UserNotFoundError()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!account.resettable) {
|
|
469
|
+
throw new ResetDisabledError()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
validatePassword(password, config)
|
|
473
|
+
|
|
474
|
+
if (!(await hash.verify(token, account.email))) {
|
|
475
|
+
throw new InvalidTokenError()
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
await queries.updateAccount(account.id, {
|
|
479
|
+
password: await hash.encode(password),
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
await queries.deleteResetToken(token)
|
|
483
|
+
|
|
484
|
+
return { accountId: account.id, email: account.email }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Check whether an account exists for the given email.
|
|
489
|
+
* @param {AuthConfig} config
|
|
490
|
+
* @param {string} email
|
|
491
|
+
* @returns {Promise<boolean>}
|
|
492
|
+
*/
|
|
493
|
+
export async function userExistsByEmail(config, email) {
|
|
494
|
+
validateEmail(email)
|
|
495
|
+
|
|
496
|
+
const queries = new AuthQueries(config)
|
|
497
|
+
const account = await queries.findAccountByEmail(email)
|
|
498
|
+
|
|
499
|
+
return account !== null
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Force logout of all sessions for the account matched by the identifier.
|
|
504
|
+
* @param {AuthConfig} config
|
|
505
|
+
* @param {UserIdentifier} identifier
|
|
506
|
+
* @returns {Promise<{ accountId: number }>}
|
|
507
|
+
* @throws {UserNotFoundError}
|
|
508
|
+
*/
|
|
509
|
+
export async function forceLogoutForUserBy(config, identifier) {
|
|
510
|
+
const queries = new AuthQueries(config)
|
|
511
|
+
const account = await findAccountByIdentifier(queries, identifier)
|
|
512
|
+
|
|
513
|
+
if (!account) {
|
|
514
|
+
throw new UserNotFoundError()
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
await queries.incrementForceLogout(account.id)
|
|
518
|
+
|
|
519
|
+
return { accountId: account.id }
|
|
520
|
+
}
|