Skip to content

Commit f3efaae

Browse files
authored
Merge pull request #5141 from NginxProxyManager/develop
v2.13.6
2 parents 847c58b + 7b3c1fd commit f3efaae

101 files changed

Lines changed: 6281 additions & 1300 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.13.5
1+
2.13.6

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<p align="center">
22
<img src="https://nginxproxymanager.com/github.png">
33
<br><br>
4-
<img src="https://img.shields.io/badge/version-2.13.5-green.svg?style=for-the-badge">
4+
<img src="https://img.shields.io/badge/version-2.13.6-green.svg?style=for-the-badge">
55
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
66
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
77
</a>

backend/certbot/dns-plugins.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@
255255
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
256256
"full_plugin_name": "dns-gcore"
257257
},
258+
"glesys": {
259+
"name": "Glesys",
260+
"package_name": "certbot-dns-glesys",
261+
"version": "~=2.1.0",
262+
"dependencies": "",
263+
"credentials": "dns_glesys_user = CL00000\ndns_glesys_password = apikeyvalue",
264+
"full_plugin_name": "dns-glesys"
265+
},
258266
"godaddy": {
259267
"name": "GoDaddy",
260268
"package_name": "certbot-dns-godaddy",
@@ -287,6 +295,14 @@
287295
"credentials": "dns_he_user = Me\ndns_he_pass = my HE password",
288296
"full_plugin_name": "dns-he"
289297
},
298+
"he-ddns": {
299+
"name": "Hurricane Electric - DDNS",
300+
"package_name": "certbot-dns-he-ddns",
301+
"version": "~=0.1.0",
302+
"dependencies": "",
303+
"credentials": "dns_he_ddns_password = verysecurepassword",
304+
"full_plugin_name": "dns-he-ddns"
305+
},
290306
"hetzner": {
291307
"name": "Hetzner",
292308
"package_name": "certbot-dns-hetzner",
@@ -367,6 +383,14 @@
367383
"credentials": "dns_joker_username = <Dynamic DNS Authentication Username>\ndns_joker_password = <Dynamic DNS Authentication Password>\ndns_joker_domain = <Dynamic DNS Domain>",
368384
"full_plugin_name": "dns-joker"
369385
},
386+
"kas": {
387+
"name": "All-Inkl",
388+
"package_name": "certbot-dns-kas",
389+
"version": "~=0.1.1",
390+
"dependencies": "kasserver",
391+
"credentials": "dns_kas_user = your_kas_user\ndns_kas_password = your_kas_password",
392+
"full_plugin_name": "dns-kas"
393+
},
370394
"leaseweb": {
371395
"name": "LeaseWeb",
372396
"package_name": "certbot-dns-leaseweb",
@@ -527,6 +551,14 @@
527551
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
528552
"full_plugin_name": "dns-route53"
529553
},
554+
"simply": {
555+
"name": "Simply",
556+
"package_name": "certbot-dns-simply",
557+
"version": "~=0.1.2",
558+
"dependencies": "",
559+
"credentials": "dns_simply_account_name = UExxxxxx\ndns_simply_api_key = DsHJdsjh2812872sahj",
560+
"full_plugin_name": "dns-simply"
561+
},
530562
"spaceship": {
531563
"name": "Spaceship",
532564
"package_name": "certbot-dns-spaceship",

backend/internal/2fa.js

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import crypto from "node:crypto";
2+
import bcrypt from "bcrypt";
3+
import { authenticator } from "otplib";
4+
import errs from "../lib/error.js";
5+
import authModel from "../models/auth.js";
6+
import internalUser from "./user.js";
7+
8+
const APP_NAME = "Nginx Proxy Manager";
9+
const BACKUP_CODE_COUNT = 8;
10+
11+
/**
12+
* Generate backup codes
13+
* @returns {Promise<{plain: string[], hashed: string[]}>}
14+
*/
15+
const generateBackupCodes = async () => {
16+
const plain = [];
17+
const hashed = [];
18+
19+
for (let i = 0; i < BACKUP_CODE_COUNT; i++) {
20+
const code = crypto.randomBytes(4).toString("hex").toUpperCase();
21+
plain.push(code);
22+
const hash = await bcrypt.hash(code, 10);
23+
hashed.push(hash);
24+
}
25+
26+
return { plain, hashed };
27+
};
28+
29+
const internal2fa = {
30+
31+
/**
32+
* Check if user has 2FA enabled
33+
* @param {number} userId
34+
* @returns {Promise<boolean>}
35+
*/
36+
isEnabled: async (userId) => {
37+
const auth = await internal2fa.getUserPasswordAuth(userId);
38+
return auth?.meta?.totp_enabled === true;
39+
},
40+
41+
/**
42+
* Get 2FA status for user
43+
* @param {Access} access
44+
* @param {number} userId
45+
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
46+
*/
47+
getStatus: async (access, userId) => {
48+
await access.can("users:password", userId);
49+
await internalUser.get(access, { id: userId });
50+
const auth = await internal2fa.getUserPasswordAuth(userId);
51+
const enabled = auth?.meta?.totp_enabled === true;
52+
let backup_codes_remaining = 0;
53+
54+
if (enabled) {
55+
const backupCodes = auth.meta.backup_codes || [];
56+
backup_codes_remaining = backupCodes.length;
57+
}
58+
59+
return {
60+
enabled,
61+
backup_codes_remaining,
62+
};
63+
},
64+
65+
/**
66+
* Start 2FA setup - store pending secret
67+
*
68+
* @param {Access} access
69+
* @param {number} userId
70+
* @returns {Promise<{secret: string, otpauth_url: string}>}
71+
*/
72+
startSetup: async (access, userId) => {
73+
await access.can("users:password", userId);
74+
const user = await internalUser.get(access, { id: userId });
75+
const secret = authenticator.generateSecret();
76+
const otpauth_url = authenticator.keyuri(user.email, APP_NAME, secret);
77+
const auth = await internal2fa.getUserPasswordAuth(userId);
78+
79+
// ensure user isn't already setup for 2fa
80+
const enabled = auth?.meta?.totp_enabled === true;
81+
if (enabled) {
82+
throw new errs.ValidationError("2FA is already enabled");
83+
}
84+
85+
const meta = auth.meta || {};
86+
meta.totp_pending_secret = secret;
87+
88+
await authModel.query()
89+
.where("id", auth.id)
90+
.andWhere("user_id", userId)
91+
.andWhere("type", "password")
92+
.patch({ meta });
93+
94+
return { secret, otpauth_url };
95+
},
96+
97+
/**
98+
* Enable 2FA after verifying code
99+
*
100+
* @param {Access} access
101+
* @param {number} userId
102+
* @param {string} code
103+
* @returns {Promise<{backup_codes: string[]}>}
104+
*/
105+
enable: async (access, userId, code) => {
106+
await access.can("users:password", userId);
107+
await internalUser.get(access, { id: userId });
108+
const auth = await internal2fa.getUserPasswordAuth(userId);
109+
const secret = auth?.meta?.totp_pending_secret || false;
110+
111+
if (!secret) {
112+
throw new errs.ValidationError("No pending 2FA setup found");
113+
}
114+
115+
const valid = authenticator.verify({ token: code, secret });
116+
if (!valid) {
117+
throw new errs.ValidationError("Invalid verification code");
118+
}
119+
120+
const { plain, hashed } = await generateBackupCodes();
121+
122+
const meta = {
123+
...auth.meta,
124+
totp_secret: secret,
125+
totp_enabled: true,
126+
totp_enabled_at: new Date().toISOString(),
127+
backup_codes: hashed,
128+
};
129+
delete meta.totp_pending_secret;
130+
131+
await authModel
132+
.query()
133+
.where("id", auth.id)
134+
.andWhere("user_id", userId)
135+
.andWhere("type", "password")
136+
.patch({ meta });
137+
138+
return { backup_codes: plain };
139+
},
140+
141+
/**
142+
* Disable 2FA
143+
*
144+
* @param {Access} access
145+
* @param {number} userId
146+
* @param {string} code
147+
* @returns {Promise<void>}
148+
*/
149+
disable: async (access, userId, code) => {
150+
await access.can("users:password", userId);
151+
await internalUser.get(access, { id: userId });
152+
const auth = await internal2fa.getUserPasswordAuth(userId);
153+
154+
const enabled = auth?.meta?.totp_enabled === true;
155+
if (!enabled) {
156+
throw new errs.ValidationError("2FA is not enabled");
157+
}
158+
159+
const valid = authenticator.verify({
160+
token: code,
161+
secret: auth.meta.totp_secret,
162+
});
163+
164+
if (!valid) {
165+
throw new errs.AuthError("Invalid verification code");
166+
}
167+
168+
const meta = { ...auth.meta };
169+
delete meta.totp_secret;
170+
delete meta.totp_enabled;
171+
delete meta.totp_enabled_at;
172+
delete meta.backup_codes;
173+
174+
await authModel
175+
.query()
176+
.where("id", auth.id)
177+
.andWhere("user_id", userId)
178+
.andWhere("type", "password")
179+
.patch({ meta });
180+
},
181+
182+
/**
183+
* Verify 2FA code for login
184+
*
185+
* @param {number} userId
186+
* @param {string} token
187+
* @returns {Promise<boolean>}
188+
*/
189+
verifyForLogin: async (userId, token) => {
190+
const auth = await internal2fa.getUserPasswordAuth(userId);
191+
const secret = auth?.meta?.totp_secret || false;
192+
193+
if (!secret) {
194+
return false;
195+
}
196+
197+
// Try TOTP code first
198+
const valid = authenticator.verify({
199+
token,
200+
secret,
201+
});
202+
203+
if (valid) {
204+
return true;
205+
}
206+
207+
// Try backup codes
208+
const backupCodes = auth?.meta?.backup_codes || [];
209+
for (let i = 0; i < backupCodes.length; i++) {
210+
const match = await bcrypt.compare(code.toUpperCase(), backupCodes[i]);
211+
if (match) {
212+
// Remove used backup code
213+
const updatedCodes = [...backupCodes];
214+
updatedCodes.splice(i, 1);
215+
const meta = { ...auth.meta, backup_codes: updatedCodes };
216+
await authModel
217+
.query()
218+
.where("id", auth.id)
219+
.andWhere("user_id", userId)
220+
.andWhere("type", "password")
221+
.patch({ meta });
222+
return true;
223+
}
224+
}
225+
226+
return false;
227+
},
228+
229+
/**
230+
* Regenerate backup codes
231+
*
232+
* @param {Access} access
233+
* @param {number} userId
234+
* @param {string} token
235+
* @returns {Promise<{backup_codes: string[]}>}
236+
*/
237+
regenerateBackupCodes: async (access, userId, token) => {
238+
await access.can("users:password", userId);
239+
await internalUser.get(access, { id: userId });
240+
const auth = await internal2fa.getUserPasswordAuth(userId);
241+
const enabled = auth?.meta?.totp_enabled === true;
242+
const secret = auth?.meta?.totp_secret || false;
243+
244+
if (!enabled) {
245+
throw new errs.ValidationError("2FA is not enabled");
246+
}
247+
if (!secret) {
248+
throw new errs.ValidationError("No 2FA secret found");
249+
}
250+
251+
const valid = authenticator.verify({
252+
token,
253+
secret,
254+
});
255+
256+
if (!valid) {
257+
throw new errs.ValidationError("Invalid verification code");
258+
}
259+
260+
const { plain, hashed } = await generateBackupCodes();
261+
262+
const meta = { ...auth.meta, backup_codes: hashed };
263+
await authModel
264+
.query()
265+
.where("id", auth.id)
266+
.andWhere("user_id", userId)
267+
.andWhere("type", "password")
268+
.patch({ meta });
269+
270+
return { backup_codes: plain };
271+
},
272+
273+
getUserPasswordAuth: async (userId) => {
274+
const auth = await authModel
275+
.query()
276+
.where("user_id", userId)
277+
.andWhere("type", "password")
278+
.first();
279+
280+
if (!auth) {
281+
throw new errs.ItemNotFoundError("Auth not found");
282+
}
283+
284+
return auth;
285+
},
286+
};
287+
288+
export default internal2fa;

0 commit comments

Comments
 (0)