support login with http-basic-authentication

Massimo Melina committed Jan 25, 2024 at 16:31 UTC 0b8e159dcd9d87aa8c80e53a322e24cbb51dd95f
3 files changed +20 -16
src/api.auth.ts
+3 -3
@@ -6,7 +6,7 @@ import { SRPServerSessionStep1 } from 'tssrp6a'
6 import { ADMIN_URI, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_FOUND } from './const'
7 import { ctxAdminAccess } from './adminApis'
8 import { sessionDuration } from './middlewares'
9 -import { getCurrentUsername, loggedIn, srpStep1 } from './auth'
9 +import { getCurrentUsername, setLoggedIn, srpStep1 } from './auth'
10 import { defineConfig } from './config'
11
12 const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
@@ -52,7 +52,7 @@ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
52 return new ApiError(HTTP_NOT_FOUND)
53 try {
54 const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
55 - await loggedIn(ctx, username)
55 + await setLoggedIn(ctx, username)
56 delete ctx.session.loggingIn
57 return {
58 proof: String(M2),
@@ -73,7 +73,7 @@ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
73 export const logout: ApiHandler = async ({}, ctx) => {
74 if (!ctx.session)
75 return new ApiError(HTTP_SERVER_ERROR)
76 - await loggedIn(ctx, false)
76 + await setLoggedIn(ctx, false)
77 // 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
78 return new ApiError(HTTP_UNAUTHORIZED)
79 }
src/auth.ts
+2 -3
@@ -2,7 +2,6 @@ import { Account, getAccount, normalizeUsername } from './perm'
2 import { HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './cross-const'
3 import { SRPParameters, SRPRoutines, SRPServerSession } from 'tssrp6a'
4 import { Context } from 'koa'
5 -import { prepareState } from './middlewares'
5 import { srpClientPart } from './srp'
6 import { getOrSet } from './cross'
7 import { createHash } from 'node:crypto'
@@ -39,7 +38,7 @@ export function getCurrentUsername(ctx: Context): string {
38 }
39
40 // centralized log-in state
42 -export async function loggedIn(ctx: Context, username: string | false) {
41 +export async function setLoggedIn(ctx: Context, username: string | false) {
42 const s = ctx.session
43 if (!s)
44 return ctx.throw(HTTP_SERVER_ERROR,'session')
@@ -49,7 +48,7 @@ export async function loggedIn(ctx: Context, username: string | false) {
48 }
49 invalidSessions.delete(username)
50 s.username = normalizeUsername(username)
52 - await prepareState(ctx, async ()=>{}) // updating the state is necessary to send complete session data so that frontend shows admin button
51 + ctx.state.account = getAccount(username)
52 }
53
54 export const invalidSessions = new Set<string>() // since session are currently stored in cookies, we need to memorize this until we meet again
src/middlewares.ts
+15 -10
@@ -9,7 +9,7 @@ import { applyBlock } from './block'
9 import { Account, accountCanLogin, getAccount } from './perm'
10 import { Connection, disconnect, normalizeIp, socket2connection, updateConnectionForCtx } from './connections'
11 import basicAuth from 'basic-auth'
12 -import { invalidSessions, srpCheck } from './auth'
12 +import { invalidSessions, setLoggedIn, srpCheck } from './auth'
13 import { constants } from 'zlib'
14 import { baseUrl, getHttpsWorkingPort } from './listen'
15 import { defineConfig } from './config'
@@ -97,7 +97,7 @@ export function getProxyDetected() {
97 }
98
99 export const prepareState: Koa.Middleware = async (ctx, next) => {
100 - if (ctx.session) {
100 + if (ctx.session?.username) {
101 if (invalidSessions.delete(ctx.session.username))
102 delete ctx.session.username
103 ctx.session.maxAge = sessionDuration.compiled()
@@ -111,22 +111,27 @@ export const prepareState: Koa.Middleware = async (ctx, next) => {
111 updateConnectionForCtx(ctx)
112 await next()
113
114 - async function urlLogin() {
114 + function urlLogin() {
115 const { login } = ctx.query
116 if (!login) return
117 const [u,p] = splitAt(':', String(login))
118 + ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length-1)) // redirect to hide credentials
119 + return doLogin(u, p)
120 + }
121 +
122 + function getHttpAccount() {
123 + const credentials = basicAuth(ctx.req)
124 + return doLogin(credentials?.name||'', credentials?.pass||'')
125 + }
126 +
127 + async function doLogin(u: string, p: string) {
128 const a = await srpCheck(u, p)
129 if (a) {
120 - ctx.session!.username = a.username
121 - ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length-1)) // redirect to hide credentials
130 + setLoggedIn(ctx, a.username)
131 + ctx.headers['x-username'] = a.username // give an easier way to determine if the login was successful
132 }
133 return a
134 }
125 -
126 - async function getHttpAccount() {
127 - const credentials = basicAuth(ctx.req)
128 - return srpCheck(credentials?.name||'', credentials?.pass||'')
129 - }
135 }
136
137 declare module "koa" {