forked from rejetto/hfs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapi.auth.ts
145 lines (132 loc) · 5.55 KB
/
api.auth.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
import { Account, accountCanLogin, getAccount, getCurrentUsername, getFromAccount, normalizeUsername } from './perm'
import { verifyPassword } from './crypt'
import { ApiError, ApiHandler } from './apiMiddleware'
import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
import {
ADMIN_URI,
HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_NOT_ACCEPTABLE, HTTP_CONFLICT, HTTP_NOT_FOUND
} from './const'
import Koa from 'koa'
import { changeSrpHelper, changePasswordHelper } from './api.helpers'
import { ctxAdminAccess } from './adminApis'
import { prepareState, sessionDuration } from './middlewares'
import { defineConfig } from './config'
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
const keepSessionAlive = defineConfig('keep_session_alive', true)
// centralized log-in state
async function loggedIn(ctx:Koa.Context, username: string | false) {
const s = ctx.session
if (!s)
return ctx.throw(HTTP_SERVER_ERROR,'session')
if (username === false) {
delete s.username
return
}
s.username = normalizeUsername(username)
await prepareState(ctx, async ()=>{}) // updating the state is necessary to send complete session data so that frontend shows admin button
}
function makeExp() {
return !keepSessionAlive.get() ? undefined
: { exp: new Date(Date.now() + sessionDuration.compiled()) }
}
export const login: ApiHandler = async ({ username, password }, ctx) => {
if (!username || !password) // some validation
return new ApiError(HTTP_BAD_REQUEST)
const account = getAccount(username)
if (!account || !accountCanLogin(account))
return new ApiError(HTTP_UNAUTHORIZED)
if (!account.hashed_password)
return new ApiError(HTTP_NOT_ACCEPTABLE)
if (!await verifyPassword(account.hashed_password, password))
return new ApiError(HTTP_UNAUTHORIZED)
if (!ctx.session)
return new ApiError(HTTP_SERVER_ERROR)
await loggedIn(ctx, username)
return { ...makeExp(), redirect: account.redirect }
}
export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
if (!username)
return new ApiError(HTTP_BAD_REQUEST)
const account = getAccount(username)
if (!ctx.session)
return new ApiError(HTTP_SERVER_ERROR)
if (!account || !accountCanLogin(account)) // TODO simulate fake account to prevent knowing valid usernames
return new ApiError(HTTP_UNAUTHORIZED)
try {
const { step1, ...rest } = await srpStep1(account)
const sid = Math.random()
ongoingLogins[sid] = step1
setTimeout(()=> delete ongoingLogins[sid], 60_000)
ctx.session.loggingIn = { username, sid }
return rest
}
catch (code: any) {
return new ApiError(code)
}
}
export async function srpStep1(account: Account) {
if (!account.srp)
throw HTTP_NOT_ACCEPTABLE
const [salt, verifier] = account.srp.split('|')
if (!salt || !verifier)
throw Error("malformed account")
const srpSession = new SRPServerSession(srp6aNimbusRoutines)
const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
return { step1, salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
}
export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
if (!ctx.session)
return new ApiError(HTTP_SERVER_ERROR)
if (!ctx.session.loggingIn)
return new ApiError(HTTP_CONFLICT)
const { username, sid } = ctx.session.loggingIn
const step1 = ongoingLogins[sid]
if (!step1)
return new ApiError(HTTP_NOT_FOUND)
try {
const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
await loggedIn(ctx, username)
delete ctx.session.loggingIn
return {
proof: String(M2),
redirect: ctx.state.account?.redirect,
...await refresh_session({},ctx)
}
}
catch(e) {
return new ApiError(HTTP_UNAUTHORIZED, String(e))
}
finally {
delete ongoingLogins[sid]
}
}
export const logout: ApiHandler = async ({}, ctx) => {
if (!ctx.session)
return new ApiError(HTTP_SERVER_ERROR)
await loggedIn(ctx, false)
// 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
return new ApiError(HTTP_UNAUTHORIZED)
}
export const refresh_session: ApiHandler = async ({}, ctx) => {
return !ctx.session ? new ApiError(HTTP_SERVER_ERROR) : {
username: getCurrentUsername(ctx),
adminUrl: ctxAdminAccess(ctx) ? ctx.state.revProxyPath + ADMIN_URI : undefined,
canChangePassword: canChangePassword(ctx.state.account),
...makeExp(),
}
}
export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
const a = ctx.state.account
return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
: changePasswordHelper(a, newPassword)
}
export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
const a = ctx.state.account
return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
: changeSrpHelper(a, salt, verifier)
}
function canChangePassword(account: Account | undefined) {
return account && !getFromAccount(account, a => a.disable_password_change)
}