WIP: Add Two Factor Authentication #7
8 changed files with 143 additions and 19 deletions
|
@ -12,6 +12,7 @@ func addWebRoutes(f *fiber.App) {
|
|||
f.Get("/admin/account/", web.HandleAdminAccountGet)
|
||||
f.Get("/admin/account/mfasetup", web.HandleAdminAccountMFASetupGet)
|
||||
f.Post("/admin/account/mfasetup", web.HandleAdminAccountMFASetupPost)
|
||||
f.Delete("/admin/account/mfa", web.HandleAdminAccountMFARemove)
|
||||
|
||||
f.Get("/admin/login", web.HandleAdminLoginGet)
|
||||
f.Post("/admin/login", web.HandleAdminLoginPost)
|
||||
|
|
|
@ -3,10 +3,14 @@ package db
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"code.lila.network/adoralaura/go-urlsh/models"
|
||||
)
|
||||
|
||||
// UserHasMFA checks the DB if given models.User has MFA enabled.
|
||||
// Returns (true, nil) if User has MFA enabled, (false, nil) if not.
|
||||
// (false, error) if a DB error happened
|
||||
func UserHasMFA(user models.User) (bool, error) {
|
||||
numrows, err := models.DB.NewSelect().Model((*models.MFAConfig)(nil)).Where("username = ?", user.UserName).Where("active = ?", true).Count(context.Background())
|
||||
if err != nil {
|
||||
|
@ -33,3 +37,30 @@ func ScratchCodeIsUnique(scratchcode string) bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RemoveMFAFromDB removes MFA entries for given models.User from the database.
|
||||
// Returns nil on success, error otherwise.
|
||||
func RemoveMFAFromDB(user models.User) error {
|
||||
hasMfa, err := UserHasMFA(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[RemoveMFAFromDB] Error removing MFA from DB for user %v: %w", user.UserName, err)
|
||||
}
|
||||
|
||||
if !hasMfa {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = models.DB.NewDelete().Model((*models.MFAConfig)(nil)).Where("username = ?", user.UserName).Exec(context.Background())
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return fmt.Errorf("[RemoveMFAFromDB] Error removing MFA Config from DB for user %v: %w", user.UserName, err)
|
||||
}
|
||||
|
||||
_, err = models.DB.NewDelete().Model((*models.MFAScratchCode)(nil)).Where("username = ?", user.UserName).Exec(context.Background())
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return fmt.Errorf("[RemoveMFAFromDB] Error removing MFA scratch codes from DB for user %v: %w", user.UserName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
49
internal/web/multifactor-remove.go
Normal file
49
internal/web/multifactor-remove.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"code.lila.network/adoralaura/go-urlsh/internal/constants"
|
||||
"code.lila.network/adoralaura/go-urlsh/internal/db"
|
||||
"code.lila.network/adoralaura/go-urlsh/internal/misc"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// HandleAdminAccountMFARemove is a DELETE endpoint that handles the deletion
|
||||
// of the logged in users MFA configuration.
|
||||
//
|
||||
// Returns HTTP 401 if no valid user cookie, HTTP 400 if no MFA is configured for the user,
|
||||
// HTTP 500 if a DB error happened or HTTP 204 if the deletion request succeeded.
|
||||
func HandleAdminAccountMFARemove(c *fiber.Ctx) error {
|
||||
|
||||
if !db.IsCookieValid(c.Cookies(constants.LoginCookieName, "")) {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := db.GetUserFromCookie(c.Cookies(constants.LoginCookieName))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
|
||||
}
|
||||
|
||||
hasMfa, err := db.UserHasMFA(user)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
|
||||
}
|
||||
|
||||
if !hasMfa {
|
||||
return misc.New400Error()
|
||||
}
|
||||
|
||||
err = db.RemoveMFAFromDB(user)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error")
|
||||
}
|
||||
|
||||
c.Status(fiber.StatusNoContent)
|
||||
return nil
|
||||
}
|
|
@ -11,9 +11,15 @@
|
|||
<link rel="stylesheet" href="/admin/custom.css">
|
||||
<script src="/admin/main.js" defer></script>
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
a[role=button] {
|
||||
width: fit-content;
|
||||
}
|
||||
|
@ -52,16 +58,22 @@
|
|||
|
||||
<h2>Security</h2>
|
||||
{{if .MFAEnabled}}
|
||||
<a role="button" class="secondary">2fa already enabled</a>
|
||||
<a href="#" onclick="HandleMFARemovalRequest()" role="button">disable Two-Factor Authentication</a>
|
||||
{{else}}
|
||||
<a href="/admin/account/mfasetup" role="button">Setup 2fa</a>
|
||||
<a href="/admin/account/mfasetup" role="button">Setup Two-Factor Authentication</a>
|
||||
{{end}}
|
||||
<dialog id="dialog-info">
|
||||
<h2 id="dialog-heading"></h2>
|
||||
<p id="dialog-text"></p>
|
||||
<form method="dialog">
|
||||
<button>Close</button>
|
||||
</form>
|
||||
<dialog id="mfa-disable-dialog" style="flex-direction: column;">
|
||||
<h2 id="dialog-heading">⚠️ Are you sure you want to disable two-factor authentication?</h2>
|
||||
<p id="dialog-text">Two-factor authentication adds an additional layer of security to your account by requiring
|
||||
more than just a password to sign in.<br />If you need to change your configuration, you can delete and re-add
|
||||
Two-Factor authentication again.<br /><br />Do you really want to remove your Two-Factor authentication?</p>
|
||||
<div class="container grid">
|
||||
|
||||
<!-- change those two buttons and rework the functions to close and initiate deletion-->
|
||||
<button id="button-keep" class="pico-background-green-450" onclick="HandleModalClose('mfa-disable-dialog')">No, keep Two-Factor authentication</button>
|
||||
<button id="button-delete" class="pico-color-red-450 pico-background-red-450" onclick="HandleMFARemoval()">Yes,
|
||||
remove Two-Factor authentication</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</main>
|
||||
{{template "partials/footer" .}}
|
||||
|
|
|
@ -72,19 +72,19 @@
|
|||
<input type="submit" value="Submit" class="contrast" style="width: fit-content;">
|
||||
</div>
|
||||
</form>
|
||||
<dialog id="dialog-success" style="flex-direction: column;">
|
||||
<dialog id="user-dialog" style="flex-direction: column;">
|
||||
<h2 id="dialog-heading">2FA successfully set up!</h2>
|
||||
<p id="dialog-text">These are your recovery codes, keep them somewhere safe, you'll only see them once:</p>
|
||||
<p id="dialog-tokens" class="monospace"></p>
|
||||
<form method="dialog">
|
||||
<button onclick="HandleModalClose('/admin/account')">Close</button>
|
||||
<button onclick="HandleModalClose('user-dialog', '/admin/account')">Close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="dialog-error" style="flex-direction: column;">
|
||||
<h2 id="dialog-heading"></h2>
|
||||
<p id="dialog-text"></p>
|
||||
<form method="dialog">
|
||||
<button onclick="HandleModalClose()">Close</button>
|
||||
<button onclick="HandleModalClose('dialog-error')">Close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</main>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
<link rel="stylesheet" href="/admin/pico.min.css">
|
||||
<link rel="stylesheet" href="/admin/pico.colors.min.css">
|
||||
<link rel="stylesheet" href="/admin/custom.css">
|
||||
<script src="/admin/link_add.js" defer></script>
|
||||
<script src="/admin/misc.js" defer></script>
|
||||
|
||||
</head>
|
||||
|
|
38
web/main.js
38
web/main.js
|
@ -145,6 +145,39 @@ async function HandleMFASetupTokenSubmit() {
|
|||
}
|
||||
}
|
||||
|
||||
async function HandleMFARemovalRequest() {
|
||||
document.getElementById('mfa-disable-dialog').showModal()
|
||||
}
|
||||
|
||||
async function HandleMFARemoval() {
|
||||
let keepButton = document.getElementById('button-keep')
|
||||
let deleteButton = document.getElementById('button-delete')
|
||||
|
||||
keepButton.disabled = true
|
||||
deleteButton.disabled = true
|
||||
deleteButton.value = 'Please wait...'
|
||||
deleteButton.setAttribute('aria-busy', 'true')
|
||||
|
||||
endpoint = "/admin/account/mfa"
|
||||
let response = await fetch(endpoint, {
|
||||
credentials: "include",
|
||||
mode: "same-origin",
|
||||
method: "DELETE"
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
document.location = "/admin/account"
|
||||
} //else {
|
||||
// TODO
|
||||
// Handle error condition
|
||||
//document.cookie = 'gourlsh_mfa_setup=; Max-Age=-1; path=/; domain=' + location.hostname;
|
||||
|
||||
//document.getElementById("dialog-heading").textContent = "Error!"
|
||||
//document.getElementById("dialog-text").textContent = "Something didnt work!"
|
||||
//document.getElementById('dialog-error').showModal()
|
||||
//}
|
||||
}
|
||||
|
||||
async function HandleMFALoginTokenPost() {
|
||||
document.getElementById("submit").disabled = true;
|
||||
document.getElementById("token").disabled = true;
|
||||
|
@ -174,9 +207,8 @@ async function HandleMFALoginTokenPost() {
|
|||
}
|
||||
}
|
||||
|
||||
function HandleModalClose(redir) {
|
||||
document.getElementById('dialog-success').close();
|
||||
document.getElementById('dialog-error').close();
|
||||
function HandleModalClose(id, redir) {
|
||||
document.getElementById(id).close();
|
||||
|
||||
if (redir) {
|
||||
document.location = redir
|
||||
|
|
Loading…
Reference in a new issue