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/", web.HandleAdminAccountGet)
|
||||||
f.Get("/admin/account/mfasetup", web.HandleAdminAccountMFASetupGet)
|
f.Get("/admin/account/mfasetup", web.HandleAdminAccountMFASetupGet)
|
||||||
f.Post("/admin/account/mfasetup", web.HandleAdminAccountMFASetupPost)
|
f.Post("/admin/account/mfasetup", web.HandleAdminAccountMFASetupPost)
|
||||||
|
f.Delete("/admin/account/mfa", web.HandleAdminAccountMFARemove)
|
||||||
|
|
||||||
f.Get("/admin/login", web.HandleAdminLoginGet)
|
f.Get("/admin/login", web.HandleAdminLoginGet)
|
||||||
f.Post("/admin/login", web.HandleAdminLoginPost)
|
f.Post("/admin/login", web.HandleAdminLoginPost)
|
||||||
|
|
|
@ -3,10 +3,14 @@ package db
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"code.lila.network/adoralaura/go-urlsh/models"
|
"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) {
|
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())
|
numrows, err := models.DB.NewSelect().Model((*models.MFAConfig)(nil)).Where("username = ?", user.UserName).Where("active = ?", true).Count(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -33,3 +37,30 @@ func ScratchCodeIsUnique(scratchcode string) bool {
|
||||||
}
|
}
|
||||||
return true
|
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">
|
<link rel="stylesheet" href="/admin/custom.css">
|
||||||
<script src="/admin/main.js" defer></script>
|
<script src="/admin/main.js" defer></script>
|
||||||
<style>
|
<style>
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a[role=button] {
|
a[role=button] {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +42,7 @@
|
||||||
<a href="/admin/apikeys/">API Keys</a>
|
<a href="/admin/apikeys/">API Keys</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript: void(0)" >Users (coming soon)</a>
|
<a href="javascript: void(0)">Users (coming soon)</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript: Logout()" style="color: red;">Logout</a>
|
<a href="javascript: Logout()" style="color: red;">Logout</a>
|
||||||
|
@ -52,16 +58,22 @@
|
||||||
|
|
||||||
<h2>Security</h2>
|
<h2>Security</h2>
|
||||||
{{if .MFAEnabled}}
|
{{if .MFAEnabled}}
|
||||||
<a role="button" class="secondary">2fa already enabled</a>
|
<a href="#" onclick="HandleMFARemovalRequest()" role="button">disable Two-Factor Authentication</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/admin/account/mfasetup" role="button">Setup 2fa</a>
|
<a href="/admin/account/mfasetup" role="button">Setup Two-Factor Authentication</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<dialog id="dialog-info">
|
<dialog id="mfa-disable-dialog" style="flex-direction: column;">
|
||||||
<h2 id="dialog-heading"></h2>
|
<h2 id="dialog-heading">⚠️ Are you sure you want to disable two-factor authentication?</h2>
|
||||||
<p id="dialog-text"></p>
|
<p id="dialog-text">Two-factor authentication adds an additional layer of security to your account by requiring
|
||||||
<form method="dialog">
|
more than just a password to sign in.<br />If you need to change your configuration, you can delete and re-add
|
||||||
<button>Close</button>
|
Two-Factor authentication again.<br /><br />Do you really want to remove your Two-Factor authentication?</p>
|
||||||
</form>
|
<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>
|
</dialog>
|
||||||
</main>
|
</main>
|
||||||
{{template "partials/footer" .}}
|
{{template "partials/footer" .}}
|
||||||
|
|
|
@ -72,19 +72,19 @@
|
||||||
<input type="submit" value="Submit" class="contrast" style="width: fit-content;">
|
<input type="submit" value="Submit" class="contrast" style="width: fit-content;">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
<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-text">These are your recovery codes, keep them somewhere safe, you'll only see them once:</p>
|
||||||
<p id="dialog-tokens" class="monospace"></p>
|
<p id="dialog-tokens" class="monospace"></p>
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button onclick="HandleModalClose('/admin/account')">Close</button>
|
<button onclick="HandleModalClose('user-dialog', '/admin/account')">Close</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<dialog id="dialog-error" style="flex-direction: column;">
|
<dialog id="dialog-error" style="flex-direction: column;">
|
||||||
<h2 id="dialog-heading"></h2>
|
<h2 id="dialog-heading"></h2>
|
||||||
<p id="dialog-text"></p>
|
<p id="dialog-text"></p>
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button onclick="HandleModalClose()">Close</button>
|
<button onclick="HandleModalClose('dialog-error')">Close</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<link rel="stylesheet" href="/admin/pico.min.css">
|
<link rel="stylesheet" href="/admin/pico.min.css">
|
||||||
<link rel="stylesheet" href="/admin/pico.colors.min.css">
|
<link rel="stylesheet" href="/admin/pico.colors.min.css">
|
||||||
<link rel="stylesheet" href="/admin/custom.css">
|
<link rel="stylesheet" href="/admin/custom.css">
|
||||||
<script src="/admin/link_add.js" defer></script>
|
|
||||||
<script src="/admin/misc.js" defer></script>
|
<script src="/admin/misc.js" defer></script>
|
||||||
|
|
||||||
</head>
|
</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() {
|
async function HandleMFALoginTokenPost() {
|
||||||
document.getElementById("submit").disabled = true;
|
document.getElementById("submit").disabled = true;
|
||||||
document.getElementById("token").disabled = true;
|
document.getElementById("token").disabled = true;
|
||||||
|
@ -174,9 +207,8 @@ async function HandleMFALoginTokenPost() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HandleModalClose(redir) {
|
function HandleModalClose(id, redir) {
|
||||||
document.getElementById('dialog-success').close();
|
document.getElementById(id).close();
|
||||||
document.getElementById('dialog-error').close();
|
|
||||||
|
|
||||||
if (redir) {
|
if (redir) {
|
||||||
document.location = redir
|
document.location = redir
|
||||||
|
|
Loading…
Reference in a new issue