mirror of
https://github.com/thomiceli/opengist.git
synced 2025-07-12 19:01:50 +02:00
Add passkeys support + MFA (#341)
This commit is contained in:
@ -1,9 +1,10 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
gojson "encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"github.com/markbates/goth/providers/gitlab"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth/webauthn"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/i18n"
|
||||
@ -166,6 +168,17 @@ func processLogin(ctx echo.Context) error {
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
|
||||
// handle MFA
|
||||
var hasMFA bool
|
||||
if hasMFA, err = user.HasMFA(); err != nil {
|
||||
return errorRes(500, "Cannot check for user MFA", err)
|
||||
}
|
||||
if hasMFA {
|
||||
sess.Values["mfaID"] = user.ID
|
||||
saveSession(sess, ctx)
|
||||
return redirect(ctx, "/mfa")
|
||||
}
|
||||
|
||||
sess.Values["user"] = user.ID
|
||||
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
|
||||
saveSession(sess, ctx)
|
||||
@ -174,6 +187,10 @@ func processLogin(ctx echo.Context) error {
|
||||
return redirect(ctx, "/")
|
||||
}
|
||||
|
||||
func mfa(ctx echo.Context) error {
|
||||
return html(ctx, "mfa.html")
|
||||
}
|
||||
|
||||
func oauthCallback(ctx echo.Context) error {
|
||||
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
||||
if err != nil {
|
||||
@ -376,6 +393,147 @@ func oauthUnlink(ctx echo.Context) error {
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
func beginWebAuthnBinding(ctx echo.Context) error {
|
||||
credsCreation, jsonWaSession, err := webauthn.BeginBinding(getUserLogged(ctx))
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot begin WebAuthn registration", err)
|
||||
}
|
||||
|
||||
sess := getSession(ctx)
|
||||
sess.Values["webauthn_registration_session"] = jsonWaSession
|
||||
sess.Options.MaxAge = 5 * 60 // 5 minutes
|
||||
saveSession(sess, ctx)
|
||||
|
||||
return ctx.JSON(200, credsCreation)
|
||||
}
|
||||
|
||||
func finishWebAuthnBinding(ctx echo.Context) error {
|
||||
sess := getSession(ctx)
|
||||
jsonWaSession, ok := sess.Values["webauthn_registration_session"].([]byte)
|
||||
if !ok {
|
||||
return jsonErrorRes(401, "Cannot get WebAuthn registration session", nil)
|
||||
}
|
||||
|
||||
user := getUserLogged(ctx)
|
||||
|
||||
// extract passkey name from request
|
||||
body, err := io.ReadAll(ctx.Request().Body)
|
||||
if err != nil {
|
||||
return jsonErrorRes(400, "Failed to read request body", err)
|
||||
}
|
||||
ctx.Request().Body.Close()
|
||||
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
dto := new(db.CrendentialDTO)
|
||||
_ = gojson.Unmarshal(body, &dto)
|
||||
|
||||
if err = ctx.Validate(dto); err != nil {
|
||||
return jsonErrorRes(400, "Invalid request", err)
|
||||
}
|
||||
passkeyName := dto.PasskeyName
|
||||
if passkeyName == "" {
|
||||
passkeyName = "WebAuthn"
|
||||
}
|
||||
|
||||
waCredential, err := webauthn.FinishBinding(user, jsonWaSession, ctx.Request())
|
||||
if err != nil {
|
||||
return jsonErrorRes(403, "Failed binding attempt for passkey", err)
|
||||
}
|
||||
|
||||
if _, err = db.CreateFromCrendential(user.ID, passkeyName, waCredential); err != nil {
|
||||
return jsonErrorRes(500, "Cannot create WebAuthn credential on database", err)
|
||||
}
|
||||
|
||||
delete(sess.Values, "webauthn_registration_session")
|
||||
saveSession(sess, ctx)
|
||||
|
||||
addFlash(ctx, tr(ctx, "flash.auth.passkey-registred", passkeyName), "success")
|
||||
return json(ctx, 200, []string{"OK"})
|
||||
}
|
||||
|
||||
func beginWebAuthnLogin(ctx echo.Context) error {
|
||||
credsCreation, jsonWaSession, err := webauthn.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
return jsonErrorRes(401, "Cannot begin WebAuthn login", err)
|
||||
}
|
||||
|
||||
sess := getSession(ctx)
|
||||
sess.Values["webauthn_login_session"] = jsonWaSession
|
||||
sess.Options.MaxAge = 5 * 60 // 5 minutes
|
||||
saveSession(sess, ctx)
|
||||
|
||||
return json(ctx, 200, credsCreation)
|
||||
}
|
||||
|
||||
func finishWebAuthnLogin(ctx echo.Context) error {
|
||||
sess := getSession(ctx)
|
||||
sessionData, ok := sess.Values["webauthn_login_session"].([]byte)
|
||||
if !ok {
|
||||
return jsonErrorRes(401, "Cannot get WebAuthn login session", nil)
|
||||
}
|
||||
|
||||
userID, err := webauthn.FinishDiscoverableLogin(sessionData, ctx.Request())
|
||||
if err != nil {
|
||||
return jsonErrorRes(403, "Failed authentication attempt for passkey", err)
|
||||
}
|
||||
|
||||
sess.Values["user"] = userID
|
||||
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
|
||||
|
||||
delete(sess.Values, "webauthn_login_session")
|
||||
saveSession(sess, ctx)
|
||||
|
||||
return json(ctx, 200, []string{"OK"})
|
||||
}
|
||||
|
||||
func beginWebAuthnAssertion(ctx echo.Context) error {
|
||||
sess := getSession(ctx)
|
||||
|
||||
ogUser, err := db.GetUserById(sess.Values["mfaID"].(uint))
|
||||
if err != nil {
|
||||
return jsonErrorRes(500, "Cannot get user", err)
|
||||
}
|
||||
|
||||
credsCreation, jsonWaSession, err := webauthn.BeginLogin(ogUser)
|
||||
if err != nil {
|
||||
return jsonErrorRes(401, "Cannot begin WebAuthn login", err)
|
||||
}
|
||||
|
||||
sess.Values["webauthn_assertion_session"] = jsonWaSession
|
||||
sess.Options.MaxAge = 5 * 60 // 5 minutes
|
||||
saveSession(sess, ctx)
|
||||
|
||||
return json(ctx, 200, credsCreation)
|
||||
}
|
||||
|
||||
func finishWebAuthnAssertion(ctx echo.Context) error {
|
||||
sess := getSession(ctx)
|
||||
sessionData, ok := sess.Values["webauthn_assertion_session"].([]byte)
|
||||
if !ok {
|
||||
return jsonErrorRes(401, "Cannot get WebAuthn assertion session", nil)
|
||||
}
|
||||
|
||||
userId := sess.Values["mfaID"].(uint)
|
||||
|
||||
ogUser, err := db.GetUserById(userId)
|
||||
if err != nil {
|
||||
return jsonErrorRes(500, "Cannot get user", err)
|
||||
}
|
||||
|
||||
if err = webauthn.FinishLogin(ogUser, sessionData, ctx.Request()); err != nil {
|
||||
return jsonErrorRes(403, "Failed authentication attempt for passkey", err)
|
||||
}
|
||||
|
||||
sess.Values["user"] = userId
|
||||
sess.Options.MaxAge = 60 * 60 * 24 * 365 // 1 year
|
||||
|
||||
delete(sess.Values, "webauthn_assertion_session")
|
||||
delete(sess.Values, "mfaID")
|
||||
saveSession(sess, ctx)
|
||||
|
||||
return json(ctx, 200, []string{"OK"})
|
||||
}
|
||||
|
||||
func logout(ctx echo.Context) error {
|
||||
deleteSession(ctx)
|
||||
deleteCsrfCookie(ctx)
|
||||
@ -427,7 +585,7 @@ func getAvatarUrlFromProvider(provider string, identifier string) string {
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
err = gojson.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
|
||||
return ""
|
||||
|
Reference in New Issue
Block a user