mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-31 08:11:27 +02:00
FRANCIS_HOST decides where the Francis actor runtime lives. When it is empty or set to "embedded" (the default) nothing changes: Pocket ID starts the runtime inside its own process, backed by its own database. Any other value is the address, or a comma-separated list of addresses, of a standalone Francis runtime; Pocket ID then connects to it as a remote actor host and starts no embedded runtime. Connecting to a standalone runtime also needs FRANCIS_HOST_PSK, the host bootstrap pre-shared key the runtime is configured with, and optionally FRANCIS_CA, the PEM-encoded cluster CA to pin before the first connection. Without a pinned CA Francis trusts the certificate it is served on first use, and warns about it. The actor host is now held as the topology-agnostic francis host.Host interface, since the concrete type depends on the configuration. The commands that reach the actor data through Pocket ID's own database (export, import, and one-time-access-token) fail with an explicit error when a standalone runtime owns that data instead, rather than silently operating on the wrong store.
112 lines
4.1 KiB
Go
112 lines
4.1 KiB
Go
package webauthn
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
francishost "github.com/italypaleale/francis/host"
|
|
"github.com/lestrrat-go/jwx/v3/jwt"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
|
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
)
|
|
|
|
type TokenService interface {
|
|
GenerateAccessToken(user model.User, authenticationMethod string, sessionDuration time.Duration) (string, error)
|
|
VerifyAccessToken(tokenString string) (jwt.Token, error)
|
|
GetAuthenticationMethod(token jwt.Token) (string, error)
|
|
}
|
|
|
|
type AuditLogger interface {
|
|
Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool)
|
|
CreateNewSignInWithEmail(ctx context.Context, ipAddress, userAgent, userID string, tx *gorm.DB, emailLoginNotificationEnabled bool) model.AuditLog
|
|
}
|
|
|
|
type Dependencies struct {
|
|
DB *gorm.DB
|
|
Actors francishost.Host
|
|
AppURL string
|
|
|
|
Signer TokenService
|
|
AuditLog AuditLogger
|
|
AppConfig appconfig.AppConfigResolver
|
|
|
|
// CleanupDisabled skips registering the cron jobs that delete expired rows from the database, for example in tests
|
|
CleanupDisabled bool
|
|
}
|
|
|
|
type Module struct {
|
|
service *Service
|
|
handler *handler
|
|
}
|
|
|
|
func New(deps Dependencies) (*Module, error) {
|
|
service, err := newService(deps)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Register the cleanup jobs for expired WebAuthn rows
|
|
if !deps.CleanupDisabled {
|
|
if deps.Actors == nil {
|
|
return nil, errors.New("actor host is required for the WebAuthn cleanup cron jobs")
|
|
}
|
|
|
|
jobs, err := newCleanupJobs(deps.DB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, cj := range jobs {
|
|
err = deps.Actors.RegisterBuiltInActor(cj)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error registering WebAuthn cleanup cron actor %q: %w", cj.ActorType(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &Module{
|
|
service: service,
|
|
handler: newHandler(service, deps.AppConfig),
|
|
}, nil
|
|
}
|
|
|
|
// RegisterRoutes mounts the WebAuthn registration, login and reauthentication endpoints
|
|
func (m *Module) RegisterRoutes(apiGroup *gin.RouterGroup, userAuth, browserAuth, loginRateLimit, reauthRateLimit gin.HandlerFunc) {
|
|
apiGroup.GET("/webauthn/register/start", browserAuth, httpserver.Handle(m.handler.beginRegistration))
|
|
apiGroup.POST("/webauthn/register/finish", browserAuth, httpserver.Handle(m.handler.verifyRegistration))
|
|
|
|
apiGroup.GET("/webauthn/login/start", httpserver.Handle(m.handler.beginLogin))
|
|
apiGroup.POST("/webauthn/login/finish", loginRateLimit, httpserver.Handle(m.handler.verifyLogin))
|
|
|
|
apiGroup.POST("/webauthn/logout", userAuth, httpserver.Handle(m.handler.logout))
|
|
|
|
apiGroup.POST("/webauthn/reauthenticate", browserAuth, reauthRateLimit, httpserver.Handle(m.handler.reauthenticate))
|
|
|
|
apiGroup.GET("/webauthn/credentials", userAuth, httpserver.Handle(m.handler.listCredentials))
|
|
apiGroup.PATCH("/webauthn/credentials/:id", userAuth, httpserver.Handle(m.handler.updateCredential))
|
|
apiGroup.DELETE("/webauthn/credentials/:id", userAuth, httpserver.Handle(m.handler.deleteCredential))
|
|
}
|
|
|
|
// ConsumeReauthenticationToken implements the OIDC module's ReauthenticationTokenConsumer interface
|
|
func (m *Module) ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error) {
|
|
return m.service.ConsumeReauthenticationToken(ctx, tx, token, userID)
|
|
}
|
|
|
|
// ListCredentials returns the passkeys registered for the given user
|
|
// It is consumed by the user controller for the admin "manage passkeys" view
|
|
func (m *Module) ListCredentials(ctx context.Context, userID string) ([]model.WebauthnCredential, error) {
|
|
return m.service.ListCredentials(ctx, userID)
|
|
}
|
|
|
|
// DeleteCredential removes a passkey, optionally on behalf of an admin acting for another user
|
|
// It is consumed by the user controller for the admin "manage passkeys" view
|
|
func (m *Module) DeleteCredential(ctx context.Context, userID, credentialID, ipAddress, userAgent, actorUserID string) error {
|
|
return m.service.DeleteCredential(ctx, userID, credentialID, ipAddress, userAgent, actorUserID)
|
|
}
|