mirror of https://github.com/h44z/wg-portal.git
				
				
				
			
		
			
				
	
	
		
			692 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			692 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
| package handlers
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"log/slog"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/go-pkgz/routegroup"
 | |
| 
 | |
| 	"github.com/h44z/wg-portal/internal/app/api/core/request"
 | |
| 	"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"
 | |
| )
 | |
| 
 | |
| type AuthenticationService interface {
 | |
| 	// GetExternalLoginProviders returns a list of all available external login providers.
 | |
| 	GetExternalLoginProviders(_ context.Context) []domain.LoginProviderInfo
 | |
| 	// PlainLogin authenticates a user with a username and password.
 | |
| 	PlainLogin(ctx context.Context, username, password string) (*domain.User, error)
 | |
| 	// OauthLoginStep1 initiates the OAuth login flow.
 | |
| 	OauthLoginStep1(_ context.Context, providerId string) (authCodeUrl, state, nonce string, err error)
 | |
| 	// OauthLoginStep2 completes the OAuth login flow and logins the user in.
 | |
| 	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(
 | |
| 	cfg *config.Config,
 | |
| 	authenticator Authenticator,
 | |
| 	session Session,
 | |
| 	validator Validator,
 | |
| 	authService AuthenticationService,
 | |
| 	webAuthn WebAuthnService,
 | |
| ) AuthEndpoint {
 | |
| 	return AuthEndpoint{
 | |
| 		cfg:           cfg,
 | |
| 		authService:   authService,
 | |
| 		authenticator: authenticator,
 | |
| 		session:       session,
 | |
| 		validate:      validator,
 | |
| 		webAuthn:      webAuthn,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (e AuthEndpoint) GetName() string {
 | |
| 	return "AuthEndpoint"
 | |
| }
 | |
| 
 | |
| func (e AuthEndpoint) RegisterRoutes(g *routegroup.Bundle) {
 | |
| 	apiGroup := g.Mount("/auth")
 | |
| 
 | |
| 	apiGroup.HandleFunc("GET /providers", e.handleExternalLoginProvidersGet())
 | |
| 	apiGroup.HandleFunc("GET /session", e.handleSessionInfoGet())
 | |
| 
 | |
| 	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())
 | |
| }
 | |
| 
 | |
| // handleExternalLoginProvidersGet returns a gorm Handler function.
 | |
| //
 | |
| // @ID auth_handleExternalLoginProvidersGet
 | |
| // @Tags Authentication
 | |
| // @Summary Get all available external login providers.
 | |
| // @Produce json
 | |
| // @Success 200 {object} []model.LoginProviderInfo
 | |
| // @Router /auth/providers [get]
 | |
| func (e AuthEndpoint) handleExternalLoginProvidersGet() http.HandlerFunc {
 | |
| 	return func(w http.ResponseWriter, r *http.Request) {
 | |
| 		providers := e.authService.GetExternalLoginProviders(r.Context())
 | |
| 
 | |
| 		respond.JSON(w, http.StatusOK, model.NewLoginProviderInfos(providers))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // handleSessionInfoGet returns a gorm Handler function.
 | |
| //
 | |
| // @ID auth_handleSessionInfoGet
 | |
| // @Tags Authentication
 | |
| // @Summary Get information about the currently logged-in user.
 | |
| // @Produce json
 | |
| // @Success 200 {object} []model.SessionInfo
 | |
| // @Failure 500 {object} model.Error
 | |
| // @Router /auth/session [get]
 | |
| func (e AuthEndpoint) handleSessionInfoGet() http.HandlerFunc {
 | |
| 	return func(w http.ResponseWriter, r *http.Request) {
 | |
| 		currentSession := e.session.GetData(r.Context())
 | |
| 
 | |
| 		var loggedInUid *string
 | |
| 		var firstname *string
 | |
| 		var lastname *string
 | |
| 		var email *string
 | |
| 
 | |
| 		if currentSession.LoggedIn {
 | |
| 			uid := currentSession.UserIdentifier
 | |
| 			f := currentSession.Firstname
 | |
| 			l := currentSession.Lastname
 | |
| 			e := currentSession.Email
 | |
| 			loggedInUid = &uid
 | |
| 			firstname = &f
 | |
| 			lastname = &l
 | |
| 			email = &e
 | |
| 		}
 | |
| 
 | |
| 		respond.JSON(w, http.StatusOK, model.SessionInfo{
 | |
| 			LoggedIn:       currentSession.LoggedIn,
 | |
| 			IsAdmin:        currentSession.IsAdmin,
 | |
| 			UserIdentifier: loggedInUid,
 | |
| 			UserFirstname:  firstname,
 | |
| 			UserLastname:   lastname,
 | |
| 			UserEmail:      email,
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // handleOauthInitiateGet returns a gorm Handler function.
 | |
| //
 | |
| // @ID auth_handleOauthInitiateGet
 | |
| // @Tags Authentication
 | |
| // @Summary Initiate the OAuth login flow.
 | |
| // @Produce json
 | |
| // @Success 200 {object} []model.LoginProviderInfo
 | |
| // @Router /auth/login/{provider}/init [get]
 | |
| func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
 | |
| 	return func(w http.ResponseWriter, r *http.Request) {
 | |
| 		currentSession := e.session.GetData(r.Context())
 | |
| 
 | |
| 		autoRedirect, _ := strconv.ParseBool(request.QueryDefault(r, "redirect", "false"))
 | |
| 		returnTo := request.Query(r, "return")
 | |
| 		provider := request.Path(r, "provider")
 | |
| 
 | |
| 		var returnUrl *url.URL
 | |
| 		var returnParams string
 | |
| 		redirectToReturn := func() {
 | |
| 			respond.Redirect(w, r, http.StatusFound, returnUrl.String()+"?"+returnParams)
 | |
| 		}
 | |
| 
 | |
| 		if returnTo != "" {
 | |
| 			if !e.isValidReturnUrl(returnTo) {
 | |
| 				respond.JSON(w, http.StatusBadRequest,
 | |
| 					model.Error{Code: http.StatusBadRequest, Message: "invalid return URL"})
 | |
| 				return
 | |
| 			}
 | |
| 			if u, err := url.Parse(returnTo); err == nil {
 | |
| 				returnUrl = u
 | |
| 			}
 | |
| 			queryParams := returnUrl.Query()
 | |
| 			queryParams.Set("wgLoginState", "err") // by default, we set the state to error
 | |
| 			returnUrl.RawQuery = ""                // remove potential query params
 | |
| 			returnParams = queryParams.Encode()
 | |
| 		}
 | |
| 
 | |
| 		if currentSession.LoggedIn {
 | |
| 			if autoRedirect && e.isValidReturnUrl(returnTo) {
 | |
| 				queryParams := returnUrl.Query()
 | |
| 				queryParams.Set("wgLoginState", "success")
 | |
| 				returnParams = queryParams.Encode()
 | |
| 				redirectToReturn()
 | |
| 			} else {
 | |
| 				respond.JSON(w, http.StatusBadRequest,
 | |
| 					model.Error{Code: http.StatusBadRequest, Message: "already logged in"})
 | |
| 			}
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
 | |
| 		if err != nil {
 | |
| 			slog.Debug("failed to create oauth auth code URL",
 | |
| 				"provider", provider, "error", err)
 | |
| 			if autoRedirect && e.isValidReturnUrl(returnTo) {
 | |
| 				redirectToReturn()
 | |
| 			} else {
 | |
| 				respond.JSON(w, http.StatusInternalServerError,
 | |
| 					model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
 | |
| 			}
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		authSession := e.session.GetData(r.Context())
 | |
| 		authSession.OauthState = state
 | |
| 		authSession.OauthNonce = nonce
 | |
| 		authSession.OauthProvider = provider
 | |
| 		authSession.OauthReturnTo = returnTo
 | |
| 		e.session.SetData(r.Context(), authSession)
 | |
| 
 | |
| 		if autoRedirect {
 | |
| 			respond.Redirect(w, r, http.StatusFound, authCodeUrl)
 | |
| 		} else {
 | |
| 			respond.JSON(w, http.StatusOK, model.OauthInitiationResponse{
 | |
| 				RedirectUrl: authCodeUrl,
 | |
| 				State:       state,
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // handleOauthCallbackGet returns a gorm Handler function.
 | |
| //
 | |
| // @ID auth_handleOauthCallbackGet
 | |
| // @Tags Authentication
 | |
| // @Summary Handle the OAuth callback.
 | |
| // @Produce json
 | |
| // @Success 200 {object} []model.LoginProviderInfo
 | |
| // @Router /auth/login/{provider}/callback [get]
 | |
| func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
 | |
| 	return func(w http.ResponseWriter, r *http.Request) {
 | |
| 		currentSession := e.session.GetData(r.Context())
 | |
| 
 | |
| 		var returnUrl *url.URL
 | |
| 		var returnParams string
 | |
| 		redirectToReturn := func() {
 | |
| 			respond.Redirect(w, r, http.StatusFound, returnUrl.String()+"?"+returnParams)
 | |
| 		}
 | |
| 
 | |
| 		if currentSession.OauthReturnTo != "" {
 | |
| 			if u, err := url.Parse(currentSession.OauthReturnTo); err == nil {
 | |
| 				returnUrl = u
 | |
| 			}
 | |
| 			queryParams := returnUrl.Query()
 | |
| 			queryParams.Set("wgLoginState", "err") // by default, we set the state to error
 | |
| 			returnUrl.RawQuery = ""                // remove potential query params
 | |
| 			returnParams = queryParams.Encode()
 | |
| 		}
 | |
| 
 | |
| 		if currentSession.LoggedIn {
 | |
| 			if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
 | |
| 				queryParams := returnUrl.Query()
 | |
| 				queryParams.Set("wgLoginState", "success")
 | |
| 				returnParams = queryParams.Encode()
 | |
| 				redirectToReturn()
 | |
| 			} else {
 | |
| 				respond.JSON(w, http.StatusBadRequest, model.Error{Message: "already logged in"})
 | |
| 			}
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		provider := request.Path(r, "provider")
 | |
| 		oauthCode := request.Query(r, "code")
 | |
| 		oauthState := request.Query(r, "state")
 | |
| 
 | |
| 		if provider != currentSession.OauthProvider {
 | |
| 			slog.Debug("invalid oauth provider in callback",
 | |
| 				"expected", currentSession.OauthProvider, "got", provider, "state", oauthState)
 | |
| 			if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
 | |
| 				redirectToReturn()
 | |
| 			} else {
 | |
| 				respond.JSON(w, http.StatusBadRequest,
 | |
| 					model.Error{Code: http.StatusBadRequest, Message: "invalid oauth provider"})
 | |
| 			}
 | |
| 			return
 | |
| 		}
 | |
| 		if oauthState != currentSession.OauthState {
 | |
| 			slog.Debug("invalid oauth state in callback",
 | |
| 				"expected", currentSession.OauthState, "got", oauthState, "provider", provider)
 | |
| 			if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
 | |
| 				redirectToReturn()
 | |
| 			} else {
 | |
| 				respond.JSON(w, http.StatusBadRequest,
 | |
| 					model.Error{Code: http.StatusBadRequest, Message: "invalid oauth state"})
 | |
| 			}
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
 | |
| 		user, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
 | |
| 			oauthCode)
 | |
| 		cancel()
 | |
| 		if err != nil {
 | |
| 			slog.Debug("failed to process oauth code",
 | |
| 				"provider", provider, "state", oauthState, "error", err)
 | |
| 			if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
 | |
| 				redirectToReturn()
 | |
| 			} else {
 | |
| 				respond.JSON(w, http.StatusUnauthorized,
 | |
| 					model.Error{Code: http.StatusUnauthorized, Message: err.Error()})
 | |
| 			}
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		e.setAuthenticatedUser(r, user)
 | |
| 
 | |
| 		if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
 | |
| 			queryParams := returnUrl.Query()
 | |
| 			queryParams.Set("wgLoginState", "success")
 | |
| 			returnParams = queryParams.Encode()
 | |
| 			redirectToReturn()
 | |
| 		} else {
 | |
| 			respond.JSON(w, http.StatusOK, user)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User) {
 | |
| 	// start a fresh session
 | |
| 	e.session.DestroyData(r.Context())
 | |
| 
 | |
| 	currentSession := e.session.GetData(r.Context())
 | |
| 
 | |
| 	currentSession.LoggedIn = true
 | |
| 	currentSession.IsAdmin = user.IsAdmin
 | |
| 	currentSession.UserIdentifier = string(user.Identifier)
 | |
| 	currentSession.Firstname = user.Firstname
 | |
| 	currentSession.Lastname = user.Lastname
 | |
| 	currentSession.Email = user.Email
 | |
| 
 | |
| 	currentSession.OauthState = ""
 | |
| 	currentSession.OauthNonce = ""
 | |
| 	currentSession.OauthProvider = ""
 | |
| 	currentSession.OauthReturnTo = ""
 | |
| 
 | |
| 	e.session.SetData(r.Context(), currentSession)
 | |
| }
 | |
| 
 | |
| // handleLoginPost returns a gorm Handler function.
 | |
| //
 | |
| // @ID auth_handleLoginPost
 | |
| // @Tags Authentication
 | |
| // @Summary Get all available external login providers.
 | |
| // @Produce json
 | |
| // @Success 200 {object} []model.LoginProviderInfo
 | |
| // @Router /auth/login [post]
 | |
| func (e AuthEndpoint) handleLoginPost() http.HandlerFunc {
 | |
| 	return func(w http.ResponseWriter, r *http.Request) {
 | |
| 		currentSession := e.session.GetData(r.Context())
 | |
| 		if currentSession.LoggedIn {
 | |
| 			respond.JSON(w, http.StatusOK, model.Error{Code: http.StatusOK, Message: "already logged in"})
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		var loginData struct {
 | |
| 			Username string `json:"username" binding:"required,min=2"`
 | |
| 			Password string `json:"password" binding:"required,min=4"`
 | |
| 		}
 | |
| 
 | |
| 		if err := request.BodyJson(r, &loginData); err != nil {
 | |
| 			respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | |
| 			return
 | |
| 		}
 | |
| 		if err := e.validate.Struct(loginData); err != nil {
 | |
| 			respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		user, err := e.authService.PlainLogin(context.Background(), loginData.Username,
 | |
| 			loginData.Password)
 | |
| 		if err != nil {
 | |
| 			respond.JSON(w, http.StatusUnauthorized,
 | |
| 				model.Error{Code: http.StatusUnauthorized, Message: "login failed"})
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		e.setAuthenticatedUser(r, user)
 | |
| 
 | |
| 		respond.JSON(w, http.StatusOK, user)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // handleLogoutPost returns a gorm Handler function.
 | |
| //
 | |
| // @ID auth_handleLogoutPost
 | |
| // @Tags Authentication
 | |
| // @Summary Get all available external login providers.
 | |
| // @Produce json
 | |
| // @Success 200 {object} model.Error
 | |
| // @Router /auth/logout [post]
 | |
| func (e AuthEndpoint) handleLogoutPost() http.HandlerFunc {
 | |
| 	return func(w http.ResponseWriter, r *http.Request) {
 | |
| 		currentSession := e.session.GetData(r.Context())
 | |
| 
 | |
| 		if !currentSession.LoggedIn { // Not logged in
 | |
| 			respond.JSON(w, http.StatusOK, model.Error{Code: http.StatusOK, Message: "not logged in"})
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		e.session.DestroyData(r.Context())
 | |
| 		respond.JSON(w, http.StatusOK, model.Error{Code: http.StatusOK, Message: "logout ok"})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // isValidReturnUrl checks if the given return URL matches the configured external URL of the application.
 | |
| func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
 | |
| 	if !strings.HasPrefix(returnUrl, e.cfg.Web.ExternalUrl) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	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))
 | |
| 	}
 | |
| }
 |