diff --git a/backend/cmd/jwt/create.go b/backend/cmd/jwt/create.go index 2fe29ede8..4a2c55bd7 100644 --- a/backend/cmd/jwt/create.go +++ b/backend/cmd/jwt/create.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/crypto/jwk" + "github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/persistence" "github.com/teamhanko/hanko/backend/session" "log" @@ -52,7 +53,20 @@ func NewCreateCommand() *cobra.Command { return } - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(args[0])) + userId := uuid.FromStringOrNil(args[0]) + + emails, err := persister.GetEmailPersister().FindByUserId(userId) + if err != nil { + fmt.Printf("failed to get emails from db: %s", err) + return + } + + var emailJwt *dto.EmailJwt + if e := emails.GetPrimary(); e != nil { + emailJwt = dto.JwtFromEmailModel(e) + } + + token, err := sessionManager.GenerateJWT(userId, emailJwt) if err != nil { fmt.Printf("failed to generate token: %s", err) return diff --git a/backend/dto/email.go b/backend/dto/email.go index 0522fef2d..5ab63e9dc 100644 --- a/backend/dto/email.go +++ b/backend/dto/email.go @@ -39,3 +39,17 @@ func FromEmailModel(email *models.Email) *EmailResponse { return emailResponse } + +type EmailJwt struct { + Address string `json:"address"` + IsPrimary bool `json:"is_primary"` + IsVerified bool `json:"is_verified"` +} + +func JwtFromEmailModel(email *models.Email) *EmailJwt { + return &EmailJwt{ + Address: email.Address, + IsPrimary: email.IsPrimary(), + IsVerified: email.Verified, + } +} diff --git a/backend/handler/email_test.go b/backend/handler/email_test.go index f073ca9bd..632ede120 100644 --- a/backend/handler/email_test.go +++ b/backend/handler/email_test.go @@ -64,7 +64,7 @@ func (s *emailSuite) TestEmailHandler_List() { for _, currentTest := range tests { s.Run(currentTest.name, func() { - token, err := sessionManager.GenerateJWT(currentTest.userId) + token, err := sessionManager.GenerateJWT(currentTest.userId, nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -177,7 +177,7 @@ func (s *emailSuite) TestEmailHandler_Create() { sessionManager, err := session.NewManager(jwkManager, cfg) s.Require().NoError(err) - token, err := sessionManager.GenerateJWT(currentTest.userId) + token, err := sessionManager.GenerateJWT(currentTest.userId, nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -244,7 +244,7 @@ func (s *emailSuite) TestEmailHandler_SetPrimaryEmail() { newPrimaryEmailId := uuid.FromStringOrNil("8bb4c8a7-a3e6-48bb-b54f-20e3b485ab33") userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5") - token, err := sessionManager.GenerateJWT(userId) + token, err := sessionManager.GenerateJWT(userId, nil) s.NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.NoError(err) @@ -285,7 +285,7 @@ func (s *emailSuite) TestEmailHandler_Delete() { sessionManager, err := session.NewManager(jwkManager, test.DefaultConfig) s.Require().NoError(err) - token, err := sessionManager.GenerateJWT(userId) + token, err := sessionManager.GenerateJWT(userId, nil) s.NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.NoError(err) diff --git a/backend/handler/passcode.go b/backend/handler/passcode.go index 0cf6799ab..8ac0d5180 100644 --- a/backend/handler/passcode.go +++ b/backend/handler/passcode.go @@ -361,7 +361,12 @@ func (h *PasscodeHandler) Finish(c echo.Context) error { } } - token, err := h.sessionManager.GenerateJWT(passcode.UserId) + var emailJwt *dto.EmailJwt + if e := user.Emails.GetPrimary(); e != nil { + emailJwt = dto.JwtFromEmailModel(e) + } + + token, err := h.sessionManager.GenerateJWT(passcode.UserId, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } diff --git a/backend/handler/passcode_test.go b/backend/handler/passcode_test.go index 5c1a3bcc7..3be799781 100644 --- a/backend/handler/passcode_test.go +++ b/backend/handler/passcode_test.go @@ -298,13 +298,13 @@ func (s *passcodeSuite) TestPasscodeHandler_Finish() { req := httptest.NewRequest(http.MethodPost, "/passcode/login/finalize", bytes.NewReader(bodyJson)) req.Header.Set("Content-Type", "application/json") if currentTest.sendSessionTokenInAuthHeader { - sessionToken, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(currentTest.userId)) + sessionToken, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(currentTest.userId), nil) s.Require().NoError(err) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sessionToken)) } if currentTest.sendSessionTokenInCookie { - sessionToken, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(currentTest.userId)) + sessionToken, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(currentTest.userId), nil) s.Require().NoError(err) sessionCookie, err := sessionManager.GenerateCookie(sessionToken) diff --git a/backend/handler/password.go b/backend/handler/password.go index 490915cf8..bb638a7af 100644 --- a/backend/handler/password.go +++ b/backend/handler/password.go @@ -218,7 +218,12 @@ func (h *PasswordHandler) Login(c echo.Context) error { return echo.NewHTTPError(http.StatusUnauthorized).SetInternal(err) } - token, err := h.sessionManager.GenerateJWT(pw.UserId) + var emailJwt *dto.EmailJwt + if e := user.Emails.GetPrimary(); e != nil { + emailJwt = dto.JwtFromEmailModel(e) + } + + token, err := h.sessionManager.GenerateJWT(pw.UserId, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } diff --git a/backend/handler/password_test.go b/backend/handler/password_test.go index 110ee7fa7..a80c747b0 100644 --- a/backend/handler/password_test.go +++ b/backend/handler/password_test.go @@ -91,7 +91,7 @@ func (s *passwordSuite) TestPasswordHandler_Set_Create() { s.Require().NoError(err) sessionManager := s.GetDefaultSessionManager() - token, err := sessionManager.GenerateJWT(currentTest.userId) + token, err := sessionManager.GenerateJWT(currentTest.userId, nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -227,17 +227,17 @@ func TestMaxPasswordLength(t *testing.T) { }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - hash, err := bcrypt.GenerateFromPassword([]byte(test.creationPassword), 12) - if test.wantErr { + for _, passwordTest := range tests { + t.Run(passwordTest.name, func(t *testing.T) { + hash, err := bcrypt.GenerateFromPassword([]byte(passwordTest.creationPassword), 12) + if passwordTest.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } - err = bcrypt.CompareHashAndPassword(hash, []byte(test.loginPassword)) - if test.wantErr { + err = bcrypt.CompareHashAndPassword(hash, []byte(passwordTest.loginPassword)) + if passwordTest.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) diff --git a/backend/handler/token.go b/backend/handler/token.go index df22ff2cb..ba2b631d1 100644 --- a/backend/handler/token.go +++ b/backend/handler/token.go @@ -82,7 +82,17 @@ func (h TokenHandler) Validate(c echo.Context) error { return fmt.Errorf("failed to delete token from db: %w", terr) } - jwtToken, err := h.sessionManager.GenerateJWT(token.UserID) + emails, err := h.persister.GetEmailPersister().FindByUserId(token.UserID) + if err != nil { + return fmt.Errorf("failed to get emails from db: %w", err) + } + + var emailJwt *dto.EmailJwt + if e := emails.GetPrimary(); e != nil { + emailJwt = dto.JwtFromEmailModel(e) + } + + jwtToken, err := h.sessionManager.GenerateJWT(token.UserID, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } diff --git a/backend/handler/user.go b/backend/handler/user.go index 4df965743..f9838f82a 100644 --- a/backend/handler/user.go +++ b/backend/handler/user.go @@ -104,7 +104,13 @@ func (h *UserHandler) Create(c echo.Context) error { return fmt.Errorf("failed to store primary email: %w", err) } - token, err := h.sessionManager.GenerateJWT(newUser.ID) + var emailJwt *dto.EmailJwt + if e := newUser.Emails.GetPrimary(); e != nil { + emailJwt = dto.JwtFromEmailModel(e) + } + + token, err := h.sessionManager.GenerateJWT(newUser.ID, emailJwt) + if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } diff --git a/backend/handler/user_test.go b/backend/handler/user_test.go index ad3a4580e..b769f20ac 100644 --- a/backend/handler/user_test.go +++ b/backend/handler/user_test.go @@ -261,7 +261,7 @@ func (s *userSuite) TestUserHandler_Get() { if err != nil { panic(fmt.Errorf("failed to create session generator: %w", err)) } - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId)) + token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -301,7 +301,7 @@ func (s *userSuite) TestUserHandler_GetUserWithWebAuthnCredential() { if err != nil { panic(fmt.Errorf("failed to create session generator: %w", err)) } - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId)) + token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -338,7 +338,7 @@ func (s *userSuite) TestUserHandler_Get_InvalidUserId() { if err != nil { panic(fmt.Errorf("failed to create session generator: %w", err)) } - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId)) + token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -475,7 +475,7 @@ func (s *userSuite) TestUserHandler_Me() { if err != nil { panic(fmt.Errorf("failed to create session generator: %w", err)) } - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId)) + token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -512,7 +512,7 @@ func (s *userSuite) TestUserHandler_Logout() { if err != nil { panic(fmt.Errorf("failed to create session generator: %w", err)) } - token, err := sessionManager.GenerateJWT(userId) + token, err := sessionManager.GenerateJWT(userId, nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -553,7 +553,7 @@ func (s *userSuite) TestUserHandler_Delete() { if err != nil { panic(fmt.Errorf("failed to create session generator: %w", err)) } - token, err := sessionManager.GenerateJWT(userId) + token, err := sessionManager.GenerateJWT(userId, nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) diff --git a/backend/handler/webauthn.go b/backend/handler/webauthn.go index 27702c0fd..ba693835f 100644 --- a/backend/handler/webauthn.go +++ b/backend/handler/webauthn.go @@ -32,6 +32,17 @@ type WebauthnHandler struct { authenticatorMetadata mapper.AuthenticatorMetadata } +const ( + GetUserFailureMessage = "failed to get user: %w" + CastSessionFailureMessage = "failed to cast session object" + CreateAuditLogFailureMessage = "failed to create audit log: %w" + UserNotFoundMessage = "user not found" + SubjectParseFailureMessage = "failed to parse subject as uuid: %w" + GetWebauthnCredentialFailureMessage = "failed to get webauthn credentials: %w" + StoredChallengeMismatchMessage = "Stored challenge and received challenge do not match" + UnknownUserMessage = "unknown user" +) + // NewWebauthnHandler creates a new handler which handles all webauthn related routes func NewWebauthnHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, auditLogger auditlog.Logger, authenticatorMetadata mapper.AuthenticatorMetadata) (*WebauthnHandler, error) { f := false @@ -76,7 +87,7 @@ func NewWebauthnHandler(cfg *config.Config, persister persistence.Persister, ses func (h *WebauthnHandler) BeginRegistration(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { - return errors.New("failed to cast session object") + return errors.New(CastSessionFailureMessage) } uId, err := uuid.FromString(sessionToken.Subject()) if err != nil { @@ -84,14 +95,14 @@ func (h *WebauthnHandler) BeginRegistration(c echo.Context) error { } webauthnUser, user, err := h.getWebauthnUser(h.persister.GetConnection(), uId) if err != nil { - return fmt.Errorf("failed to get user: %w", err) + return fmt.Errorf(GetUserFailureMessage, err) } if webauthnUser == nil { err = h.auditLogger.Create(c, models.AuditLogWebAuthnRegistrationInitFailed, nil, fmt.Errorf("unknown user")) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusBadRequest, "user not found").SetInternal(errors.New(fmt.Sprintf("user %s not found ", uId))) + return echo.NewHTTPError(http.StatusBadRequest, UserNotFoundMessage).SetInternal(errors.New(fmt.Sprintf("user %s not found ", uId))) } t := true @@ -117,7 +128,7 @@ func (h *WebauthnHandler) BeginRegistration(c echo.Context) error { err = h.auditLogger.Create(c, models.AuditLogWebAuthnRegistrationInitSucceeded, user, nil) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return c.JSON(http.StatusOK, options) @@ -128,7 +139,7 @@ func (h *WebauthnHandler) BeginRegistration(c echo.Context) error { func (h *WebauthnHandler) FinishRegistration(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { - return errors.New("failed to cast session object") + return errors.New(CastSessionFailureMessage) } request, err := protocol.ParseCredentialCreationResponse(c.Request()) if err != nil { @@ -148,30 +159,30 @@ func (h *WebauthnHandler) FinishRegistration(c echo.Context) error { if sessionData == nil { err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, nil, fmt.Errorf("received unkown challenge")) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusBadRequest, "Stored challenge and received challenge do not match").SetInternal(errors.New("sessionData not found")) + return echo.NewHTTPError(http.StatusBadRequest, StoredChallengeMismatchMessage).SetInternal(errors.New("sessionData not found")) } if sessionToken.Subject() != sessionData.UserId.String() { err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, nil, fmt.Errorf("user session does not match sessionData subject")) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusBadRequest, "Stored challenge and received challenge do not match").SetInternal(errors.New("userId in webauthn.sessionData does not match user session")) + return echo.NewHTTPError(http.StatusBadRequest, StoredChallengeMismatchMessage).SetInternal(errors.New("userId in webauthn.sessionData does not match user session")) } webauthnUser, user, err := h.getWebauthnUser(tx, sessionData.UserId) if err != nil { - return fmt.Errorf("failed to get user: %w", err) + return fmt.Errorf(GetUserFailureMessage, err) } if webauthnUser == nil { - err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, nil, fmt.Errorf("unkown user")) + err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, nil, fmt.Errorf(UnknownUserMessage)) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("user not found")) + return echo.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New(UserNotFoundMessage)) } credential, err := h.webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), request) @@ -191,7 +202,7 @@ func (h *WebauthnHandler) FinishRegistration(c echo.Context) error { } err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, user, errors.New(errorMessage)) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return echo.NewHTTPError(errorStatus, errorMessage).SetInternal(err) @@ -212,7 +223,7 @@ func (h *WebauthnHandler) FinishRegistration(c echo.Context) error { err = h.auditLogger.Create(c, models.AuditLogWebAuthnRegistrationFinalSucceeded, user, nil) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return c.JSON(http.StatusOK, map[string]string{"credential_id": model.ID, "user_id": webauthnUser.UserId.String()}) @@ -240,21 +251,21 @@ func (h *WebauthnHandler) BeginAuthentication(c echo.Context) error { if err != nil { err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationInitFailed, nil, fmt.Errorf("user_id is not a uuid")) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return echo.NewHTTPError(http.StatusBadRequest, "failed to parse UserID as uuid").SetInternal(err) } var webauthnUser *intern.WebauthnUser - webauthnUser, user, err = h.getWebauthnUser(h.persister.GetConnection(), userId) // TODO: + webauthnUser, user, err = h.getWebauthnUser(h.persister.GetConnection(), userId) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(fmt.Errorf("failed to get user: %w", err)) + return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(fmt.Errorf(GetUserFailureMessage, err)) } if webauthnUser == nil { - err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationInitFailed, nil, fmt.Errorf("unkown user")) + err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationInitFailed, nil, fmt.Errorf(UnknownUserMessage)) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusBadRequest, "user not found") + return echo.NewHTTPError(http.StatusBadRequest, UserNotFoundMessage) } if len(webauthnUser.WebAuthnCredentials()) > 0 { @@ -284,13 +295,13 @@ func (h *WebauthnHandler) BeginAuthentication(c echo.Context) error { // Remove all transports, because of a bug in android and windows where the internal authenticator gets triggered, // when the transports array contains the type 'internal' although the credential is not available on the device. - for i, _ := range options.Response.AllowedCredentials { + for i := range options.Response.AllowedCredentials { options.Response.AllowedCredentials[i].Transport = nil } err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationInitSucceeded, user, nil) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return c.JSON(http.StatusOK, options) @@ -317,9 +328,9 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { if sessionData == nil { err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf("received unkown challenge")) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusUnauthorized, "Stored challenge and received challenge do not match").SetInternal(errors.New("sessionData not found")) + return echo.NewHTTPError(http.StatusUnauthorized, StoredChallengeMismatchMessage).SetInternal(errors.New("sessionData not found")) } model := intern.WebauthnSessionDataFromModel(sessionData) @@ -335,15 +346,15 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { } webauthnUser, user, err = h.getWebauthnUser(tx, userId) if err != nil { - return fmt.Errorf("failed to get user: %w", err) + return fmt.Errorf(GetUserFailureMessage, err) } if webauthnUser == nil { - err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf("unkown user")) + err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf(UnknownUserMessage)) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("user not found")) + return echo.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(UserNotFoundMessage)) } credential, err = h.webauthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) { @@ -352,7 +363,7 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { if err != nil { logErr := h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, user, fmt.Errorf("assertion validation failed")) if logErr != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return echo.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err) } @@ -360,20 +371,20 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { // non discoverable Login webauthnUser, user, err = h.getWebauthnUser(tx, sessionData.UserId) if err != nil { - return fmt.Errorf("failed to get user: %w", err) + return fmt.Errorf(GetUserFailureMessage, err) } if webauthnUser == nil { - err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf("unkown user")) + err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf(UnknownUserMessage)) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } - return echo.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("user not found")) + return echo.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(UserNotFoundMessage)) } credential, err = h.webauthn.ValidateLogin(webauthnUser, *model, request) if err != nil { logErr := h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, user, fmt.Errorf("assertion validation failed")) if logErr != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return echo.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err) } @@ -406,7 +417,12 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { return fmt.Errorf("failed to delete assertion session data: %w", err) } - token, err := h.sessionManager.GenerateJWT(webauthnUser.UserId) + var emailJwt *dto.EmailJwt + if e := user.Emails.GetPrimary(); e != nil { + emailJwt = dto.JwtFromEmailModel(e) + } + + token, err := h.sessionManager.GenerateJWT(webauthnUser.UserId, emailJwt) if err != nil { return fmt.Errorf("failed to generate jwt: %w", err) } @@ -426,7 +442,7 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationFinalSucceeded, user, nil) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return c.JSON(http.StatusOK, map[string]string{"credential_id": base64.RawURLEncoding.EncodeToString(credential.ID), "user_id": webauthnUser.UserId.String()}) @@ -436,17 +452,17 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error { func (h *WebauthnHandler) ListCredentials(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { - return errors.New("failed to cast session object") + return errors.New(CastSessionFailureMessage) } userId, err := uuid.FromString(sessionToken.Subject()) if err != nil { - return fmt.Errorf("failed to parse subject as uuid: %w", err) + return fmt.Errorf(SubjectParseFailureMessage, err) } credentials, err := h.persister.GetWebauthnCredentialPersister().GetFromUser(userId) if err != nil { - return fmt.Errorf("failed to get webauthn credentials: %w", err) + return fmt.Errorf(GetWebauthnCredentialFailureMessage, err) } response := make([]*dto.WebauthnCredentialResponse, len(credentials)) @@ -461,12 +477,12 @@ func (h *WebauthnHandler) ListCredentials(c echo.Context) error { func (h *WebauthnHandler) UpdateCredential(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { - return errors.New("failed to cast session object") + return errors.New(CastSessionFailureMessage) } userId, err := uuid.FromString(sessionToken.Subject()) if err != nil { - return fmt.Errorf("failed to parse subject as uuid: %w", err) + return fmt.Errorf(SubjectParseFailureMessage, err) } credentialID := c.Param("id") @@ -480,12 +496,12 @@ func (h *WebauthnHandler) UpdateCredential(c echo.Context) error { user, err := h.persister.GetUserPersister().Get(userId) if err != nil { - return fmt.Errorf("failed to get user: %w", err) + return fmt.Errorf(GetUserFailureMessage, err) } credential, err := h.persister.GetWebauthnCredentialPersister().Get(credentialID) if err != nil { - return fmt.Errorf("failed to get webauthn credentials: %w", err) + return fmt.Errorf(GetWebauthnCredentialFailureMessage, err) } if credential == nil || credential.UserId.String() != user.ID.String() { @@ -503,7 +519,7 @@ func (h *WebauthnHandler) UpdateCredential(c echo.Context) error { } err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnCredentialUpdated, user, nil) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return nil }) @@ -512,12 +528,12 @@ func (h *WebauthnHandler) UpdateCredential(c echo.Context) error { func (h *WebauthnHandler) DeleteCredential(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { - return errors.New("failed to cast session object") + return errors.New(CastSessionFailureMessage) } userId, err := uuid.FromString(sessionToken.Subject()) if err != nil { - return fmt.Errorf("failed to parse subject as uuid: %w", err) + return fmt.Errorf(SubjectParseFailureMessage, err) } user, err := h.persister.GetUserPersister().Get(userId) @@ -544,7 +560,7 @@ func (h *WebauthnHandler) DeleteCredential(c echo.Context) error { err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnCredentialDeleted, user, nil) if err != nil { - return fmt.Errorf("failed to create audit log: %w", err) + return fmt.Errorf(CreateAuditLogFailureMessage, err) } return c.NoContent(http.StatusNoContent) @@ -554,7 +570,7 @@ func (h *WebauthnHandler) DeleteCredential(c echo.Context) error { func (h WebauthnHandler) getWebauthnUser(connection *pop.Connection, userId uuid.UUID) (*intern.WebauthnUser, *models.User, error) { user, err := h.persister.GetUserPersisterWithConnection(connection).Get(userId) if err != nil { - return nil, nil, fmt.Errorf("failed to get user: %w", err) + return nil, nil, fmt.Errorf(GetUserFailureMessage, err) } if user == nil { @@ -563,7 +579,7 @@ func (h WebauthnHandler) getWebauthnUser(connection *pop.Connection, userId uuid credentials, err := h.persister.GetWebauthnCredentialPersisterWithConnection(connection).GetFromUser(user.ID) if err != nil { - return nil, nil, fmt.Errorf("failed to get webauthn credentials: %w", err) + return nil, nil, fmt.Errorf(GetWebauthnCredentialFailureMessage, err) } webauthnUser, err := intern.NewWebauthnUser(*user, credentials) diff --git a/backend/handler/webauthn_test.go b/backend/handler/webauthn_test.go index 7790c486f..0cd11edc0 100644 --- a/backend/handler/webauthn_test.go +++ b/backend/handler/webauthn_test.go @@ -8,8 +8,8 @@ import ( "github.com/labstack/echo/v4" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/stretchr/testify/suite" - "github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/crypto/jwk" + "github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/persistence/models" "github.com/teamhanko/hanko/backend/session" "github.com/teamhanko/hanko/backend/test" @@ -17,7 +17,6 @@ import ( "net/http/httptest" "strings" "testing" - "time" ) func TestWebauthnSuite(t *testing.T) { @@ -51,7 +50,7 @@ func (s *webauthnSuite) TestWebauthnHandler_BeginRegistration() { e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil) sessionManager := s.GetDefaultSessionManager() - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId)) + token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -92,7 +91,7 @@ func (s *webauthnSuite) TestWebauthnHandler_FinalizeRegistration() { e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil) sessionManager := s.GetDefaultSessionManager() - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId)) + token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -138,7 +137,7 @@ func (s *webauthnSuite) TestWebauthnHandler_FinalizeRegistration_SessionDataExpi e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil) sessionManager := s.GetDefaultSessionManager() - token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId)) + token, err := sessionManager.GenerateJWT(uuid.FromStringOrNil(userId), nil) s.Require().NoError(err) cookie, err := sessionManager.GenerateCookie(token) s.Require().NoError(err) @@ -330,29 +329,10 @@ func (s *webauthnSuite) GetDefaultSessionManager() session.Manager { var userId = "ec4ef049-5b88-4321-a173-21b0eff06a04" -var defaultConfig = config.Config{ - Webauthn: config.WebauthnSettings{ - RelyingParty: config.RelyingParty{ - Id: "localhost", - DisplayName: "Test Relying Party", - Icon: "", - Origins: []string{"http://localhost:8080"}, - }, - Timeout: 60000, - }, - Secrets: config.Secrets{ - Keys: []string{"abcdefghijklmnop"}, - }, - Smtp: config.SMTP{ - Host: "localhost", - Port: "2500", - }, -} - type sessionManager struct { } -func (s sessionManager) GenerateJWT(uuid uuid.UUID) (string, error) { +func (s sessionManager) GenerateJWT(_ uuid.UUID, _ *dto.EmailJwt) (string, error) { return userId, nil } @@ -377,7 +357,7 @@ func (s sessionManager) DeleteCookie() (*http.Cookie, error) { }, nil } -func (s sessionManager) Verify(token string) (jwt.Token, error) { +func (s sessionManager) Verify(_ string) (jwt.Token, error) { return nil, nil } @@ -390,14 +370,3 @@ var emails = []models.Email{ PrimaryEmail: &models.PrimaryEmail{ID: uId}, }, } - -var users = []models.User{ - func() models.User { - return models.User{ - ID: uId, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Emails: emails, - } - }(), -} diff --git a/backend/session/session.go b/backend/session/session.go index bd385b4e4..e56bfaaa0 100644 --- a/backend/session/session.go +++ b/backend/session/session.go @@ -7,12 +7,13 @@ import ( "github.com/teamhanko/hanko/backend/config" hankoJwk "github.com/teamhanko/hanko/backend/crypto/jwk" hankoJwt "github.com/teamhanko/hanko/backend/crypto/jwt" + "github.com/teamhanko/hanko/backend/dto" "net/http" "time" ) type Manager interface { - GenerateJWT(uuid.UUID) (string, error) + GenerateJWT(userId uuid.UUID, userDto *dto.EmailJwt) (string, error) Verify(string) (jwt.Token, error) GenerateCookie(token string) (*http.Cookie, error) DeleteCookie() (*http.Cookie, error) @@ -35,19 +36,23 @@ type cookieConfig struct { Secure bool } +const ( + GeneratorCreateFailure = "failed to create session generator: %w" +) + // NewManager returns a new Manager which will be used to create and verify sessions JWTs func NewManager(jwkManager hankoJwk.Manager, config config.Config) (Manager, error) { signatureKey, err := jwkManager.GetSigningKey() if err != nil { - return nil, fmt.Errorf("failed to create session generator: %w", err) + return nil, fmt.Errorf(GeneratorCreateFailure, err) } verificationKeys, err := jwkManager.GetPublicKeys() if err != nil { - return nil, fmt.Errorf("failed to create session generator: %w", err) + return nil, fmt.Errorf(GeneratorCreateFailure, err) } g, err := hankoJwt.NewGenerator(signatureKey, verificationKeys) if err != nil { - return nil, fmt.Errorf("failed to create session generator: %w", err) + return nil, fmt.Errorf(GeneratorCreateFailure, err) } duration, _ := time.ParseDuration(config.Session.Lifespan) // error can be ignored, value is checked in config validation @@ -85,7 +90,7 @@ func NewManager(jwkManager hankoJwk.Manager, config config.Config) (Manager, err } // GenerateJWT creates a new session JWT for the given user -func (m *manager) GenerateJWT(userId uuid.UUID) (string, error) { +func (m *manager) GenerateJWT(userId uuid.UUID, email *dto.EmailJwt) (string, error) { issuedAt := time.Now() expiration := issuedAt.Add(m.sessionLength) @@ -94,6 +99,11 @@ func (m *manager) GenerateJWT(userId uuid.UUID) (string, error) { _ = token.Set(jwt.IssuedAtKey, issuedAt) _ = token.Set(jwt.ExpirationKey, expiration) _ = token.Set(jwt.AudienceKey, m.audience) + + if email != nil { + _ = token.Set("email", &email) + } + if m.issuer != "" { _ = token.Set(jwt.IssuerKey, m.issuer) } diff --git a/backend/session/session_test.go b/backend/session/session_test.go index 4e75459ac..5acb0484e 100644 --- a/backend/session/session_test.go +++ b/backend/session/session_test.go @@ -1,11 +1,13 @@ package session import ( + "encoding/json" "github.com/gofrs/uuid" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/teamhanko/hanko/backend/config" + "github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/test" "testing" "time" @@ -29,7 +31,7 @@ func TestGenerator_Generate(t *testing.T) { userId, err := uuid.NewV4() assert.NoError(t, err) - session, err := sessionGenerator.GenerateJWT(userId) + session, err := sessionGenerator.GenerateJWT(userId, nil) assert.NoError(t, err) require.NotEmpty(t, session) } @@ -47,7 +49,15 @@ func TestGenerator_Verify(t *testing.T) { userId, err := uuid.NewV4() assert.NoError(t, err) - session, err := sessionGenerator.GenerateJWT(userId) + testEmail := "lorem@ipsum.local" + + emailDto := &dto.EmailJwt{ + Address: testEmail, + IsPrimary: true, + IsVerified: false, + } + + session, err := sessionGenerator.GenerateJWT(userId, emailDto) assert.NoError(t, err) require.NotEmpty(t, session) @@ -58,6 +68,19 @@ func TestGenerator_Verify(t *testing.T) { assert.False(t, time.Time{}.Equal(token.IssuedAt())) assert.False(t, time.Time{}.Equal(token.Expiration())) + emailClaim, ok := token.Get("email") + assert.True(t, ok) + assert.NotNil(t, emailClaim) + + // Workaround as .(EmailJwt) interface conversion is not possible + emailJson, _ := json.Marshal(emailClaim) + var tokenEmail dto.EmailJwt + _ = json.Unmarshal(emailJson, &tokenEmail) + + assert.Equal(t, testEmail, tokenEmail.Address) + assert.True(t, tokenEmail.IsPrimary) + assert.False(t, tokenEmail.IsVerified) + sessionDuration, _ := time.ParseDuration(sessionLifespan) assert.True(t, token.IssuedAt().Add(sessionDuration).Equal(token.Expiration())) } @@ -80,7 +103,7 @@ func TestManager_GenerateJWT_IssAndAud(t *testing.T) { require.NotEmpty(t, sessionGenerator) userId, _ := uuid.NewV4() - j, err := sessionGenerator.GenerateJWT(userId) + j, err := sessionGenerator.GenerateJWT(userId, nil) assert.NoError(t, err) token, err := jwt.ParseString(j, jwt.WithVerify(false)) @@ -111,7 +134,7 @@ func TestManager_GenerateJWT_AdditionalAudiences(t *testing.T) { require.NotEmpty(t, sessionGenerator) userId, _ := uuid.NewV4() - j, err := sessionGenerator.GenerateJWT(userId) + j, err := sessionGenerator.GenerateJWT(userId, nil) assert.NoError(t, err) token, err := jwt.ParseString(j, jwt.WithVerify(false)) @@ -145,9 +168,9 @@ func TestGenerator_Verify_Error(t *testing.T) { }, } - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - _, err := sessionGenerator.Verify(test.Input) + for _, verifyTest := range tests { + t.Run(verifyTest.Name, func(t *testing.T) { + _, err := sessionGenerator.Verify(verifyTest.Input) assert.Error(t, err) }) }