ggdgsdbsdbbb / two_factor_routes.v
112 lines · 102 sloc · 3.82 KB · f69106d41622b949751c70d69da785cd02447f13
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import veb
6import time
7import crypto.hmac
8import crypto.sha256
9import encoding.hex
10
11const two_factor_pending_cookie = 'pending_2fa'
12const two_factor_pending_ttl = 300 // seconds
13
14fn (mut app App) pending_2fa_key(user User) []u8 {
15 return '${user.password}:${user.salt}'.bytes()
16}
17
18fn (mut app App) sign_pending_2fa(user User, expires i64) string {
19 payload := '${user.id}:${expires}'
20 mac := hmac.new(app.pending_2fa_key(user), payload.bytes(), sha256.sum, sha256.block_size)
21 return '${payload}:${hex.encode(mac)}'
22}
23
24fn (mut app App) verify_pending_2fa(token string) ?User {
25 parts := token.split(':')
26 if parts.len != 3 {
27 return none
28 }
29 user_id := parts[0].int()
30 expires := parts[1].i64()
31 sig := parts[2]
32 if expires < time.now().unix() {
33 return none
34 }
35 user := app.get_user_by_id(user_id) or { return none }
36 payload := '${user_id}:${expires}'
37 expected := hmac.new(app.pending_2fa_key(user), payload.bytes(), sha256.sum, sha256.block_size)
38 if hex.encode(expected) != sig {
39 return none
40 }
41 return user
42}
43
44@['/login/2fa']
45pub fn (mut app App) two_factor_prompt(mut ctx Context) veb.Result {
46 pending := ctx.get_cookie(two_factor_pending_cookie) or { return ctx.redirect_to_login() }
47 app.verify_pending_2fa(pending) or { return ctx.redirect_to_login() }
48 return $veb.html('templates/two_factor_login.html')
49}
50
51@['/login/2fa'; post]
52pub fn (mut app App) handle_two_factor_login(mut ctx Context, code string) veb.Result {
53 pending := ctx.get_cookie(two_factor_pending_cookie) or { return ctx.redirect_to_login() }
54 user := app.verify_pending_2fa(pending) or { return ctx.redirect_to_login() }
55 tf := app.find_two_factor(user.id) or { return ctx.redirect_to_login() }
56 if !tf.is_enabled || !verify_totp(tf.secret, code.trim_space()) {
57 ctx.error('Invalid verification code')
58 return $veb.html('templates/two_factor_login.html')
59 }
60 ctx.set_cookie(name: two_factor_pending_cookie, value: '')
61 app.auth_user(mut ctx, user, ctx.ip()) or {
62 ctx.error('There was an error while logging in')
63 return ctx.redirect_to_login()
64 }
65 app.add_security_log(user_id: user.id, kind: .logged_in) or { app.info(err.str()) }
66 return ctx.redirect('/${user.username}')
67}
68
69@['/:username/settings/2fa']
70pub fn (mut app App) view_two_factor_settings(mut ctx Context, username string) veb.Result {
71 if !ctx.logged_in || ctx.user.username != username {
72 return ctx.redirect_to_index()
73 }
74 tf := app.find_two_factor(ctx.user.id) or {
75 TwoFactor{
76 user_id: ctx.user.id
77 }
78 }
79 enabled := tf.is_enabled
80 mut secret := ''
81 mut provisioning_uri := ''
82 if !enabled {
83 secret = if tf.secret == '' { generate_totp_secret() } else { tf.secret }
84 app.upsert_two_factor(ctx.user.id, secret, false) or {}
85 provisioning_uri = totp_provisioning_uri(username, secret)
86 }
87 return $veb.html('templates/two_factor_settings.html')
88}
89
90@['/:username/settings/2fa/enable'; post]
91pub fn (mut app App) handle_enable_two_factor(mut ctx Context, username string) veb.Result {
92 if !ctx.logged_in || ctx.user.username != username {
93 return ctx.redirect_to_index()
94 }
95 code := ctx.form['code'].trim_space()
96 tf := app.find_two_factor(ctx.user.id) or { return ctx.redirect('/${username}/settings/2fa') }
97 if !verify_totp(tf.secret, code) {
98 ctx.error('Invalid verification code')
99 return ctx.redirect('/${username}/settings/2fa')
100 }
101 app.upsert_two_factor(ctx.user.id, tf.secret, true) or {}
102 return ctx.redirect('/${username}/settings/2fa')
103}
104
105@['/:username/settings/2fa/disable'; post]
106pub fn (mut app App) handle_disable_two_factor(mut ctx Context, username string) veb.Result {
107 if !ctx.logged_in || ctx.user.username != username {
108 return ctx.redirect_to_index()
109 }
110 app.delete_two_factor(ctx.user.id) or {}
111 return ctx.redirect('/${username}/settings/2fa')
112}
113