mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-15 07:11:15 +00:00
Merge branch 'passkey_support'
This commit is contained in:
@@ -129,6 +129,152 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/webauthn/credential/{id}": {
|
||||
"put": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Update a WebAuthn credential.",
|
||||
"operationId": "auth_handleWebAuthnCredentialsPut",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Base64 encoded Credential ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Credential name",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.WebAuthnCredentialRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Delete a WebAuthn credential.",
|
||||
"operationId": "auth_handleWebAuthnCredentialsDelete",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Base64 encoded Credential ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/webauthn/credentials": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Get all available external login providers.",
|
||||
"operationId": "auth_handleWebAuthnCredentialsGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/webauthn/login/finish": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Finish the WebAuthn login process.",
|
||||
"operationId": "auth_handleWebAuthnLoginFinish",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/webauthn/register/finish": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Finish the WebAuthn registration process.",
|
||||
"operationId": "auth_handleWebAuthnRegisterFinish",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "\"\"",
|
||||
"description": "Credential name",
|
||||
"name": "credential_name",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/{provider}/callback": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -2093,6 +2239,9 @@
|
||||
},
|
||||
"SelfProvisioning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"WebAuthnEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2161,6 +2310,28 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.WebAuthnCredentialRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.WebAuthnCredentialResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"CreatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"ID": {
|
||||
"type": "string"
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -387,6 +387,8 @@ definitions:
|
||||
type: boolean
|
||||
SelfProvisioning:
|
||||
type: boolean
|
||||
WebAuthnEnabled:
|
||||
type: boolean
|
||||
type: object
|
||||
model.User:
|
||||
properties:
|
||||
@@ -433,6 +435,20 @@ definitions:
|
||||
Source:
|
||||
type: string
|
||||
type: object
|
||||
model.WebAuthnCredentialRequest:
|
||||
properties:
|
||||
Name:
|
||||
type: string
|
||||
type: object
|
||||
model.WebAuthnCredentialResponse:
|
||||
properties:
|
||||
CreatedAt:
|
||||
type: string
|
||||
ID:
|
||||
type: string
|
||||
Name:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact:
|
||||
name: WireGuard Portal Developers
|
||||
@@ -548,6 +564,102 @@ paths:
|
||||
summary: Get information about the currently logged-in user.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/webauthn/credential/{id}:
|
||||
delete:
|
||||
operationId: auth_handleWebAuthnCredentialsDelete
|
||||
parameters:
|
||||
- description: Base64 encoded Credential ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||
type: array
|
||||
summary: Delete a WebAuthn credential.
|
||||
tags:
|
||||
- Authentication
|
||||
put:
|
||||
operationId: auth_handleWebAuthnCredentialsPut
|
||||
parameters:
|
||||
- description: Base64 encoded Credential ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Credential name
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.WebAuthnCredentialRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||
type: array
|
||||
summary: Update a WebAuthn credential.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/webauthn/credentials:
|
||||
get:
|
||||
operationId: auth_handleWebAuthnCredentialsGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/webauthn/login/finish:
|
||||
post:
|
||||
operationId: auth_handleWebAuthnLoginFinish
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/model.User'
|
||||
summary: Finish the WebAuthn login process.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/webauthn/register/finish:
|
||||
post:
|
||||
operationId: auth_handleWebAuthnRegisterFinish
|
||||
parameters:
|
||||
- default: '""'
|
||||
description: Credential name
|
||||
in: query
|
||||
name: credential_name
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||
type: array
|
||||
summary: Finish the WebAuthn registration process.
|
||||
tags:
|
||||
- Authentication
|
||||
/config/frontend.js:
|
||||
get:
|
||||
operationId: config_handleConfigJsGet
|
||||
|
@@ -99,6 +99,8 @@ type Authenticator interface {
|
||||
LoggedIn(scopes ...Scope) func(next http.Handler) http.Handler
|
||||
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
|
||||
UserIdMatch(idParameter string) func(next http.Handler) http.Handler
|
||||
// InfoOnly only add user info to the request context. No login check is performed.
|
||||
InfoOnly() func(next http.Handler) http.Handler
|
||||
}
|
||||
|
||||
type Session interface {
|
||||
|
@@ -28,12 +28,54 @@ type AuthenticationService interface {
|
||||
OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error)
|
||||
}
|
||||
|
||||
type WebAuthnService interface {
|
||||
Enabled() bool
|
||||
StartWebAuthnRegistration(ctx context.Context, userId domain.UserIdentifier) (
|
||||
responseOptions []byte,
|
||||
sessionData []byte,
|
||||
err error,
|
||||
)
|
||||
FinishWebAuthnRegistration(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
name string,
|
||||
sessionDataAsJSON []byte,
|
||||
r *http.Request,
|
||||
) ([]domain.UserWebauthnCredential, error)
|
||||
GetCredentials(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
) ([]domain.UserWebauthnCredential, error)
|
||||
RemoveCredential(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
credentialIdBase64 string,
|
||||
) ([]domain.UserWebauthnCredential, error)
|
||||
UpdateCredential(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
credentialIdBase64 string,
|
||||
name string,
|
||||
) ([]domain.UserWebauthnCredential, error)
|
||||
StartWebAuthnLogin(_ context.Context) (
|
||||
optionsAsJSON []byte,
|
||||
sessionDataAsJSON []byte,
|
||||
err error,
|
||||
)
|
||||
FinishWebAuthnLogin(
|
||||
ctx context.Context,
|
||||
sessionDataAsJSON []byte,
|
||||
r *http.Request,
|
||||
) (*domain.User, error)
|
||||
}
|
||||
|
||||
type AuthEndpoint struct {
|
||||
cfg *config.Config
|
||||
authService AuthenticationService
|
||||
authenticator Authenticator
|
||||
session Session
|
||||
validate Validator
|
||||
webAuthn WebAuthnService
|
||||
}
|
||||
|
||||
func NewAuthEndpoint(
|
||||
@@ -42,6 +84,7 @@ func NewAuthEndpoint(
|
||||
session Session,
|
||||
validator Validator,
|
||||
authService AuthenticationService,
|
||||
webAuthn WebAuthnService,
|
||||
) AuthEndpoint {
|
||||
return AuthEndpoint{
|
||||
cfg: cfg,
|
||||
@@ -49,6 +92,7 @@ func NewAuthEndpoint(
|
||||
authenticator: authenticator,
|
||||
session: session,
|
||||
validate: validator,
|
||||
webAuthn: webAuthn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +109,19 @@ func (e AuthEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup.HandleFunc("GET /login/{provider}/init", e.handleOauthInitiateGet())
|
||||
apiGroup.HandleFunc("GET /login/{provider}/callback", e.handleOauthCallbackGet())
|
||||
|
||||
apiGroup.HandleFunc("POST /webauthn/login/start", e.handleWebAuthnLoginStart())
|
||||
apiGroup.HandleFunc("POST /webauthn/login/finish", e.handleWebAuthnLoginFinish())
|
||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("GET /webauthn/credentials",
|
||||
e.handleWebAuthnCredentialsGet())
|
||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("POST /webauthn/register/start",
|
||||
e.handleWebAuthnRegisterStart())
|
||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("POST /webauthn/register/finish",
|
||||
e.handleWebAuthnRegisterFinish())
|
||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("DELETE /webauthn/credential/{id}",
|
||||
e.handleWebAuthnCredentialsDelete())
|
||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("PUT /webauthn/credential/{id}",
|
||||
e.handleWebAuthnCredentialsPut())
|
||||
|
||||
apiGroup.HandleFunc("POST /login", e.handleLoginPost())
|
||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("POST /logout", e.handleLogoutPost())
|
||||
}
|
||||
@@ -389,3 +446,237 @@ func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// handleWebAuthnCredentialsGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID auth_handleWebAuthnCredentialsGet
|
||||
// @Tags Authentication
|
||||
// @Summary Get all available external login providers.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||
// @Router /auth/webauthn/credentials [get]
|
||||
func (e AuthEndpoint) handleWebAuthnCredentialsGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.webAuthn.Enabled() {
|
||||
respond.JSON(w, http.StatusOK, []model.WebAuthnCredentialResponse{})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||
|
||||
credentials, err := e.webAuthn.GetCredentials(r.Context(), userIdentifier)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebAuthnCredentialsDelete returns a gorm Handler function.
|
||||
//
|
||||
// @ID auth_handleWebAuthnCredentialsDelete
|
||||
// @Tags Authentication
|
||||
// @Summary Delete a WebAuthn credential.
|
||||
// @Param id path string true "Base64 encoded Credential ID"
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||
// @Router /auth/webauthn/credential/{id} [delete]
|
||||
func (e AuthEndpoint) handleWebAuthnCredentialsDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.webAuthn.Enabled() {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||
|
||||
credentialId := Base64UrlDecode(request.Path(r, "id"))
|
||||
|
||||
credentials, err := e.webAuthn.RemoveCredential(r.Context(), userIdentifier, credentialId)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebAuthnCredentialsPut returns a gorm Handler function.
|
||||
//
|
||||
// @ID auth_handleWebAuthnCredentialsPut
|
||||
// @Tags Authentication
|
||||
// @Summary Update a WebAuthn credential.
|
||||
// @Param id path string true "Base64 encoded Credential ID"
|
||||
// @Param request body model.WebAuthnCredentialRequest true "Credential name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||
// @Router /auth/webauthn/credential/{id} [put]
|
||||
func (e AuthEndpoint) handleWebAuthnCredentialsPut() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.webAuthn.Enabled() {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||
|
||||
credentialId := Base64UrlDecode(request.Path(r, "id"))
|
||||
var req model.WebAuthnCredentialRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
credentials, err := e.webAuthn.UpdateCredential(r.Context(), userIdentifier, credentialId, req.Name)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||
}
|
||||
}
|
||||
|
||||
func (e AuthEndpoint) handleWebAuthnRegisterStart() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.webAuthn.Enabled() {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||
|
||||
options, sessionData, err := e.webAuthn.StartWebAuthnRegistration(r.Context(), userIdentifier)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession.WebAuthnData = string(sessionData)
|
||||
e.session.SetData(r.Context(), currentSession)
|
||||
|
||||
respond.Data(w, http.StatusOK, "application/json", options)
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebAuthnRegisterFinish returns a gorm Handler function.
|
||||
//
|
||||
// @ID auth_handleWebAuthnRegisterFinish
|
||||
// @Tags Authentication
|
||||
// @Summary Finish the WebAuthn registration process.
|
||||
// @Param credential_name query string false "Credential name" default("")
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||
// @Router /auth/webauthn/register/finish [post]
|
||||
func (e AuthEndpoint) handleWebAuthnRegisterFinish() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.webAuthn.Enabled() {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
name := request.QueryDefault(r, "credential_name", "")
|
||||
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
webAuthnSessionData := []byte(currentSession.WebAuthnData)
|
||||
currentSession.WebAuthnData = "" // clear the session data
|
||||
e.session.SetData(r.Context(), currentSession)
|
||||
|
||||
credentials, err := e.webAuthn.FinishWebAuthnRegistration(
|
||||
r.Context(),
|
||||
domain.UserIdentifier(currentSession.UserIdentifier),
|
||||
name,
|
||||
webAuthnSessionData,
|
||||
r)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||
}
|
||||
}
|
||||
|
||||
func (e AuthEndpoint) handleWebAuthnLoginStart() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.webAuthn.Enabled() {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
options, sessionData, err := e.webAuthn.StartWebAuthnLogin(r.Context())
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession.WebAuthnData = string(sessionData)
|
||||
e.session.SetData(r.Context(), currentSession)
|
||||
|
||||
respond.Data(w, http.StatusOK, "application/json", options)
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebAuthnLoginFinish returns a gorm Handler function.
|
||||
//
|
||||
// @ID auth_handleWebAuthnLoginFinish
|
||||
// @Tags Authentication
|
||||
// @Summary Finish the WebAuthn login process.
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.User
|
||||
// @Router /auth/webauthn/login/finish [post]
|
||||
func (e AuthEndpoint) handleWebAuthnLoginFinish() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.webAuthn.Enabled() {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
currentSession := e.session.GetData(r.Context())
|
||||
|
||||
webAuthnSessionData := []byte(currentSession.WebAuthnData)
|
||||
currentSession.WebAuthnData = "" // clear the session data
|
||||
e.session.SetData(r.Context(), currentSession)
|
||||
|
||||
user, err := e.webAuthn.FinishWebAuthnLogin(
|
||||
r.Context(),
|
||||
webAuthnSessionData,
|
||||
r)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
e.setAuthenticatedUser(r, user)
|
||||
|
||||
respond.JSON(w, http.StatusOK, model.NewUser(user, false))
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/h44z/wg-portal/internal/app/api/core/respond"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
//go:embed frontend_config.js.gotpl
|
||||
@@ -46,7 +47,7 @@ func (e ConfigEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup := g.Mount("/config")
|
||||
|
||||
apiGroup.HandleFunc("GET /frontend.js", e.handleConfigJsGet())
|
||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("GET /settings", e.handleSettingsGet())
|
||||
apiGroup.With(e.authenticator.InfoOnly()).HandleFunc("GET /settings", e.handleSettingsGet())
|
||||
}
|
||||
|
||||
// handleConfigJsGet returns a gorm Handler function.
|
||||
@@ -93,11 +94,22 @@ func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||
// @Router /config/settings [get]
|
||||
func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.JSON(w, http.StatusOK, model.Settings{
|
||||
MailLinkOnly: e.cfg.Mail.LinkOnly,
|
||||
PersistentConfigSupported: e.cfg.Advanced.ConfigStoragePath != "",
|
||||
SelfProvisioning: e.cfg.Core.SelfProvisioningAllowed,
|
||||
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
|
||||
})
|
||||
sessionUser := domain.GetUserInfo(r.Context())
|
||||
|
||||
// For anonymous users, we return the settings object with minimal information
|
||||
if sessionUser.Id == domain.CtxUnknownUserId || sessionUser.Id == "" {
|
||||
respond.JSON(w, http.StatusOK, model.Settings{
|
||||
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||
})
|
||||
} else {
|
||||
respond.JSON(w, http.StatusOK, model.Settings{
|
||||
MailLinkOnly: e.cfg.Mail.LinkOnly,
|
||||
PersistentConfigSupported: e.cfg.Advanced.ConfigStoragePath != "",
|
||||
SelfProvisioning: e.cfg.Core.SelfProvisioningAllowed,
|
||||
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
|
||||
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -72,6 +72,32 @@ func (h AuthenticationHandler) LoggedIn(scopes ...Scope) func(next http.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
// InfoOnly only checks if the user is logged in and adds the user id to the context.
|
||||
// If the user is not logged in, the context user id is set to domain.CtxUnknownUserId.
|
||||
func (h AuthenticationHandler) InfoOnly() func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session := h.session.GetData(r.Context())
|
||||
|
||||
var newContext context.Context
|
||||
|
||||
if !session.LoggedIn {
|
||||
newContext = domain.SetUserInfo(r.Context(), domain.DefaultContextUserInfo())
|
||||
} else {
|
||||
newContext = domain.SetUserInfo(r.Context(), &domain.ContextUserInfo{
|
||||
Id: domain.UserIdentifier(session.UserIdentifier),
|
||||
IsAdmin: session.IsAdmin,
|
||||
})
|
||||
}
|
||||
|
||||
r = r.WithContext(newContext)
|
||||
|
||||
// Continue down the chain to Handler etc
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
|
||||
func (h AuthenticationHandler) UserIdMatch(idParameter string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
|
@@ -31,6 +31,8 @@ type SessionData struct {
|
||||
OauthProvider string
|
||||
OauthReturnTo string
|
||||
|
||||
WebAuthnData string
|
||||
|
||||
CsrfToken string
|
||||
}
|
||||
|
||||
|
@@ -10,4 +10,6 @@ type Settings struct {
|
||||
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
||||
SelfProvisioning bool `json:"SelfProvisioning"`
|
||||
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
||||
WebAuthnEnabled bool `json:"WebAuthnEnabled"`
|
||||
MinPasswordLength int `json:"MinPasswordLength"`
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
package model
|
||||
|
||||
import "github.com/h44z/wg-portal/internal/domain"
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type LoginProviderInfo struct {
|
||||
Identifier string `json:"Identifier" example:"google"`
|
||||
@@ -39,3 +44,32 @@ type OauthInitiationResponse struct {
|
||||
RedirectUrl string
|
||||
State string
|
||||
}
|
||||
|
||||
type WebAuthnCredentialRequest struct {
|
||||
Name string `json:"Name"`
|
||||
}
|
||||
type WebAuthnCredentialResponse struct {
|
||||
ID string `json:"ID"`
|
||||
Name string `json:"Name"`
|
||||
CreatedAt string `json:"CreatedAt"`
|
||||
}
|
||||
|
||||
func NewWebAuthnCredentialResponse(src domain.UserWebauthnCredential) WebAuthnCredentialResponse {
|
||||
return WebAuthnCredentialResponse{
|
||||
ID: src.CredentialIdentifier,
|
||||
Name: src.DisplayName,
|
||||
CreatedAt: src.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
func NewWebAuthnCredentialResponses(src []domain.UserWebauthnCredential) []WebAuthnCredentialResponse {
|
||||
credentials := make([]WebAuthnCredentialResponse, len(src))
|
||||
for i := range src {
|
||||
credentials[i] = NewWebAuthnCredentialResponse(src[i])
|
||||
}
|
||||
// Sort by CreatedAt, newest first
|
||||
slices.SortFunc(credentials, func(i, j WebAuthnCredentialResponse) int {
|
||||
return strings.Compare(i.CreatedAt, j.CreatedAt)
|
||||
})
|
||||
return credentials
|
||||
}
|
||||
|
301
internal/app/auth/webauthn.go
Normal file
301
internal/app/auth/webauthn.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/audit"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type WebAuthnUserManager interface {
|
||||
// GetUser returns a user by its identifier.
|
||||
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||
// GetUserByWebAuthnCredential returns a user by its WebAuthn ID.
|
||||
GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error)
|
||||
// UpdateUser updates an existing user in the database.
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
}
|
||||
|
||||
type WebAuthnAuthenticator struct {
|
||||
webAuthn *webauthn.WebAuthn
|
||||
users WebAuthnUserManager
|
||||
bus EventBus
|
||||
}
|
||||
|
||||
func NewWebAuthnAuthenticator(cfg *config.Config, bus EventBus, users WebAuthnUserManager) (
|
||||
*WebAuthnAuthenticator,
|
||||
error,
|
||||
) {
|
||||
if !cfg.Auth.WebAuthn.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
extUrl, err := url.Parse(cfg.Web.ExternalUrl)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse external URL - required for WebAuthn RP ID")
|
||||
}
|
||||
|
||||
rpId := extUrl.Hostname()
|
||||
if rpId == "" {
|
||||
return nil, errors.New("failed to determine Webauthn RPID")
|
||||
}
|
||||
|
||||
// Initialize the WebAuthn authenticator with the provided configuration
|
||||
awCfg := &webauthn.Config{
|
||||
RPID: rpId,
|
||||
RPDisplayName: cfg.Web.SiteTitle,
|
||||
RPOrigins: []string{cfg.Web.ExternalUrl},
|
||||
}
|
||||
|
||||
webAuthn, err := webauthn.New(awCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Webauthn instance: %w", err)
|
||||
}
|
||||
|
||||
return &WebAuthnAuthenticator{
|
||||
webAuthn: webAuthn,
|
||||
users: users,
|
||||
bus: bus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) Enabled() bool {
|
||||
return a != nil && a.webAuthn != nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) StartWebAuthnRegistration(ctx context.Context, userId domain.UserIdentifier) (
|
||||
optionsAsJSON []byte,
|
||||
sessionDataAsJSON []byte,
|
||||
err error,
|
||||
) {
|
||||
user, err := a.users.GetUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
if user.IsLocked() || user.IsDisabled() {
|
||||
return nil, nil, errors.New("user is locked") // adding passkey to locked user is not allowed
|
||||
}
|
||||
|
||||
if user.WebAuthnId == "" {
|
||||
user.GenerateWebAuthnId()
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to store webauthn id to user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
options, sessionData, err := a.webAuthn.BeginRegistration(user,
|
||||
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err)
|
||||
}
|
||||
|
||||
optionsAsJSON, err = json.Marshal(options)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal webauthn options to JSON: %w", err)
|
||||
}
|
||||
sessionDataAsJSON, err = json.Marshal(sessionData)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal webauthn session data to JSON: %w", err)
|
||||
}
|
||||
|
||||
return optionsAsJSON, sessionDataAsJSON, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) FinishWebAuthnRegistration(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
name string,
|
||||
sessionDataAsJSON []byte,
|
||||
r *http.Request,
|
||||
) ([]domain.UserWebauthnCredential, error) {
|
||||
user, err := a.users.GetUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
if user.IsLocked() || user.IsDisabled() {
|
||||
return nil, errors.New("user is locked") // adding passkey to locked user is not allowed
|
||||
}
|
||||
|
||||
var webAuthnData webauthn.SessionData
|
||||
err = json.Unmarshal(sessionDataAsJSON, &webAuthnData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal webauthn session data: %w", err)
|
||||
}
|
||||
|
||||
credential, err := a.webAuthn.FinishRegistration(user, webAuthnData, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("Passkey %d", len(user.WebAuthnCredentialList)+1) // fallback name
|
||||
}
|
||||
|
||||
// Add the credential to the user
|
||||
err = user.AddCredential(userId, name, *credential)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.WebAuthnCredentialList, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) GetCredentials(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
) ([]domain.UserWebauthnCredential, error) {
|
||||
user, err := a.users.GetUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
return user.WebAuthnCredentialList, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) RemoveCredential(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
credentialIdBase64 string,
|
||||
) ([]domain.UserWebauthnCredential, error) {
|
||||
user, err := a.users.GetUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
user.RemoveCredential(credentialIdBase64)
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.WebAuthnCredentialList, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) UpdateCredential(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
credentialIdBase64 string,
|
||||
name string,
|
||||
) ([]domain.UserWebauthnCredential, error) {
|
||||
user, err := a.users.GetUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
err = user.UpdateCredential(credentialIdBase64, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.WebAuthnCredentialList, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) StartWebAuthnLogin(_ context.Context) (
|
||||
optionsAsJSON []byte,
|
||||
sessionDataAsJSON []byte,
|
||||
err error,
|
||||
) {
|
||||
options, sessionData, err := a.webAuthn.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to begin WebAuthn login: %w", err)
|
||||
}
|
||||
|
||||
optionsAsJSON, err = json.Marshal(options)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal webauthn options to JSON: %w", err)
|
||||
}
|
||||
sessionDataAsJSON, err = json.Marshal(sessionData)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal webauthn session data to JSON: %w", err)
|
||||
}
|
||||
|
||||
return optionsAsJSON, sessionDataAsJSON, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) FinishWebAuthnLogin(
|
||||
ctx context.Context,
|
||||
sessionDataAsJSON []byte,
|
||||
r *http.Request,
|
||||
) (*domain.User, error) {
|
||||
|
||||
var webAuthnData webauthn.SessionData
|
||||
err := json.Unmarshal(sessionDataAsJSON, &webAuthnData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal webauthn session data: %w", err)
|
||||
}
|
||||
|
||||
// switch to admin context for user lookup
|
||||
ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo())
|
||||
|
||||
credential, err := a.webAuthn.FinishDiscoverableLogin(a.findUserForWebAuthnSecretFn(ctx), webAuthnData, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find the user by the WebAuthn ID
|
||||
user, err := a.users.GetUserByWebAuthnCredential(ctx,
|
||||
base64.StdEncoding.EncodeToString(credential.ID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user by webauthn credential: %w", err)
|
||||
}
|
||||
|
||||
if user.IsLocked() || user.IsDisabled() {
|
||||
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
|
||||
Ctx: ctx,
|
||||
Source: "passkey",
|
||||
Event: audit.AuthEvent{
|
||||
Username: string(user.Identifier), Error: "User is locked",
|
||||
},
|
||||
})
|
||||
return nil, errors.New("user is locked") // login with passkey is not allowed
|
||||
}
|
||||
|
||||
a.bus.Publish(app.TopicAuthLogin, user.Identifier)
|
||||
a.bus.Publish(app.TopicAuditLoginSuccess, domain.AuditEventWrapper[audit.AuthEvent]{
|
||||
Ctx: ctx,
|
||||
Source: "passkey",
|
||||
Event: audit.AuthEvent{
|
||||
Username: string(user.Identifier),
|
||||
},
|
||||
})
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *WebAuthnAuthenticator) findUserForWebAuthnSecretFn(ctx context.Context) func(rawID, userHandle []byte) (
|
||||
user webauthn.User,
|
||||
err error,
|
||||
) {
|
||||
return func(rawID, userHandle []byte) (webauthn.User, error) {
|
||||
// Find the user by the WebAuthn ID
|
||||
user, err := a.users.GetUserByWebAuthnCredential(ctx, base64.StdEncoding.EncodeToString(rawID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user by webauthn credential: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
}
|
@@ -25,6 +25,8 @@ type UserDatabaseRepo interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
// GetUserByEmail returns the user with the given email address.
|
||||
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
// GetUserByWebAuthnCredential returns the user for the given WebAuthn credential ID.
|
||||
GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error)
|
||||
// GetAllUsers returns all users.
|
||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
// FindUsers returns all users matching the search string.
|
||||
@@ -129,6 +131,25 @@ func (m Manager) GetUserByEmail(ctx context.Context, email string) (*domain.User
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByWebAuthnCredential returns the user for the given WebAuthn credential.
|
||||
func (m Manager) GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error) {
|
||||
|
||||
user, err := m.users.GetUserByWebAuthnCredential(ctx, credentialIdBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load user for webauthn credential %s: %w", credentialIdBase64, err)
|
||||
}
|
||||
|
||||
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peers, _ := m.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case
|
||||
|
||||
user.LinkedPeerCount = len(peers)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetAllUsers returns all users.
|
||||
func (m Manager) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
@@ -343,6 +364,10 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
|
||||
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if err := new.HasWeakPassword(m.cfg.Auth.MinPasswordLength); err != nil {
|
||||
return errors.Join(fmt.Errorf("password too weak: %w", err), domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
|
||||
return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
|
||||
}
|
||||
@@ -397,7 +422,11 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
|
||||
|
||||
// database users must have a password
|
||||
if new.Source == domain.UserSourceDatabase && string(new.Password) == "" {
|
||||
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
|
||||
return fmt.Errorf("missing password: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if err := new.HasWeakPassword(m.cfg.Auth.MinPasswordLength); err != nil {
|
||||
return errors.Join(fmt.Errorf("password too weak: %w", err), domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
Reference in New Issue
Block a user