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