mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-19 01:09:52 +00:00
198 lines
6.2 KiB
Go
198 lines
6.2 KiB
Go
package webauthn
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
|
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
|
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
|
)
|
|
|
|
type emptyOutput struct {
|
|
SetCookie []http.Cookie `header:"Set-Cookie"`
|
|
}
|
|
|
|
type bodyOutput[T any] struct {
|
|
SetCookie []http.Cookie `header:"Set-Cookie"`
|
|
Body T
|
|
}
|
|
|
|
type credentialBodyInput struct {
|
|
Body json.RawMessage
|
|
}
|
|
|
|
type optionalCredentialBodyInput struct {
|
|
Body *json.RawMessage `required:"false"`
|
|
}
|
|
|
|
type credentialIDInput struct {
|
|
ID string `path:"id"`
|
|
}
|
|
|
|
type credentialUpdateInput struct {
|
|
ID string `path:"id"`
|
|
Body dto.WebauthnCredentialUpdateDto
|
|
}
|
|
|
|
type handler struct {
|
|
service *Service
|
|
appConfig AppConfigProvider
|
|
}
|
|
|
|
func newHandler(service *Service, appConfig AppConfigProvider) *handler {
|
|
return &handler{service: service, appConfig: appConfig}
|
|
}
|
|
|
|
func (h *handler) beginRegistration(ctx context.Context, _ *httpapi.EmptyInput) (*bodyOutput[protocol.PublicKeyCredentialCreationOptions], error) {
|
|
options, err := h.service.BeginRegistration(ctx, httpapi.UserID(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &bodyOutput[protocol.PublicKeyCredentialCreationOptions]{
|
|
SetCookie: []http.Cookie{*cookie.NewSessionIDCookie(int(options.Timeout.Seconds()), options.SessionID)},
|
|
Body: options.Response,
|
|
}, nil
|
|
}
|
|
|
|
func (h *handler) verifyRegistration(ctx context.Context, input *credentialBodyInput) (*bodyOutput[dto.WebauthnCredentialDto], error) {
|
|
sessionID, err := sessionID(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request := requestWithBody(ctx, input.Body)
|
|
credential, err := h.service.VerifyRegistration(ctx, sessionID, httpapi.UserID(ctx), request, httpapi.ClientIP(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var output dto.WebauthnCredentialDto
|
|
if err := dto.MapStruct(credential, &output); err != nil {
|
|
return nil, err
|
|
}
|
|
return &bodyOutput[dto.WebauthnCredentialDto]{Body: output}, nil
|
|
}
|
|
|
|
func (h *handler) beginLogin(ctx context.Context, _ *httpapi.EmptyInput) (*bodyOutput[protocol.PublicKeyCredentialRequestOptions], error) {
|
|
options, err := h.service.BeginLogin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &bodyOutput[protocol.PublicKeyCredentialRequestOptions]{
|
|
SetCookie: []http.Cookie{*cookie.NewSessionIDCookie(int(options.Timeout.Seconds()), options.SessionID)},
|
|
Body: options.Response,
|
|
}, nil
|
|
}
|
|
|
|
func (h *handler) verifyLogin(ctx context.Context, input *credentialBodyInput) (*bodyOutput[dto.UserDto], error) {
|
|
sessionID, err := sessionID(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assertion, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(input.Body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user, token, err := h.service.VerifyLogin(ctx, sessionID, assertion, httpapi.ClientIP(ctx), httpapi.UserAgent(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var output dto.UserDto
|
|
if err := dto.MapStruct(user, &output); err != nil {
|
|
return nil, err
|
|
}
|
|
maxAge := int(h.appConfig.GetDbConfig().SessionDuration.AsDurationMinutes().Seconds())
|
|
return &bodyOutput[dto.UserDto]{SetCookie: []http.Cookie{*cookie.NewAccessTokenCookie(maxAge, token)}, Body: output}, nil
|
|
}
|
|
|
|
func (h *handler) listCredentials(ctx context.Context, _ *httpapi.EmptyInput) (*bodyOutput[[]dto.WebauthnCredentialDto], error) {
|
|
credentials, err := h.service.ListCredentials(ctx, httpapi.UserID(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var output []dto.WebauthnCredentialDto
|
|
if err := dto.MapStructList(credentials, &output); err != nil {
|
|
return nil, err
|
|
}
|
|
return &bodyOutput[[]dto.WebauthnCredentialDto]{Body: output}, nil
|
|
}
|
|
|
|
func (h *handler) deleteCredential(ctx context.Context, input *credentialIDInput) (*emptyOutput, error) {
|
|
userID := httpapi.UserID(ctx)
|
|
if err := h.service.DeleteCredential(ctx, userID, input.ID, httpapi.ClientIP(ctx), httpapi.UserAgent(ctx), userID); err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptyOutput{}, nil
|
|
}
|
|
|
|
func (h *handler) updateCredential(ctx context.Context, input *credentialUpdateInput) (*bodyOutput[dto.WebauthnCredentialDto], error) {
|
|
credential, err := h.service.UpdateCredential(ctx, httpapi.UserID(ctx), input.ID, input.Body.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var output dto.WebauthnCredentialDto
|
|
if err := dto.MapStruct(credential, &output); err != nil {
|
|
return nil, err
|
|
}
|
|
return &bodyOutput[dto.WebauthnCredentialDto]{Body: output}, nil
|
|
}
|
|
|
|
func (h *handler) logout(_ context.Context, _ *httpapi.EmptyInput) (*emptyOutput, error) {
|
|
return &emptyOutput{SetCookie: []http.Cookie{*cookie.NewAccessTokenCookie(-1, "")}}, nil
|
|
}
|
|
|
|
func (h *handler) reauthenticate(ctx context.Context, input *optionalCredentialBodyInput) (*emptyOutput, error) {
|
|
var token string
|
|
var err error
|
|
if input.Body != nil {
|
|
assertion, parseErr := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(*input.Body))
|
|
if parseErr == nil {
|
|
sessionCookieID, sessionErr := sessionID(ctx)
|
|
if sessionErr != nil {
|
|
return nil, sessionErr
|
|
}
|
|
token, err = h.service.CreateReauthenticationTokenWithWebauthn(ctx, sessionCookieID, assertion)
|
|
} else {
|
|
token, err = h.reauthenticateWithAccessToken(ctx)
|
|
}
|
|
} else {
|
|
token, err = h.reauthenticateWithAccessToken(ctx)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptyOutput{SetCookie: []http.Cookie{*cookie.NewReauthenticationTokenCookie(token)}}, nil
|
|
}
|
|
|
|
func (h *handler) reauthenticateWithAccessToken(ctx context.Context) (string, error) {
|
|
accessToken, _ := httpapi.Cookie(ctx, cookie.AccessTokenCookieName)
|
|
value := ""
|
|
if accessToken != nil {
|
|
value = accessToken.Value
|
|
}
|
|
return h.service.CreateReauthenticationTokenWithAccessToken(ctx, value)
|
|
}
|
|
|
|
func sessionID(ctx context.Context) (string, error) {
|
|
id, err := httpapi.Cookie(ctx, cookie.SessionIdCookieName)
|
|
if err != nil {
|
|
return "", &common.MissingSessionIdError{}
|
|
}
|
|
return id.Value, nil
|
|
}
|
|
|
|
func requestWithBody(ctx context.Context, body []byte) *http.Request {
|
|
request := httpapi.Request(ctx).Clone(ctx)
|
|
request.Body = http.NoBody
|
|
if len(body) > 0 {
|
|
request.Body = io.NopCloser(bytes.NewReader(body))
|
|
}
|
|
request.ContentLength = int64(len(body))
|
|
return request
|
|
}
|