@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,158 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import Otp, { InvalidSecretError, InvalidOtpLengthError, InvalidHashFunctionError, InvalidIntervalError } from "../totp.js"
|
|
3
|
+
|
|
4
|
+
// the totp primitive is inlined into @prsm/auth (not re-exported from the package
|
|
5
|
+
// index), but it is the crypto backing all 2fa, so it gets a direct suite. these
|
|
6
|
+
// port the @eaccess/totp tests and pin the RFC 6238 vectors, which also prove the
|
|
7
|
+
// self-contained base32 (decode) matches the @scure/base implementation it replaced
|
|
8
|
+
|
|
9
|
+
// base32 of the RFC 6238 ASCII seeds (verified equal to @scure/base output)
|
|
10
|
+
const SHA1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" // "12345678901234567890"
|
|
11
|
+
const SHA256 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA" // ...12 bytes more
|
|
12
|
+
const SHA512 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA"
|
|
13
|
+
|
|
14
|
+
describe("totp - secret generation", () => {
|
|
15
|
+
it("generates base32 secrets sized by strength", () => {
|
|
16
|
+
expect(Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_LOW).length).toBe(16)
|
|
17
|
+
expect(Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_MODERATE).length).toBe(26)
|
|
18
|
+
expect(Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_HIGH).length).toBe(32)
|
|
19
|
+
expect(Otp.createSecret().length).toBe(32) // default high
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("only emits base32 alphabet characters", () => {
|
|
23
|
+
expect(Otp.createSecret()).toMatch(/^[A-Z2-7]+$/)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("totp - generate and verify round-trip", () => {
|
|
28
|
+
it("generates a 6-digit code that verifies (exercises base32 encode + decode)", () => {
|
|
29
|
+
const secret = Otp.createSecret()
|
|
30
|
+
const code = Otp.generateTotp(secret)
|
|
31
|
+
expect(code).toHaveLength(6)
|
|
32
|
+
expect(Otp.verifyTotp(secret, code)).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("rejects a wrong code", () => {
|
|
36
|
+
const secret = Otp.createSecret()
|
|
37
|
+
expect(Otp.verifyTotp(secret, "000000")).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("supports custom 8-digit length", () => {
|
|
41
|
+
const secret = Otp.createSecret()
|
|
42
|
+
const code = Otp.generateTotp(secret, undefined, 8)
|
|
43
|
+
expect(code).toHaveLength(8)
|
|
44
|
+
expect(Otp.verifyTotp(secret, code, undefined, undefined, undefined, 8)).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe("totp - QR provisioning URI", () => {
|
|
49
|
+
it("builds an otpauth uri", () => {
|
|
50
|
+
const uri = Otp.createTotpKeyUriForQrCode("app.example.com", "john.doe@example.org", "SECRET")
|
|
51
|
+
expect(uri).toContain("otpauth://totp/app.example.com:john.doe%40example.org")
|
|
52
|
+
expect(uri).toContain("secret=SECRET")
|
|
53
|
+
expect(uri).toContain("issuer=app.example.com")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("url-encodes issuer special characters", () => {
|
|
57
|
+
const uri = Otp.createTotpKeyUriForQrCode("My App/Service", "user@example.com", "SECRET")
|
|
58
|
+
expect(uri).toContain("otpauth://totp/My%20App%2FService:")
|
|
59
|
+
expect(uri).toContain("issuer=My%20App%2FService")
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("totp - input validation", () => {
|
|
64
|
+
it("throws on a secret shorter than 16 chars", () => {
|
|
65
|
+
expect(() => Otp.generateTotp("shortsecret")).toThrow(InvalidSecretError)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("throws on an out-of-range otp length", () => {
|
|
69
|
+
const secret = Otp.createSecret()
|
|
70
|
+
expect(() => Otp.generateTotp(secret, undefined, 5)).toThrow(InvalidOtpLengthError)
|
|
71
|
+
expect(() => Otp.generateTotp(secret, undefined, 9)).toThrow(InvalidOtpLengthError)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("throws on an unknown hash function", () => {
|
|
75
|
+
const secret = Otp.createSecret()
|
|
76
|
+
expect(() => Otp.generateTotp(secret, undefined, undefined, undefined, undefined, 999)).toThrow(InvalidHashFunctionError)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("throws on a non-positive interval", () => {
|
|
80
|
+
const secret = Otp.createSecret()
|
|
81
|
+
expect(() => Otp.generateTotp(secret, undefined, 6, 0)).toThrow(InvalidIntervalError)
|
|
82
|
+
expect(() => Otp.generateTotp(secret, undefined, 6, -30)).toThrow(InvalidIntervalError)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("rejects codes of the wrong length without throwing", () => {
|
|
86
|
+
const secret = Otp.createSecret()
|
|
87
|
+
expect(Otp.verifyTotp(secret, "12345")).toBe(false) // too short
|
|
88
|
+
expect(Otp.verifyTotp(secret, "123456789")).toBe(false) // too long
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("totp - drift window", () => {
|
|
93
|
+
const now = 1000000000
|
|
94
|
+
const step = 30
|
|
95
|
+
|
|
96
|
+
it("accepts a one-step-ahead code within a symmetric window", () => {
|
|
97
|
+
const secret = Otp.createSecret()
|
|
98
|
+
const ahead = Otp.generateTotp(secret, now + step)
|
|
99
|
+
expect(Otp.verifyTotp(secret, ahead, 0, undefined, now)).toBe(false) // window 0
|
|
100
|
+
expect(Otp.verifyTotp(secret, ahead, 1, undefined, now)).toBe(true) // window 1 (symmetric)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("honors an explicit asymmetric window", () => {
|
|
104
|
+
const secret = Otp.createSecret()
|
|
105
|
+
const ahead = Otp.generateTotp(secret, now + step)
|
|
106
|
+
expect(Otp.verifyTotp(secret, ahead, 2, 0, now)).toBe(false) // look only behind
|
|
107
|
+
expect(Otp.verifyTotp(secret, ahead, 0, 1, now)).toBe(true) // look ahead 1
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("is strict with a window of 0", () => {
|
|
111
|
+
const secret = Otp.createSecret()
|
|
112
|
+
const code = Otp.generateTotp(secret, now)
|
|
113
|
+
expect(Otp.verifyTotp(secret, code, 0, undefined, now)).toBe(true)
|
|
114
|
+
const old = Otp.generateTotp(secret, now - step)
|
|
115
|
+
expect(Otp.verifyTotp(secret, old, 0, undefined, now)).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("rejects codes well outside the window (expired and future)", () => {
|
|
119
|
+
const secret = Otp.createSecret()
|
|
120
|
+
expect(Otp.verifyTotp(secret, Otp.generateTotp(secret, now - 300), 2, 2, now)).toBe(false)
|
|
121
|
+
expect(Otp.verifyTotp(secret, Otp.generateTotp(secret, now + 300), 2, 2, now)).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe("totp - RFC 6238 test vectors", () => {
|
|
126
|
+
// exact generated values from RFC 6238 Appendix B; deterministic, no clock dependence
|
|
127
|
+
it("matches the SHA-1 vectors (8 digits)", () => {
|
|
128
|
+
const cases = [
|
|
129
|
+
[59, "94287082"],
|
|
130
|
+
[1111111109, "07081804"],
|
|
131
|
+
[1111111111, "14050471"],
|
|
132
|
+
[1234567890, "89005924"],
|
|
133
|
+
[2000000000, "69279037"],
|
|
134
|
+
[20000000000, "65353130"],
|
|
135
|
+
]
|
|
136
|
+
for (const [t, expected] of cases) {
|
|
137
|
+
expect(Otp.generateTotp(SHA1, t, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(expected)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("matches the SHA-256 vector (8 digits)", () => {
|
|
142
|
+
expect(Otp.generateTotp(SHA256, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe("46119246")
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("matches the SHA-512 vector (8 digits)", () => {
|
|
146
|
+
expect(Otp.generateTotp(SHA512, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe("90693936")
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("verifies an RFC vector through verifyTotp at a fixed time", () => {
|
|
150
|
+
expect(Otp.verifyTotp(SHA1, "94287082", 0, 0, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true)
|
|
151
|
+
expect(Otp.verifyTotp(SHA1, "94287082", 0, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("truncates to 6 digits consistently with the 8-digit vector", () => {
|
|
155
|
+
// RFC 6238 6-digit value at t=59 is the last 6 digits of the 8-digit one
|
|
156
|
+
expect(Otp.generateTotp(SHA1, 59, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe("287082")
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import express from "express"
|
|
2
|
+
import session from "express-session"
|
|
3
|
+
import cookieParser from "cookie-parser"
|
|
4
|
+
import pg from "pg"
|
|
5
|
+
import { createAuthMiddleware, createAuthTables, dropAuthTables, closeInvalidationListeners } from "../index.js"
|
|
6
|
+
|
|
7
|
+
const { Pool } = pg
|
|
8
|
+
|
|
9
|
+
export async function createTestDatabase() {
|
|
10
|
+
const url = process.env.AUTH_TEST_POSTGRES_URL
|
|
11
|
+
const poolOptions = { max: 15, idleTimeoutMillis: 1000 }
|
|
12
|
+
const pool = url
|
|
13
|
+
? new Pool({ connectionString: url, ...poolOptions })
|
|
14
|
+
: new Pool({
|
|
15
|
+
host: process.env.PGHOST || "localhost",
|
|
16
|
+
port: parseInt(process.env.PGPORT || "5432"),
|
|
17
|
+
database: process.env.PGDATABASE || "auth_test",
|
|
18
|
+
user: process.env.PGUSER || "auth",
|
|
19
|
+
password: process.env.PGPASSWORD || "auth_password",
|
|
20
|
+
...poolOptions,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await pool.query("SELECT NOW()")
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error("Failed to connect to test database. Make sure to run: make up")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return pool
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createTwoFactorTestApp() {
|
|
33
|
+
const pool = await createTestDatabase()
|
|
34
|
+
|
|
35
|
+
const app = express()
|
|
36
|
+
|
|
37
|
+
app.use(express.json())
|
|
38
|
+
app.use(cookieParser())
|
|
39
|
+
app.use(
|
|
40
|
+
session({
|
|
41
|
+
secret: "2fa-test-secret-key-for-testing-only",
|
|
42
|
+
resave: false,
|
|
43
|
+
saveUninitialized: false,
|
|
44
|
+
cookie: { secure: false, httpOnly: true },
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const authConfig = {
|
|
49
|
+
db: pool,
|
|
50
|
+
tablePrefix: "mfa_test_",
|
|
51
|
+
minPasswordLength: 6,
|
|
52
|
+
maxPasswordLength: 50,
|
|
53
|
+
rememberDuration: "7d",
|
|
54
|
+
rememberCookieName: "2fa_test_remember_token",
|
|
55
|
+
resyncInterval: "30s",
|
|
56
|
+
twoFactor: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
issuer: "EasyAccess Test",
|
|
59
|
+
totpWindow: 1,
|
|
60
|
+
backupCodesCount: 10,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await dropAuthTables(authConfig)
|
|
65
|
+
await createAuthTables(authConfig)
|
|
66
|
+
|
|
67
|
+
app.use(createAuthMiddleware(authConfig))
|
|
68
|
+
|
|
69
|
+
// standard auth routes
|
|
70
|
+
app.post("/register", async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const { email, password, requireConfirmation } = req.body
|
|
73
|
+
let confirmationToken
|
|
74
|
+
|
|
75
|
+
const account = await req.auth.register(
|
|
76
|
+
email,
|
|
77
|
+
password,
|
|
78
|
+
"2fa-test-user-123",
|
|
79
|
+
requireConfirmation
|
|
80
|
+
? (token) => {
|
|
81
|
+
confirmationToken = token
|
|
82
|
+
}
|
|
83
|
+
: undefined,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
res.json({
|
|
87
|
+
success: true,
|
|
88
|
+
account: {
|
|
89
|
+
id: account.id,
|
|
90
|
+
email: account.email,
|
|
91
|
+
verified: account.verified,
|
|
92
|
+
status: account.status,
|
|
93
|
+
},
|
|
94
|
+
confirmationToken,
|
|
95
|
+
})
|
|
96
|
+
} catch (error) {
|
|
97
|
+
res.status(400).json({ error: error.message })
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
app.post("/login", async (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
const { email, password, remember } = req.body
|
|
104
|
+
await req.auth.login(email, password, remember)
|
|
105
|
+
res.json({ success: true })
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.constructor.name === "SecondFactorRequiredError") {
|
|
108
|
+
// capture the OTP value from the challenge for testing
|
|
109
|
+
if (error.availableMethods.email?.otpValue) {
|
|
110
|
+
lastGeneratedOtp = error.availableMethods.email.otpValue
|
|
111
|
+
} else if (error.availableMethods.sms?.otpValue) {
|
|
112
|
+
lastGeneratedOtp = error.availableMethods.sms.otpValue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return res.status(202).json({
|
|
116
|
+
requiresTwoFactor: true,
|
|
117
|
+
availableMethods: error.availableMethods,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
res.status(401).json({ error: error.message })
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
app.post("/logout", async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
await req.auth.logout()
|
|
127
|
+
res.json({ success: true })
|
|
128
|
+
} catch (error) {
|
|
129
|
+
res.status(500).json({ error: error.message })
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
app.post("/verify-2fa", async (req, res) => {
|
|
134
|
+
try {
|
|
135
|
+
const { code } = req.body || {}
|
|
136
|
+
await req.auth.twoFactor.verify.otp(code)
|
|
137
|
+
await req.auth.completeTwoFactorLogin()
|
|
138
|
+
res.json({ success: true })
|
|
139
|
+
} catch (error) {
|
|
140
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
app.post("/verify-2fa-totp", async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const { code } = req.body || {}
|
|
147
|
+
await req.auth.twoFactor.verify.totp(code)
|
|
148
|
+
await req.auth.completeTwoFactorLogin()
|
|
149
|
+
res.json({ success: true })
|
|
150
|
+
} catch (error) {
|
|
151
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
app.post("/verify-2fa-backup", async (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const { code } = req.body || {}
|
|
158
|
+
await req.auth.twoFactor.verify.backupCode(code)
|
|
159
|
+
await req.auth.completeTwoFactorLogin()
|
|
160
|
+
res.json({ success: true })
|
|
161
|
+
} catch (error) {
|
|
162
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
app.get("/2fa/contact/:mechanism", async (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
const mechanism = parseInt(req.params.mechanism)
|
|
169
|
+
const contact = await req.auth.twoFactor.getContact(mechanism)
|
|
170
|
+
res.json({ contact })
|
|
171
|
+
} catch (error) {
|
|
172
|
+
res.status(400).json({ error: error.message })
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
app.get("/2fa/totp-uri", async (req, res) => {
|
|
177
|
+
try {
|
|
178
|
+
const uri = await req.auth.twoFactor.getTotpUri()
|
|
179
|
+
res.json({ uri })
|
|
180
|
+
} catch (error) {
|
|
181
|
+
res.status(400).json({ error: error.message })
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
app.get("/2fa/is-enabled", async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const enabled = await req.auth.twoFactor.isEnabled()
|
|
188
|
+
const totp = await req.auth.twoFactor.totpEnabled()
|
|
189
|
+
const email = await req.auth.twoFactor.emailEnabled()
|
|
190
|
+
const sms = await req.auth.twoFactor.smsEnabled()
|
|
191
|
+
res.json({ enabled, totp, email, sms })
|
|
192
|
+
} catch (error) {
|
|
193
|
+
res.status(400).json({ error: error.message })
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
app.get("/profile", async (req, res) => {
|
|
198
|
+
if (!req.auth.isLoggedIn()) {
|
|
199
|
+
return res.status(401).json({ error: "Not logged in" })
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
res.json({
|
|
203
|
+
id: req.auth.getId(),
|
|
204
|
+
email: req.auth.getEmail(),
|
|
205
|
+
status: req.auth.getStatus(),
|
|
206
|
+
statusName: req.auth.getStatusName(),
|
|
207
|
+
verified: req.auth.getVerified(),
|
|
208
|
+
roles: req.auth.getRoleNames(),
|
|
209
|
+
remembered: req.auth.isRemembered(),
|
|
210
|
+
isAdmin: await req.auth.isAdmin(),
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// 2FA setup routes
|
|
215
|
+
app.post("/2fa/setup-totp", async (req, res) => {
|
|
216
|
+
try {
|
|
217
|
+
const { requireVerification } = req.body || {}
|
|
218
|
+
const result = await req.auth.twoFactor.setup.totp(requireVerification)
|
|
219
|
+
res.json(result)
|
|
220
|
+
} catch (error) {
|
|
221
|
+
res.status(400).json({ error: error.message })
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
app.post("/2fa/verify-totp-setup", async (req, res) => {
|
|
226
|
+
try {
|
|
227
|
+
const { code } = req.body
|
|
228
|
+
const backupCodes = await req.auth.twoFactor.complete.totp(code)
|
|
229
|
+
res.json({ success: true, backupCodes })
|
|
230
|
+
} catch (error) {
|
|
231
|
+
res.status(400).json({ error: error.message })
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
app.post("/2fa/setup-email", async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
const { requireVerification } = req.body || {}
|
|
238
|
+
await req.auth.twoFactor.setup.email(undefined, requireVerification)
|
|
239
|
+
res.json({ success: true })
|
|
240
|
+
} catch (error) {
|
|
241
|
+
res.status(400).json({ error: error.message })
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
app.post("/2fa/setup-sms", async (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
const { phoneNumber, requireVerification } = req.body || {}
|
|
248
|
+
await req.auth.twoFactor.setup.sms(phoneNumber, requireVerification !== false)
|
|
249
|
+
res.json({ success: true })
|
|
250
|
+
} catch (error) {
|
|
251
|
+
res.status(400).json({ error: error.message })
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
app.post("/2fa/verify-setup", async (req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
const { code, mechanism } = req.body || {}
|
|
258
|
+
if (mechanism === "email") {
|
|
259
|
+
await req.auth.twoFactor.complete.email(code)
|
|
260
|
+
} else if (mechanism === "sms") {
|
|
261
|
+
await req.auth.twoFactor.complete.sms(code)
|
|
262
|
+
}
|
|
263
|
+
res.json({ success: true })
|
|
264
|
+
} catch (error) {
|
|
265
|
+
res.status(400).json({ error: error.message })
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
app.get("/2fa/methods", async (req, res) => {
|
|
270
|
+
try {
|
|
271
|
+
const methods = await req.auth.twoFactor.getEnabledMethods()
|
|
272
|
+
res.json({ methods })
|
|
273
|
+
} catch (error) {
|
|
274
|
+
res.status(400).json({ error: error.message })
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
app.delete("/2fa/method/:mechanism", async (req, res) => {
|
|
279
|
+
try {
|
|
280
|
+
const mechanism = parseInt(req.params.mechanism)
|
|
281
|
+
await req.auth.twoFactor.disable(mechanism)
|
|
282
|
+
res.json({ success: true })
|
|
283
|
+
} catch (error) {
|
|
284
|
+
res.status(400).json({ error: error.message })
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
app.post("/2fa/regenerate-backup-codes", async (req, res) => {
|
|
289
|
+
try {
|
|
290
|
+
const codes = await req.auth.twoFactor.generateNewBackupCodes()
|
|
291
|
+
res.json({ codes })
|
|
292
|
+
} catch (error) {
|
|
293
|
+
res.status(400).json({ error: error.message })
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// utility routes for tests
|
|
298
|
+
app.post("/set-user-session", (req, res) => {
|
|
299
|
+
req.session.userId = req.body.userId || "2fa-test-user-123"
|
|
300
|
+
req.session.save((err) => {
|
|
301
|
+
if (err) {
|
|
302
|
+
return res.status(500).json({ error: "Failed to save session" })
|
|
303
|
+
}
|
|
304
|
+
res.json({ success: true })
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
app.get("/session-info", (req, res) => {
|
|
309
|
+
res.json({
|
|
310
|
+
sessionId: req.sessionID,
|
|
311
|
+
userId: req.session.userId,
|
|
312
|
+
auth: req.session.auth || null,
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
let lastGeneratedOtp = null
|
|
317
|
+
|
|
318
|
+
app.get("/test-otp", (_req, res) => {
|
|
319
|
+
res.json({ otp: lastGeneratedOtp })
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
app,
|
|
324
|
+
pool,
|
|
325
|
+
authConfig,
|
|
326
|
+
cleanup: async () => {
|
|
327
|
+
await closeInvalidationListeners()
|
|
328
|
+
await pool.end()
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
}
|