http basic authentication support https://github.com/rejetto/hfs/issues/57
Massimo Melina committed
Jun 26, 2022 at 21:42 UTC
ef589f2db28f819929c3ec0145e373e801da7076
5 files changed
+44
-12
server/package.json
+2
@@ -12,6 +12,7 @@
12
"dependencies": {
13
"@koa/router": "^10.1.1",
14
"@node-rs/crc32": "^1.5.1",
15
+ "basic-auth": "^2.0.1",
16
"buffer-crc32": "https://github.com/rejetto/buffer-crc32.git",
17
"cidr-tools": "^4.3.0",
18
"fast-glob": "^3.2.7",
@@ -30,6 +31,7 @@
31
},
32
"devDependencies": {
33
"@types/archiver": "^5.1.1",
34
+ "@types/basic-auth": "^1.1.3",
35
"@types/koa": "^2.13.4",
36
"@types/koa__router": "^8.0.11",
37
"@types/koa-compress": "^4.0.3",
server/src/api.auth.ts
+18
-9
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { getAccount, getCurrentUsername } from './perm'
3
+import { Account, getAccount, getCurrentUsername } from './perm'
4
import { verifyPassword } from './crypt'
5
import { ApiError, ApiHandler } from './apiMiddleware'
6
import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
@@ -12,7 +12,6 @@ import { ctxAdminAccess } from './adminApis'
12
import { prepareState } from './middlewares'
13
14
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
15
-const srpSession = new SRPServerSession(srp6aNimbusRoutines)
15
const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
16
17
// centralized log-in state
@@ -61,16 +60,26 @@ export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
60
return new ApiError(500)
61
if (!account) // TODO simulate fake account to prevent knowing valid usernames
62
return new ApiError(UNAUTHORIZED)
63
+ try {
64
+ const { step1, ...rest } = await srpStep1(account)
65
+ const sid = Math.random()
66
+ ongoingLogins[sid] = step1
67
+ setTimeout(()=> delete ongoingLogins[sid], 60_000)
68
+ ctx.session.login = { username, sid }
69
+ return rest
70
+ }
71
+ catch (code: any) {
72
+ return new ApiError(code)
73
+ }
74
+}
75
+
76
+export async function srpStep1(account: Account) {
77
if (!account.srp)
65
- return new ApiError(406) // unacceptable
78
+ throw 406 // unacceptable
79
const [salt, verifier] = account.srp.split('|')
80
+ const srpSession = new SRPServerSession(srp6aNimbusRoutines)
81
const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
68
- const sid = Math.random()
69
- ongoingLogins[sid] = step1
70
- setTimeout(()=> delete ongoingLogins[sid], 60_000)
71
-
72
- ctx.session.login = { username, sid }
73
- return { salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
82
+ return { step1, salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
83
}
84
85
export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
server/src/middlewares.ts
+23
-1
@@ -16,6 +16,9 @@ import { Readable } from 'stream'
16
import { applyBlock } from './block'
17
import { getAccount, getCurrentUsername } from './perm'
18
import { socket2connection, updateConnection, normalizeIp } from './connections'
19
+import basicAuth from 'basic-auth'
20
+import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
21
+import { srpStep1 } from './api.auth'
22
23
export const gzipper = compress({
24
threshold: 2048,
@@ -78,6 +81,7 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
81
// this folder was requested without the trailing / and we may still log in
82
if (isFolder && !path.endsWith('/') && !ctx.state.account)
83
return ctx.redirect(path + '/')
84
+ ctx.set('WWW-Authenticate', 'Basic')
85
return serveFrontendFiles(ctx, next)
86
}
87
const { get } = ctx.query
@@ -119,9 +123,27 @@ export function getProxyDetected() {
123
}
124
export const prepareState: Koa.Middleware = async (ctx, next) => {
125
// calculate these once and for all
122
- ctx.state.account = getAccount(getCurrentUsername(ctx))
126
+ ctx.state.account = await getHttpAccount(ctx) ?? getAccount(getCurrentUsername(ctx))
127
const conn = ctx.state.connection = socket2connection(ctx.socket)
128
if (conn)
129
updateConnection(conn, { ctx })
130
await next()
131
}
132
+
133
+async function getHttpAccount(ctx: Koa.Context) {
134
+ const credentials = basicAuth(ctx.req)
135
+ const account = getAccount(credentials?.name||'')
136
+ if (account && await srpCheck(account.username, credentials!.pass))
137
+ return account
138
+}
139
+
140
+async function srpCheck(username: string, password: string) {
141
+ username = username.toLocaleLowerCase()
142
+ const account = getAccount(username)
143
+ if (!account?.srp) return false
144
+ const { step1, salt, pubKey } = await srpStep1(account)
145
+ const client = new SRPClientSession(new SRPRoutines(new SRPParameters()))
146
+ const clientRes1 = await client.step1(username, password)
147
+ const clientRes2 = await clientRes1.step2(BigInt(salt), BigInt(pubKey))
148
+ return await step1.step2(clientRes2.A, clientRes2.M1).then(() => true, () => false)
149
+}
server/src/perm.ts
+1
-1
@@ -29,7 +29,7 @@ export function getAccounts() {
29
}
30
31
export function getCurrentUsername(ctx: Koa.Context): string {
32
- return ctx.session?.username || ''
32
+ return ctx.state.account?.username || ctx.session?.username || ''
33
}
34
35
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
todo.md
-1
@@ -5,7 +5,6 @@
5
- show public ip use, https://github.com/sindresorhus/public-ip
6
- configure router with upnp. If it fails, suggest a guide
7
- offer ddns registration/update
8
-- support http authentication
8
- use dialogs instead of side-forms on mobile (admin/fs+accounts)
9
- admin/fs: sort items
10
- admin/fs: render virtual folders differently