Email: trigger email verification flow (#85587)

* Add email and email_verified to id token if identity is a user

* Add endpoint to trigger email verification for user

* Add function to clear stored id tokens and use it when email verification is completed
This commit is contained in:
Karl Persson
2024-04-05 12:05:46 +02:00
committed by GitHub
parent 661aaf352e
commit ba41954854
14 changed files with 122 additions and 19 deletions
+1
View File
@@ -191,6 +191,7 @@ func (hs *HTTPServer) registerRoutes() {
// update user email
if hs.Cfg.Smtp.Enabled && hs.Cfg.VerifyEmailEnabled {
r.Get("/user/email/update", reqSignedInNoAnonymous, routing.Wrap(hs.UpdateUserEmail))
r.Post("/api/user/email/start-verify", reqSignedInNoAnonymous, routing.Wrap(hs.StartEmailVerificaton))
}
// invited
+30 -3
View File
@@ -264,12 +264,12 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC
if err != nil {
return response.Error(http.StatusBadRequest, "Invalid email address", err)
}
return hs.verifyEmailUpdate(ctx, normalized, user.EmailUpdateAction, usr)
return hs.startEmailVerification(ctx, normalized, user.EmailUpdateAction, usr)
}
if len(cmd.Login) != 0 && usr.Login != cmd.Login {
normalized, err := ValidateAndNormalizeEmail(cmd.Login)
if err == nil && usr.Email != normalized {
return hs.verifyEmailUpdate(ctx, cmd.Login, user.LoginUpdateAction, usr)
return hs.startEmailVerification(ctx, cmd.Login, user.LoginUpdateAction, usr)
}
}
}
@@ -284,7 +284,34 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC
return response.Success("User updated")
}
func (hs *HTTPServer) verifyEmailUpdate(ctx context.Context, email string, field user.UpdateEmailActionType, usr *user.User) response.Response {
func (hs *HTTPServer) StartEmailVerificaton(c *contextmodel.ReqContext) response.Response {
namespace, id := c.SignedInUser.GetNamespacedID()
if !identity.IsNamespace(namespace, identity.NamespaceUser) {
return response.Error(http.StatusBadRequest, "Only users can verify their email", nil)
}
if c.SignedInUser.IsEmailVerified() {
// email is already verified so we don't need to trigger the flow.
return response.Respond(http.StatusNotModified, nil)
}
userID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return response.Error(http.StatusInternalServerError, "Got invalid user id", err)
}
usr, err := hs.userService.GetByID(c.Req.Context(), &user.GetUserByIDQuery{
ID: userID,
})
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to fetch user", err)
}
return hs.startEmailVerification(c.Req.Context(), usr.Email, user.EmailUpdateAction, usr)
}
func (hs *HTTPServer) startEmailVerification(ctx context.Context, email string, field user.UpdateEmailActionType, usr *user.User) response.Response {
if err := hs.userVerifier.Start(ctx, user.StartVerifyEmailCommand{
User: *usr,
Email: email,
+9 -9
View File
@@ -10,13 +10,6 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
@@ -30,23 +23,30 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/socialtest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/auth/idtest"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
"github.com/grafana/grafana/pkg/services/login/authinfotest"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"github.com/grafana/grafana/pkg/services/secrets/database"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
"github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
const newEmail = "newemail@localhost"
@@ -397,7 +397,7 @@ func setupUpdateEmailTests(t *testing.T, cfg *setting.Cfg) (*user.User, *HTTPSer
require.NoError(t, err)
nsMock := notifications.MockNotificationService()
verifier := userimpl.ProvideVerifier(cfg, userSvc, tempUserService, nsMock)
verifier := userimpl.ProvideVerifier(cfg, userSvc, tempUserService, nsMock, &idtest.MockService{})
hs := &HTTPServer{
Cfg: cfg,
@@ -620,7 +620,7 @@ func TestUser_UpdateEmail(t *testing.T) {
hs.tempUserService = tempUserSvc
hs.NotificationService = nsMock
hs.SecretsService = fakes.NewFakeSecretsService()
hs.userVerifier = userimpl.ProvideVerifier(settings, userSvc, tempUserSvc, nsMock)
hs.userVerifier = userimpl.ProvideVerifier(settings, userSvc, tempUserSvc, nsMock, &idtest.MockService{})
// User is internal
hs.authInfoService = &authinfotest.FakeService{ExpectedError: user.ErrUserNotFound}
})