diff --git a/server/api/dto/intern/webauthn_credential.go b/server/api/dto/intern/webauthn_credential.go index 11fa60d..4bbc67c 100644 --- a/server/api/dto/intern/webauthn_credential.go +++ b/server/api/dto/intern/webauthn_credential.go @@ -10,7 +10,7 @@ import ( "time" ) -func WebauthnCredentialToModel(credential *webauthn.Credential, userId uuid.UUID, webauthnUserId uuid.UUID, backupEligible bool, backupState bool) *models.WebauthnCredential { +func WebauthnCredentialToModel(credential *webauthn.Credential, userId string, webauthnUserId uuid.UUID, backupEligible bool, backupState bool) *models.WebauthnCredential { now := time.Now().UTC() aaguid, _ := uuid.FromBytes(credential.Authenticator.AAGUID) credentialID := base64.RawURLEncoding.EncodeToString(credential.ID) diff --git a/server/api/dto/intern/webauthn_session_data.go b/server/api/dto/intern/webauthn_session_data.go index a424ecc..62e451f 100644 --- a/server/api/dto/intern/webauthn_session_data.go +++ b/server/api/dto/intern/webauthn_session_data.go @@ -7,6 +7,7 @@ import ( "github.com/gobuffalo/nulls" "github.com/gofrs/uuid" "github.com/teamhanko/passkey-server/persistence/models" + "strings" "time" ) @@ -20,8 +21,8 @@ func WebauthnSessionDataFromModel(data *models.WebauthnSessionData) *webauthn.Se allowedCredentials = append(allowedCredentials, credentialId) } var userId []byte = nil - if !data.UserId.IsNil() { - userId = data.UserId.Bytes() + if strings.TrimSpace(data.UserId) != "" { + userId = []byte(data.UserId) } return &webauthn.SessionData{ Challenge: data.Challenge, @@ -34,7 +35,6 @@ func WebauthnSessionDataFromModel(data *models.WebauthnSessionData) *webauthn.Se func WebauthnSessionDataToModel(data *webauthn.SessionData, tenantId uuid.UUID, operation models.Operation) *models.WebauthnSessionData { id, _ := uuid.NewV4() - userId, _ := uuid.FromBytes(data.UserID) now := time.Now() var allowedCredentials []models.WebauthnSessionDataAllowedCredential @@ -54,7 +54,7 @@ func WebauthnSessionDataToModel(data *webauthn.SessionData, tenantId uuid.UUID, return &models.WebauthnSessionData{ ID: id, Challenge: data.Challenge, - UserId: userId, + UserId: string(data.UserID), UserVerification: string(data.UserVerification), CreatedAt: now, UpdatedAt: now, diff --git a/server/api/dto/intern/webauthn_user.go b/server/api/dto/intern/webauthn_user.go index e03a0ae..8284f19 100644 --- a/server/api/dto/intern/webauthn_user.go +++ b/server/api/dto/intern/webauthn_user.go @@ -2,12 +2,11 @@ package intern import ( "github.com/go-webauthn/webauthn/webauthn" - "github.com/gofrs/uuid" "github.com/teamhanko/passkey-server/persistence/models" ) type WebauthnUser struct { - UserId uuid.UUID + UserId string Name string Icon string DisplayName string @@ -25,7 +24,7 @@ func NewWebauthnUser(user models.WebauthnUser) *WebauthnUser { } func (u *WebauthnUser) WebAuthnID() []byte { - return u.UserId.Bytes() + return []byte(u.UserId) } func (u *WebauthnUser) WebAuthnName() string { diff --git a/server/api/dto/request/requests.go b/server/api/dto/request/requests.go index d1524c9..84e5b94 100644 --- a/server/api/dto/request/requests.go +++ b/server/api/dto/request/requests.go @@ -9,7 +9,7 @@ type TenantDto struct { } type ListCredentialsDto struct { - UserId string `query:"user_id" validate:"required,uuid4"` + UserId string `query:"user_id" validate:"required"` } type DeleteCredentialsDto struct { @@ -22,7 +22,7 @@ type UpdateCredentialsDto struct { } type InitRegistrationDto struct { - UserId string `json:"user_id" validate:"required,uuid4"` + UserId string `json:"user_id" validate:"required"` Username string `json:"username" validate:"required"` DisplayName *string `json:"display_name"` Icon *string `json:"icon"` diff --git a/server/api/handler/credentials.go b/server/api/handler/credentials.go index 94bb6d2..ba0f160 100644 --- a/server/api/handler/credentials.go +++ b/server/api/handler/credentials.go @@ -3,10 +3,10 @@ package handler import ( "fmt" "github.com/gobuffalo/pop/v6" - "github.com/gofrs/uuid" "github.com/labstack/echo/v4" "github.com/teamhanko/passkey-server/api/dto/request" "github.com/teamhanko/passkey-server/api/dto/response" + "github.com/teamhanko/passkey-server/api/helper" "github.com/teamhanko/passkey-server/persistence" "github.com/teamhanko/passkey-server/persistence/models" "net/http" @@ -40,14 +40,14 @@ func (credHandler *credentialsHandler) List(ctx echo.Context) error { return err } - userId, err := uuid.FromString(requestDto.UserId) + h, err := helper.GetHandlerContext(ctx) if err != nil { ctx.Logger().Error(err) return err } credentialPersister := credHandler.persister.GetWebauthnCredentialPersister(nil) - credentialModels, err := credentialPersister.GetFromUser(userId) + credentialModels, err := credentialPersister.GetFromUser(requestDto.UserId, h.Tenant.ID) if err != nil { ctx.Logger().Error(err) return err @@ -83,7 +83,7 @@ func (credHandler *credentialsHandler) Update(ctx echo.Context) error { }) } - h, err := GetHandlerContext(ctx) + h, err := helper.GetHandlerContext(ctx) if err != nil { ctx.Logger().Error(err) return err @@ -98,7 +98,7 @@ func (credHandler *credentialsHandler) Update(ctx echo.Context) error { ctx.Logger().Error(err) return err } - err := h.auditLog.CreateWithConnection(tx, ctx, h.tenant, models.AuditLogWebAuthnCredentialUpdated, &credential.UserId, nil) + err := h.AuditLog.CreateWithConnection(tx, ctx, h.Tenant, models.AuditLogWebAuthnCredentialUpdated, &credential.UserId, nil) if err != nil { ctx.Logger().Error(err) return err @@ -121,7 +121,7 @@ func (credHandler *credentialsHandler) Delete(ctx echo.Context) error { return err } - h, err := GetHandlerContext(ctx) + h, err := helper.GetHandlerContext(ctx) if err != nil { ctx.Logger().Error(err) return err @@ -135,7 +135,7 @@ func (credHandler *credentialsHandler) Delete(ctx echo.Context) error { return err } - err = h.auditLog.CreateWithConnection(tx, ctx, h.tenant, models.AuditLogWebAuthnCredentialDeleted, nil, nil) + err = h.AuditLog.CreateWithConnection(tx, ctx, h.Tenant, models.AuditLogWebAuthnCredentialDeleted, nil, nil) if err != nil { ctx.Logger().Error(err) return err diff --git a/server/api/handler/login.go b/server/api/handler/login.go index e903e90..eb37ff2 100644 --- a/server/api/handler/login.go +++ b/server/api/handler/login.go @@ -11,6 +11,7 @@ import ( "github.com/labstack/echo/v4" "github.com/teamhanko/passkey-server/api/dto/intern" "github.com/teamhanko/passkey-server/api/dto/response" + "github.com/teamhanko/passkey-server/api/helper" "github.com/teamhanko/passkey-server/crypto/jwt" "github.com/teamhanko/passkey-server/persistence" "github.com/teamhanko/passkey-server/persistence/models" @@ -35,21 +36,21 @@ func NewLoginHandler(persister persistence.Persister) (WebauthnHandler, error) { } func (lh *loginHandler) Init(ctx echo.Context) error { - h, err := GetHandlerContext(ctx) + h, err := helper.GetHandlerContext(ctx) if err != nil { ctx.Logger().Error(err) return err } - options, sessionData, err := h.webauthn.BeginDiscoverableLogin( - webauthn.WithUserVerification(h.config.WebauthnConfig.UserVerification), + options, sessionData, err := h.Webauthn.BeginDiscoverableLogin( + webauthn.WithUserVerification(h.Config.WebauthnConfig.UserVerification), ) if err != nil { ctx.Logger().Error(err) return fmt.Errorf("failed to create webauthn assertion options for discoverable login: %w", err) } - err = lh.persister.GetWebauthnSessionDataPersister(nil).Create(*intern.WebauthnSessionDataToModel(sessionData, h.tenant.ID, models.WebauthnOperationAuthentication)) + err = lh.persister.GetWebauthnSessionDataPersister(nil).Create(*intern.WebauthnSessionDataToModel(sessionData, h.Tenant.ID, models.WebauthnOperationAuthentication)) if err != nil { ctx.Logger().Error(err) return fmt.Errorf("failed to store webauthn assertion session data: %w", err) @@ -71,7 +72,7 @@ func (lh *loginHandler) Finish(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, err) } - h, err := GetHandlerContext(ctx) + h, err := helper.GetHandlerContext(ctx) if err != nil { ctx.Logger().Error(err) return err @@ -82,20 +83,24 @@ func (lh *loginHandler) Finish(ctx echo.Context) error { webauthnUserPersister := lh.persister.GetWebauthnUserPersister(tx) credentialPersister := lh.persister.GetWebauthnCredentialPersister(tx) - sessionData, err := lh.getSessionDataByChallenge(parsedRequest.Response.CollectedClientData.Challenge, sessionDataPersister, h.tenant.ID) + sessionData, err := lh.getSessionDataByChallenge(parsedRequest.Response.CollectedClientData.Challenge, sessionDataPersister, h.Tenant.ID) if err != nil { ctx.Logger().Error(err) return echo.NewHTTPError(http.StatusUnauthorized, "failed to get session data").SetInternal(err) } sessionDataModel := intern.WebauthnSessionDataFromModel(sessionData) - webauthnUser, err := lh.getWebauthnUserByUserHandle(parsedRequest.Response.UserHandle, h.tenant.ID, webauthnUserPersister) + webauthnUser, err := lh.getWebauthnUserByUserHandle(parsedRequest.Response.UserHandle, h.Tenant.ID, webauthnUserPersister) if err != nil { ctx.Logger().Error(err) return echo.NewHTTPError(http.StatusUnauthorized, "failed to get user handle").SetInternal(err) } - credential, err := h.webauthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) { + // backward compatibility + userId := lh.convertUserHandle(parsedRequest.Response.UserHandle) + parsedRequest.Response.UserHandle = []byte(userId) + + credential, err := h.Webauthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) { return webauthnUser, nil }, *sessionDataModel, parsedRequest) @@ -154,10 +159,7 @@ func (lh *loginHandler) getSessionDataByChallenge(challenge string, persister pe } func (lh *loginHandler) getWebauthnUserByUserHandle(userHandle []byte, tenantId uuid.UUID, persister persisters.WebauthnUserPersister) (*intern.WebauthnUser, error) { - userId, err := uuid.FromBytes(userHandle) - if err != nil { - return nil, echo.NewHTTPError(http.StatusBadRequest, "failed to parse userHandle as uuid").SetInternal(err) - } + userId := lh.convertUserHandle(userHandle) user, err := persister.GetByUserId(userId, tenantId) if err != nil { @@ -170,3 +172,13 @@ func (lh *loginHandler) getWebauthnUserByUserHandle(userHandle []byte, tenantId return intern.NewWebauthnUser(*user), nil } + +func (lh *loginHandler) convertUserHandle(userHandle []byte) string { + userId := string(userHandle) + userUuid, err := uuid.FromBytes(userHandle) + if err == nil { + userId = userUuid.String() + } + + return userId +} diff --git a/server/api/handler/registration.go b/server/api/handler/registration.go index caf15a4..2aa05af 100644 --- a/server/api/handler/registration.go +++ b/server/api/handler/registration.go @@ -11,6 +11,7 @@ import ( "github.com/teamhanko/passkey-server/api/dto/intern" "github.com/teamhanko/passkey-server/api/dto/request" "github.com/teamhanko/passkey-server/api/dto/response" + "github.com/teamhanko/passkey-server/api/helper" "github.com/teamhanko/passkey-server/crypto/jwt" "github.com/teamhanko/passkey-server/persistence" "github.com/teamhanko/passkey-server/persistence/models" @@ -46,7 +47,7 @@ func (r *registrationHandler) Init(ctx echo.Context) error { return err } - h, err := GetHandlerContext(ctx) + h, err := helper.GetHandlerContext(ctx) if err != nil { ctx.Logger().Error(err) return err @@ -56,7 +57,7 @@ func (r *registrationHandler) Init(ctx echo.Context) error { webauthnUserPersister := r.persister.GetWebauthnUserPersister(tx) webauthnSessionPersister := r.persister.GetWebauthnSessionDataPersister(tx) - webauthnUser.Tenant = h.tenant + webauthnUser.Tenant = h.Tenant internalUserDto, _, err := r.GetWebauthnUser(webauthnUser.UserID, webauthnUser.Tenant.ID, webauthnUserPersister) if err != nil { ctx.Logger().Error(err) @@ -74,24 +75,24 @@ func (r *registrationHandler) Init(ctx echo.Context) error { } t := true - options, sessionData, err := h.webauthn.BeginRegistration( + options, sessionData, err := h.Webauthn.BeginRegistration( internalUserDto, webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ RequireResidentKey: &t, ResidentKey: protocol.ResidentKeyRequirementRequired, - UserVerification: h.config.WebauthnConfig.UserVerification, + UserVerification: h.Config.WebauthnConfig.UserVerification, }), webauthn.WithConveyancePreference(protocol.PreferNoAttestation), // don't set the excludeCredentials list, so an already registered device can be re-registered ) - err = webauthnSessionPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, h.tenant.ID, models.WebauthnOperationRegistration)) + err = webauthnSessionPersister.Create(*intern.WebauthnSessionDataToModel(sessionData, h.Tenant.ID, models.WebauthnOperationRegistration)) if err != nil { ctx.Logger().Error(err) return fmt.Errorf("failed to create session data: %w", err) } - err = h.auditLog.CreateWithConnection(tx, ctx, h.tenant, models.AuditLogWebAuthnRegistrationInitSucceeded, &webauthnUser.UserID, nil) + err = h.AuditLog.CreateWithConnection(tx, ctx, h.Tenant, models.AuditLogWebAuthnRegistrationInitSucceeded, &webauthnUser.UserID, nil) if err != nil { ctx.Logger().Error(err) return fmt.Errorf("failed to create audit log: %w", err) @@ -108,7 +109,7 @@ func (r *registrationHandler) Finish(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "unable to parse credential creation response").SetInternal(err) } - h, err := GetHandlerContext(ctx) + h, err := helper.GetHandlerContext(ctx) if err != nil { ctx.Logger().Error(err) return err @@ -118,13 +119,13 @@ func (r *registrationHandler) Finish(ctx echo.Context) error { sessionDataPersister := r.persister.GetWebauthnSessionDataPersister(tx) webauthnUserPersister := r.persister.GetWebauthnUserPersister(tx) - sessionData, err := r.getSessionByChallenge(parsedRequest.Response.CollectedClientData.Challenge, h.tenant.ID, sessionDataPersister) + sessionData, err := r.getSessionByChallenge(parsedRequest.Response.CollectedClientData.Challenge, h.Tenant.ID, sessionDataPersister) if err != nil { ctx.Logger().Error(err) return err } - webauthnUser, userModel, err := r.GetWebauthnUser(sessionData.UserId, h.tenant.ID, webauthnUserPersister) + webauthnUser, userModel, err := r.GetWebauthnUser(sessionData.UserId, h.Tenant.ID, webauthnUserPersister) if err != nil { ctx.Logger().Error(err) return err @@ -134,7 +135,7 @@ func (r *registrationHandler) Finish(ctx echo.Context) error { return echo.NewHTTPError(http.StatusNotFound, "user not found") } - credential, err := h.webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), parsedRequest) + credential, err := h.Webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), parsedRequest) if err != nil { errorMessage := "failed to validate attestation" errorStatus := http.StatusBadRequest @@ -193,7 +194,7 @@ func (r *registrationHandler) getSessionByChallenge(challenge string, tenantId u return sessionData, nil } -func (r *registrationHandler) GetWebauthnUser(userId uuid.UUID, tenantId uuid.UUID, persister persisters.WebauthnUserPersister) (*intern.WebauthnUser, *models.WebauthnUser, error) { +func (r *registrationHandler) GetWebauthnUser(userId string, tenantId uuid.UUID, persister persisters.WebauthnUserPersister) (*intern.WebauthnUser, *models.WebauthnUser, error) { user, err := persister.GetByUserId(userId, tenantId) if err != nil { return nil, nil, err diff --git a/server/api/handler/webauthn.go b/server/api/handler/webauthn.go index 36f85e5..95a5543 100644 --- a/server/api/handler/webauthn.go +++ b/server/api/handler/webauthn.go @@ -1,12 +1,9 @@ package handler import ( - "github.com/go-webauthn/webauthn/webauthn" "github.com/labstack/echo/v4" "github.com/teamhanko/passkey-server/api/dto/request" - auditlog "github.com/teamhanko/passkey-server/audit_log" "github.com/teamhanko/passkey-server/persistence" - "github.com/teamhanko/passkey-server/persistence/models" "net/http" ) @@ -15,13 +12,6 @@ type WebauthnHandler interface { Finish(ctx echo.Context) error } -type WebauthnContext struct { - tenant *models.Tenant - webauthn *webauthn.WebAuthn - config models.Config - auditLog auditlog.Logger -} - type webauthnHandler struct { persister persistence.Persister } @@ -32,33 +22,6 @@ func newWebAuthnHandler(persister persistence.Persister) (*webauthnHandler, erro }, nil } -func GetHandlerContext(ctx echo.Context) (*WebauthnContext, error) { - ctxTenant := ctx.Get("tenant") - if ctxTenant == nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, "Unable to find tenant") - } - tenant := ctxTenant.(*models.Tenant) - - ctxWebautn := ctx.Get("webauthn_client") - var webauthnClient *webauthn.WebAuthn - if ctxWebautn != nil { - webauthnClient = ctxWebautn.(*webauthn.WebAuthn) - } - - ctxAuditLog := ctx.Get("audit_logger") - var auditLogger auditlog.Logger - if ctxAuditLog != nil { - auditLogger = ctxAuditLog.(auditlog.Logger) - } - - return &WebauthnContext{ - tenant: tenant, - webauthn: webauthnClient, - config: tenant.Config, - auditLog: auditLogger, - }, nil -} - func BindAndValidateRequest[I request.CredentialRequest | request.InitRegistrationDto](ctx echo.Context) (*I, error) { var requestDto I err := ctx.Bind(&requestDto) diff --git a/server/api/helper/context_helper.go b/server/api/helper/context_helper.go new file mode 100644 index 0000000..f022672 --- /dev/null +++ b/server/api/helper/context_helper.go @@ -0,0 +1,43 @@ +package helper + +import ( + "github.com/go-webauthn/webauthn/webauthn" + "github.com/labstack/echo/v4" + auditlog "github.com/teamhanko/passkey-server/audit_log" + "github.com/teamhanko/passkey-server/persistence/models" + "net/http" +) + +type WebauthnContext struct { + Tenant *models.Tenant + Webauthn *webauthn.WebAuthn + Config models.Config + AuditLog auditlog.Logger +} + +func GetHandlerContext(ctx echo.Context) (*WebauthnContext, error) { + ctxTenant := ctx.Get("tenant") + if ctxTenant == nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, "Unable to find tenant") + } + tenant := ctxTenant.(*models.Tenant) + + ctxWebautn := ctx.Get("webauthn_client") + var webauthnClient *webauthn.WebAuthn + if ctxWebautn != nil { + webauthnClient = ctxWebautn.(*webauthn.WebAuthn) + } + + ctxAuditLog := ctx.Get("audit_logger") + var auditLogger auditlog.Logger + if ctxAuditLog != nil { + auditLogger = ctxAuditLog.(auditlog.Logger) + } + + return &WebauthnContext{ + Tenant: tenant, + Webauthn: webauthnClient, + Config: tenant.Config, + AuditLog: auditLogger, + }, nil +} diff --git a/server/audit_log/logger.go b/server/audit_log/logger.go index 102c46e..6a6fbed 100644 --- a/server/audit_log/logger.go +++ b/server/audit_log/logger.go @@ -16,8 +16,8 @@ import ( ) type Logger interface { - Create(echo.Context, *models.Tenant, models.AuditLogType, *uuid.UUID, error) error - CreateWithConnection(*pop.Connection, echo.Context, *models.Tenant, models.AuditLogType, *uuid.UUID, error) error + Create(echo.Context, *models.Tenant, models.AuditLogType, *string, error) error + CreateWithConnection(*pop.Connection, echo.Context, *models.Tenant, models.AuditLogType, *string, error) error } type logger struct { @@ -46,11 +46,11 @@ func NewLogger(persister persistence.Persister, cfg models.AuditLogConfig) Logge } } -func (l *logger) Create(context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { +func (l *logger) Create(context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *string, logError error) error { return l.CreateWithConnection(l.persister.GetConnection(), context, tenant, auditLogType, user, logError) } -func (l *logger) CreateWithConnection(tx *pop.Connection, context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { +func (l *logger) CreateWithConnection(tx *pop.Connection, context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *string, logError error) error { if l.storageEnabled { err := l.store(tx, context, tenant, auditLogType, user, logError) if err != nil { @@ -65,7 +65,7 @@ func (l *logger) CreateWithConnection(tx *pop.Connection, context echo.Context, return nil } -func (l *logger) store(tx *pop.Connection, context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) error { +func (l *logger) store(tx *pop.Connection, context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *string, logError error) error { id, err := uuid.NewV4() if err != nil { return fmt.Errorf("failed to create id: %w", err) @@ -94,7 +94,7 @@ func (l *logger) store(tx *pop.Connection, context echo.Context, tenant *models. return l.persister.GetAuditLogPersister(tx).Create(al) } -func (l *logger) logToConsole(context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *uuid.UUID, logError error) { +func (l *logger) logToConsole(context echo.Context, tenant *models.Tenant, auditLogType models.AuditLogType, user *string, logError error) { now := time.Now() loggerEvent := zeroLogger.Log(). Str("audience", "audit"). @@ -108,7 +108,7 @@ func (l *logger) logToConsole(context echo.Context, tenant *models.Tenant, audit Str("time_unix", strconv.FormatInt(now.Unix(), 10)) if user != nil { - loggerEvent.Str("user_id", user.String()) + loggerEvent.Str("user_id", *user) } loggerEvent.Send() diff --git a/server/crypto/jwt/jwt.go b/server/crypto/jwt/jwt.go index fb1b529..5a89803 100644 --- a/server/crypto/jwt/jwt.go +++ b/server/crypto/jwt/jwt.go @@ -14,7 +14,7 @@ import ( type Generator interface { Sign(jwt.Token) ([]byte, error) Verify([]byte) (jwt.Token, error) - Generate(userId uuid.UUID, crendetialId string) (string, error) + Generate(userId string, crendetialId string) (string, error) } // Generator is used to sign and verify JWTs @@ -66,11 +66,11 @@ func (g *generator) Verify(signed []byte) (jwt.Token, error) { return token, nil } -func (g *generator) Generate(userId uuid.UUID, credentialId string) (string, error) { +func (g *generator) Generate(userId string, credentialId string) (string, error) { issuedAt := time.Now() token := jwt.New() - _ = token.Set(jwt.SubjectKey, userId.String()) + _ = token.Set(jwt.SubjectKey, userId) _ = token.Set(jwt.IssuedAtKey, issuedAt) _ = token.Set(jwt.AudienceKey, []string{g.config.RelyingParty.RPId}) _ = token.Set("cred", credentialId) diff --git a/server/persistence/migrations/20231108143016_change_user_id_to_varchar.down.fizz b/server/persistence/migrations/20231108143016_change_user_id_to_varchar.down.fizz new file mode 100644 index 0000000..51a2e53 --- /dev/null +++ b/server/persistence/migrations/20231108143016_change_user_id_to_varchar.down.fizz @@ -0,0 +1,4 @@ +change_column("webauthn_users", "user_id", "uuid", {}) +change_column("webauthn_credentials", "user_id", "uuid", {}) +change_column("webauthn_session_data", "user_id", "uuid", {}) +change_column("audit_logs", "actor_user_id", "uuid", { "null": true }) diff --git a/server/persistence/migrations/20231108143016_change_user_id_to_varchar.up.fizz b/server/persistence/migrations/20231108143016_change_user_id_to_varchar.up.fizz new file mode 100644 index 0000000..ecd5668 --- /dev/null +++ b/server/persistence/migrations/20231108143016_change_user_id_to_varchar.up.fizz @@ -0,0 +1,4 @@ +change_column("webauthn_users", "user_id", "string", {}) +change_column("webauthn_credentials", "user_id", "string", {}) +change_column("webauthn_session_data", "user_id", "string", {}) +change_column("audit_logs", "actor_user_id", "string", { "null": true }) diff --git a/server/persistence/models/audit_log.go b/server/persistence/models/audit_log.go index 73675ea..cc5b33e 100644 --- a/server/persistence/models/audit_log.go +++ b/server/persistence/models/audit_log.go @@ -13,7 +13,7 @@ type AuditLog struct { MetaHttpRequestId string `db:"meta_http_request_id" json:"meta_http_request_id"` MetaSourceIp string `db:"meta_source_ip" json:"meta_source_ip"` MetaUserAgent string `db:"meta_user_agent" json:"meta_user_agent"` - ActorUserId *uuid.UUID `db:"actor_user_id" json:"actor_user_id,omitempty"` + ActorUserId *string `db:"actor_user_id" json:"actor_user_id,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` Tenant *Tenant `json:"tenant" belongs_to:"tenants"` diff --git a/server/persistence/models/webauthn_credential.go b/server/persistence/models/webauthn_credential.go index 1591ac5..26a7beb 100644 --- a/server/persistence/models/webauthn_credential.go +++ b/server/persistence/models/webauthn_credential.go @@ -12,7 +12,7 @@ import ( // WebauthnCredential is used by pop to map your webauthn_credentials database table to your go code. type WebauthnCredential struct { ID string `db:"id" json:"id"` - UserId uuid.UUID `db:"user_id" json:"-"` + UserId string `db:"user_id" json:"-"` Name *string `db:"name" json:"-"` PublicKey string `db:"public_key" json:"-"` AttestationType string `db:"attestation_type" json:"-"` @@ -35,7 +35,7 @@ type WebauthnCredentials []WebauthnCredential func (credential *WebauthnCredential) Validate(tx *pop.Connection) (*validate.Errors, error) { return validate.Validate( &validators.StringIsPresent{Name: "ID", Field: credential.ID}, - &validators.UUIDIsPresent{Name: "UserId", Field: credential.UserId}, + &validators.StringIsPresent{Name: "UserId", Field: credential.UserId}, &validators.StringIsPresent{Name: "PublicKey", Field: credential.PublicKey}, &validators.IntIsGreaterThan{Name: "SignCount", Field: credential.SignCount, Compared: -1}, &validators.TimeIsPresent{Name: "CreatedAt", Field: credential.CreatedAt}, diff --git a/server/persistence/models/webauthn_session_data.go b/server/persistence/models/webauthn_session_data.go index e80fb0d..d82bee9 100644 --- a/server/persistence/models/webauthn_session_data.go +++ b/server/persistence/models/webauthn_session_data.go @@ -20,7 +20,7 @@ var ( // WebauthnSessionData is used by pop to map your webauthn_session_data database table to your go code. type WebauthnSessionData struct { ID uuid.UUID `db:"id"` - UserId uuid.UUID `db:"user_id" json:"-"` + UserId string `db:"user_id" json:"-"` Challenge string `db:"challenge"` UserVerification string `db:"user_verification"` CreatedAt time.Time `db:"created_at"` diff --git a/server/persistence/models/webauthn_user.go b/server/persistence/models/webauthn_user.go index 009b217..851e5ca 100644 --- a/server/persistence/models/webauthn_user.go +++ b/server/persistence/models/webauthn_user.go @@ -14,7 +14,7 @@ import ( // WebauthnUser is used by pop to map your webauthn_users database table to your go code. type WebauthnUser struct { ID uuid.UUID `json:"id" db:"id"` - UserID uuid.UUID `json:"user_id" db:"user_id"` + UserID string `json:"user_id" db:"user_id"` Name string `json:"name" db:"name"` Icon string `json:"icon" db:"icon"` DisplayName string `json:"display_name" db:"display_name"` @@ -29,7 +29,7 @@ type WebauthnUser struct { type WebauthnUsers []WebauthnUser func (webauthnUser *WebauthnUser) WebAuthnID() []byte { - return webauthnUser.UserID.Bytes() + return []byte(webauthnUser.UserID) } func (webauthnUser *WebauthnUser) WebAuthnName() string { @@ -49,7 +49,7 @@ func (webauthnUser *WebauthnUser) WebAuthnIcon() string { func (webauthnUser *WebauthnUser) Validate(tx *pop.Connection) (*validate.Errors, error) { return validate.Validate( &validators.UUIDIsPresent{Name: "ID", Field: webauthnUser.ID}, - &validators.UUIDIsPresent{Name: "UserID", Field: webauthnUser.UserID}, + &validators.StringIsPresent{Name: "UserID", Field: webauthnUser.UserID}, &validators.StringIsPresent{Name: "Name", Field: webauthnUser.Name}, &validators.StringIsPresent{Name: "DisplayName", Field: webauthnUser.DisplayName}, &validators.TimeIsPresent{Name: "UpdatedAt", Field: webauthnUser.UpdatedAt}, @@ -73,16 +73,11 @@ func FromRegistrationDto(dto *request.InitRegistrationDto) (*WebauthnUser, error return nil, err } - userId, err := uuid.FromString(dto.UserId) - if err != nil { - return nil, err - } - now := time.Now() return &WebauthnUser{ ID: webauthnId, - UserID: userId, + UserID: dto.UserId, Name: dto.Username, Icon: icon, DisplayName: displayName, diff --git a/server/persistence/persister.go b/server/persistence/persister.go index 26a14b5..12ee1e1 100644 --- a/server/persistence/persister.go +++ b/server/persistence/persister.go @@ -2,8 +2,6 @@ package persistence import ( "embed" - "fmt" - "github.com/gobuffalo/pop/v6" "github.com/teamhanko/passkey-server/config" "github.com/teamhanko/passkey-server/persistence/persisters" @@ -117,7 +115,6 @@ func (p *persister) GetAuditLogPersister(tx *pop.Connection) persisters.AuditLog } func (p *persister) GetWebauthnCredentialPersister(tx *pop.Connection) persisters.WebauthnCredentialPersister { - fmt.Println("Get Database Connection") if tx == nil { return persisters.NewWebauthnCredentialPersister(p.Database) } diff --git a/server/persistence/persisters/webauthn_credential_persister.go b/server/persistence/persisters/webauthn_credential_persister.go index d64504d..d584dcf 100644 --- a/server/persistence/persisters/webauthn_credential_persister.go +++ b/server/persistence/persisters/webauthn_credential_persister.go @@ -4,9 +4,9 @@ import ( "database/sql" "errors" "fmt" + "github.com/gofrs/uuid" "github.com/gobuffalo/pop/v6" - "github.com/gofrs/uuid" "github.com/teamhanko/passkey-server/persistence/models" ) @@ -15,7 +15,7 @@ type WebauthnCredentialPersister interface { Create(credential *models.WebauthnCredential) error Update(credential *models.WebauthnCredential) error Delete(credential *models.WebauthnCredential) error - GetFromUser(uuid.UUID) ([]models.WebauthnCredential, error) + GetFromUser(string, uuid.UUID) ([]models.WebauthnCredential, error) } type webauthnCredentialPersister struct { @@ -87,9 +87,12 @@ func (w *webauthnCredentialPersister) Delete(credential *models.WebauthnCredenti return nil } -func (w *webauthnCredentialPersister) GetFromUser(userId uuid.UUID) ([]models.WebauthnCredential, error) { +func (w *webauthnCredentialPersister) GetFromUser(userId string, tenantId uuid.UUID) ([]models.WebauthnCredential, error) { var credentials []models.WebauthnCredential - err := w.database.Eager().Where("user_id = ?", &userId).Order("created_at asc").All(&credentials) + err := w.database.Eager(). + Where("webauthn_credentials.user_id = ? AND u.tenant_id = ?", &userId, tenantId). + LeftJoin("webauthn_users u", "u.id = webauthn_credentials.webauthn_user_id"). + Order("created_at asc").All(&credentials) if err != nil && errors.Is(err, sql.ErrNoRows) { return credentials, nil } diff --git a/server/persistence/persisters/webauthn_user_persister.go b/server/persistence/persisters/webauthn_user_persister.go index 6955ab6..38c20bc 100644 --- a/server/persistence/persisters/webauthn_user_persister.go +++ b/server/persistence/persisters/webauthn_user_persister.go @@ -13,7 +13,7 @@ import ( type WebauthnUserPersister interface { Create(webauthnUser *models.WebauthnUser) error Get(id uuid.UUID) (*models.WebauthnUser, error) - GetByUserId(userId uuid.UUID, tenantId uuid.UUID) (*models.WebauthnUser, error) + GetByUserId(userId string, tenantId uuid.UUID) (*models.WebauthnUser, error) Delete(webauthnUser *models.WebauthnUser) error } @@ -64,7 +64,7 @@ func (p *webauthnUserPersister) Delete(webauthnUser *models.WebauthnUser) error return nil } -func (p *webauthnUserPersister) GetByUserId(userId uuid.UUID, tenantId uuid.UUID) (*models.WebauthnUser, error) { +func (p *webauthnUserPersister) GetByUserId(userId string, tenantId uuid.UUID) (*models.WebauthnUser, error) { weauthnUser := models.WebauthnUser{} err := p.database.Eager().Where("user_id = ? AND tenant_id = ?", userId, tenantId).First(&weauthnUser) if err != nil && errors.Is(err, sql.ErrNoRows) { diff --git a/spec/passkey-server-admin.yaml b/spec/passkey-server-admin.yaml index 300f198..58ab70e 100644 --- a/spec/passkey-server-admin.yaml +++ b/spec/passkey-server-admin.yaml @@ -3,7 +3,7 @@ info: version: '1.0' title: passkey-server-admin summary: Admin API for Passkey Server - description: 'Admin API for Hanko Passkey Server. Allows creation and configuration of tenants and api keys.' + description: 'ADmin API for Hanko Passkey Server. Allows creation and configiration of tenants and api keys, ' termsOfService: 'https://www.hanko.io/terms' contact: name: Hanko Dev Team @@ -16,10 +16,8 @@ servers: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' paths: /tenants: @@ -41,10 +39,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' post: summary: Create a tenant @@ -63,10 +59,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' '/tenants/{tenant_id}': get: @@ -86,10 +80,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' put: summary: Update tenant @@ -106,10 +98,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' delete: summary: Delete tenant @@ -124,14 +114,12 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' '/tenants/{tenant_id}/secrets/jwk': post: - summary: Create secret + summary: Create JWK description: Creates a new JWT encryption key operationId: post-admin-tenant-tenant_id-secrets-jwk parameters: @@ -158,13 +146,11 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' get: - summary: '' + summary: List JWKs description: Get all JWKs as list operationId: get-path_prefix-tenants-tenant_id-secrets-jwk parameters: @@ -180,10 +166,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' '/tenants/{tenant_id}/secrets/api': post: @@ -214,10 +198,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Hostp part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' get: summary: List API keys @@ -236,10 +218,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' '/tenants/{tenant_id}/secrets/jwk/{secret_id}': delete: @@ -251,15 +231,13 @@ paths: - $ref: '#/components/parameters/secret_id' responses: '204': - description: No content + description: No Content servers: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' '/tenants/{tenant_id}/secrets/api/{secret_id}': delete: @@ -276,10 +254,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' '/tenants/{tenant_id}/config': put: @@ -297,10 +273,8 @@ paths: - url: 'http://{host}:8001/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path-Prefix default: '' tags: - name: admin api diff --git a/spec/passkey-server.yaml b/spec/passkey-server.yaml index 3982063..39cf17c 100644 --- a/spec/passkey-server.yaml +++ b/spec/passkey-server.yaml @@ -2,12 +2,12 @@ openapi: 3.1.0 info: version: '1.0' title: passkey-server - summary: 'OpenAPI Spec for creating, managing, and using passkeys' - description: 'This API shall represent the private and public endpoints for passkey registration, management, and authentication' + summary: 'OpenAPI Spec for creating, managing and using passkeys' + description: 'This API shall represent the private and public endpoints for passkey registration, management and authentication' termsOfService: 'https://www.hanko.io/terms' contact: email: developers@hanko.io - url: hanko.io + url: 'https://www.hanko.io' name: Hanko Dev Team license: url: 'https://www.gnu.org/licenses/gpl-3.0.de.html' @@ -16,10 +16,8 @@ servers: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path prefix default: '' paths: '/{tenant_id}/credentials': @@ -50,10 +48,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host Part of the URL default: localhost path_prefix: - description: Path prefix default: '' '/{tenant_id}/credentials/{credential_id}': patch: @@ -70,7 +66,7 @@ paths: $ref: '#/components/requestBodies/patch-credential' responses: '204': - description: No content + description: No Content '400': $ref: '#/components/responses/error' '401': @@ -84,10 +80,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host Part of the URL default: localhost path_prefix: - description: Path prefix default: '' delete: summary: Remove Credential @@ -98,7 +92,7 @@ paths: - $ref: '#/components/parameters/tenant_id' responses: '204': - description: No content + description: No Content '400': $ref: '#/components/responses/error' '401': @@ -112,10 +106,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host Part of the URL default: localhost path_prefix: - description: Path prefix default: '' '/{tenant_id}/registration/initialize': post: @@ -143,10 +135,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path prefix default: '' '/{tenant_id}/registration/finalize': post: @@ -175,10 +165,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host Part of the URL default: localhost path_prefix: - description: Path prefix default: '' '/{tenant_id}/login/initialize': post: @@ -203,10 +191,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path prefix default: '' '/{tenant_id}/login/finalize': post: @@ -233,10 +219,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path prefix default: '' '/{tenant_id}/.well-known/jwks.json': get: @@ -255,10 +239,8 @@ paths: - url: 'http://{host}:8000/{path_prefix}' variables: host: - description: Host part of the URL default: localhost path_prefix: - description: Path prefix default: '' tags: - name: credentials @@ -270,15 +252,10 @@ components: user_id: name: user_id in: query - description: uuid representation of the user + description: representational id of the user required: true schema: type: string - format: uuid - minLength: 36 - maxLength: 36 - examples: - - 1f496bcd-49da-4839-a02f-7ce681ccb488 credential_id: name: credential_id in: path @@ -331,9 +308,6 @@ components: properties: user_id: type: string - format: uuid - minLength: 36 - maxLength: 36 username: type: string icon: @@ -434,7 +408,7 @@ components: - backup_eligible - backup_state error: - description: Error response with detailed information + description: Error Response with detailed information content: application/json: schema: