aboutsummaryrefslogtreecommitdiff
path: root/lib/vnlib.browser/src/mfa/login.ts
blob: 57465efe41e7bcc22cecf8a082487b7705e37b4c (plain)
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// Copyright (c) 2024 Vaughn Nugent
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import { decodeJwt, type JWTPayload } from "jose";
import { forEach, isNil } from 'lodash-es';
import { get } from "@vueuse/core";
import { debugLog } from "../util"
import { useUser, type ExtendedLoginResponse } from "../user";
import { AccountEndpoint, type IUserInternal } from "../user/internal";
import { useAxiosInternal } from "../axios";
import type { Ref } from "vue";
import type { Axios } from "axios";
import type { ITokenResponse } from "../session";
import type { WebMessage } from "../types";

export type MfaMethod = 'totp' | 'fido' | 'pkotp';

export interface IMfaSubmission {
    /**
     * TOTP code submission
     */
    readonly code?:  number;
}

export interface IMfaMessage extends JWTPayload {
    /**
     * The type of mfa upgrade
     */
    readonly type: MfaMethod;
    /**
     * The time in seconds that the mfa upgrade is valid for
     */
    readonly expires?: number;
}

export interface IMfaFlowContinuiation extends IMfaMessage {
    /**
     * Sumits the mfa message to the server and attempts to complete 
     * a login process
     * @param message The mfa submission to send to the server
     * @returns A promise that resolves to a login result
     */
    submit: <T>(message: IMfaSubmission) => Promise<WebMessage<T>>;
}

/**
 * Interface for handling mfa upgrade submissions to the server
 */
export interface MfaSumissionHandler {
    /**
     * Submits an mfa upgrade submission to the server
     * @param submission The mfa upgrade submission to send to the server to complete an mfa login
     */
    submit<T>(submission: IMfaSubmission): Promise<WebMessage<T>>;
}

/**
 * Interface for processing mfa messages from the server of a given 
 * mfa type
 */
export interface IMfaTypeProcessor {
    readonly type: MfaMethod;
    /**
     * Processes an MFA message payload of the registered mfa type
     * @param payload The mfa message from the server as a string
     * @param onSubmit The submission handler to use to submit the mfa upgrade
     * @returns A promise that resolves to a Login request
     */
    processMfa: (payload: IMfaMessage, onSubmit : MfaSumissionHandler) => Promise<IMfaFlowContinuiation>
}

export interface IMfaLoginManager {
    /**
     * Logs a user in with the given username and password, and returns a login result
     * or a mfa flow continuation depending on the login flow
     * @param userName The username of the user to login
     * @param password The password of the user to login
     */
    login(userName: string, password: string): Promise<WebMessage | IMfaFlowContinuiation>;
}

const getMfaProcessor = (user: IUserInternal, axios:Ref<Axios>) =>{

    //Store handlers by their mfa type
    const handlerMap = new Map<MfaMethod, IMfaTypeProcessor>();

    //Creates a submission handler for an mfa upgrade
    const createSubHandler = (upgrade : string, finalize: (res: ITokenResponse) => Promise<void>) :MfaSumissionHandler => {

        const submit = async<T>(submission: IMfaSubmission): Promise<WebMessage<T>> => {
            const { post } = get(axios);

            //All mfa upgrades use the account login endpoint
            const ep = user.getEndpoint(AccountEndpoint.Login);

            //Get the mfa type from the upgrade message
            const { type } = decodeJwt(upgrade) as IMfaMessage;

            //MFA upgrades currently use the login endpoint with a query string. The type that is captured from the upgrade
            const endpoint = `${ep}?mfa=${type}`;

            //Submit request
            const response = await post<ITokenResponse>(endpoint, {
                //Pass raw upgrade message back to server as its signed
                upgrade,
                //publish submission
                ...submission,
                //Local time as an ISO string of the current time
                localtime: new Date().toISOString()
            })

            // If the server returned a token, complete the login
            if (response.data.success && !isNil(response.data.token)) {
                await finalize(response.data)
            }

            return response.data as WebMessage<T>;
        }

        return { submit }
    }

    const processMfa = (mfaMessage: string, finalize: (res: ITokenResponse) => Promise<void>) : Promise<IMfaFlowContinuiation> => {
        
        //Mfa message is a jwt, decode it (unsecure decode)
        const mfa = decodeJwt(mfaMessage) as IMfaMessage;
        debugLog(mfa)

        //Select the mfa handler
        const handler = handlerMap.get(mfa.type);

        //If no handler is found, throw an error
        if(!handler){
            throw new Error('Server responded with an unsupported two factor auth type, login cannot continue.')
        }

        //Init submission handler
        const submitHandler = createSubHandler(mfaMessage, finalize);

        //Process the mfa message
        return handler.processMfa(mfa, submitHandler);
    }

    const registerHandler = (handler: IMfaTypeProcessor) => {
        handlerMap.set(handler.type, handler);
    }

    return { processMfa, registerHandler }
}

/**
 * Gets a pre-configured TOTP mfa flow processor
 * @returns A pre-configured TOTP mfa flow processor
 */
export const totpMfaProcessor = (): IMfaTypeProcessor => {

    const processMfa = async (payload: IMfaMessage, onSubmit: MfaSumissionHandler): Promise<IMfaFlowContinuiation> => {
        return { ... payload, submit: onSubmit.submit }
    }

    return {
        type: 'totp',
        processMfa
    }
}

/**
 * Gets the mfa login handler for the accounts backend
 * @param handlers A list of mfa handlers to register
 * @returns The configured mfa login handler
 */
export const useMfaLogin = (handlers : IMfaTypeProcessor[]): IMfaLoginManager => {
    
    //get the user instance
    const user = useUser() as IUserInternal

    const axios = useAxiosInternal(null)

    //Get new mfa processor
    const mfaProcessor = getMfaProcessor(user, axios);

    //Login that passes through logins with mfa
    const login = async <T>(userName: string, password: string) : Promise<ExtendedLoginResponse<T> | IMfaFlowContinuiation> => {

        //User-login with mfa response
        const response = await user.login(userName, password);

        const { mfa } = response as { mfa?: boolean }

        //Get the mfa upgrade message from the server
        if (mfa === true){

            // Process the two factor auth message and add it to the response
            const result = await mfaProcessor.processMfa(response.result as string, response.finalize);

            return {
                ...result
            };
        }

        //If no mfa upgrade message is returned, the login is complete
        return response as ExtendedLoginResponse<T>;
    }

    //Register all the handlers
    forEach(handlers, mfaProcessor.registerHandler);

    return { login }
}