satellite/console: Automatically log a user in after verifying email
When an email is verified, insert an auth cookie so that when the user is redirected after verifying their email, they are immediately taken to the onboarding flow. Change-Id: I557d8a2805b24dd8039ada255522bc1b56cc8b53
This commit is contained in:
parent
b8dd35ceaf
commit
5b729779a2
@ -232,7 +232,7 @@ func (system *Satellite) AddUser(ctx context.Context, newUser console.CreateUser
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = system.API.Console.Service.ActivateAccount(ctx, activationToken)
|
||||
_, err = system.API.Console.Service.ActivateAccount(ctx, activationToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ func TestGraphqlMutation(t *testing.T) {
|
||||
activationToken, err := service.GenerateActivationToken(ctx, rootUser.ID, rootUser.Email)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.ActivateAccount(ctx, activationToken)
|
||||
_, err = service.ActivateAccount(ctx, activationToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := service.Token(ctx, console.AuthUser{Email: createUser.Email, Password: createUser.Password})
|
||||
@ -229,7 +229,7 @@ func TestGraphqlMutation(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.ActivateAccount(ctx, activationToken1)
|
||||
_, err = service.ActivateAccount(ctx, activationToken1)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1.Email = "u1@mail.test"
|
||||
@ -253,7 +253,7 @@ func TestGraphqlMutation(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.ActivateAccount(ctx, activationToken2)
|
||||
_, err = service.ActivateAccount(ctx, activationToken2)
|
||||
require.NoError(t, err)
|
||||
|
||||
user2.Email = "u2@mail.test"
|
||||
|
@ -134,7 +134,7 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
"mtest@mail.test",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = service.ActivateAccount(ctx, activationToken)
|
||||
_, err = service.ActivateAccount(ctx, activationToken)
|
||||
require.NoError(t, err)
|
||||
rootUser.Email = "mtest@mail.test"
|
||||
})
|
||||
@ -209,7 +209,7 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
"muu1@mail.test",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = service.ActivateAccount(ctx, activationToken1)
|
||||
_, err = service.ActivateAccount(ctx, activationToken1)
|
||||
require.NoError(t, err)
|
||||
user1.Email = "muu1@mail.test"
|
||||
})
|
||||
@ -232,7 +232,7 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
"muu2@mail.test",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = service.ActivateAccount(ctx, activationToken2)
|
||||
_, err = service.ActivateAccount(ctx, activationToken2)
|
||||
require.NoError(t, err)
|
||||
user2.Email = "muu2@mail.test"
|
||||
})
|
||||
|
@ -553,7 +553,7 @@ func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Re
|
||||
defer mon.Task()(&ctx)(nil)
|
||||
activationToken := r.URL.Query().Get("token")
|
||||
|
||||
err := server.service.ActivateAccount(ctx, activationToken)
|
||||
token, err := server.service.ActivateAccount(ctx, activationToken)
|
||||
if err != nil {
|
||||
server.log.Error("activation: failed to activate account",
|
||||
zap.String("token", activationToken),
|
||||
@ -573,7 +573,9 @@ func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, server.config.AccountActivationRedirectURL, http.StatusTemporaryRedirect)
|
||||
server.cookieAuth.SetTokenCookie(w, token)
|
||||
|
||||
http.Redirect(w, r, server.config.ExternalAddress, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (server *Server) cancelPasswordRecoveryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -39,7 +39,7 @@ func TestActivationRouting(t *testing.T) {
|
||||
activationToken, err := service.GenerateActivationToken(ctx, user.ID, user.Email)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkActivationRedirect := func(testMsg, redirectURL string) {
|
||||
checkActivationRedirect := func(testMsg, redirectURL string, shouldHaveCookie bool) {
|
||||
url := "http://" + sat.API.Console.Listener.Addr().String() + "/activation/?token=" + activationToken
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
||||
@ -48,6 +48,16 @@ func TestActivationRouting(t *testing.T) {
|
||||
result, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err, testMsg)
|
||||
|
||||
// cookie should be set on successful activation
|
||||
hasCookie := false
|
||||
for _, c := range result.Cookies() {
|
||||
if c.Name == "_tokenKey" {
|
||||
hasCookie = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Equal(t, shouldHaveCookie, hasCookie)
|
||||
|
||||
require.Equal(t, http.StatusTemporaryRedirect, result.StatusCode, testMsg)
|
||||
require.Equal(t, redirectURL, result.Header.Get("Location"), testMsg)
|
||||
require.NoError(t, result.Body.Close(), testMsg)
|
||||
@ -57,10 +67,13 @@ func TestActivationRouting(t *testing.T) {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
loginURL := "http://" + sat.API.Console.Listener.Addr().String() + "/login"
|
||||
baseURL := "http://" + sat.API.Console.Listener.Addr().String() + "/"
|
||||
loginURL := baseURL + "login"
|
||||
|
||||
checkActivationRedirect("Activation - Fresh Token", loginURL+"?activated=true")
|
||||
checkActivationRedirect("Activation - Used Token", loginURL+"?activated=false")
|
||||
// successful activation should set cookie and redirect to home page.
|
||||
checkActivationRedirect("Activation - Fresh Token", baseURL, true)
|
||||
// unsuccessful redirect should not set cookie and redirect to login page.
|
||||
checkActivationRedirect("Activation - Used Token", loginURL+"?activated=false", false)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -704,45 +704,59 @@ func (s *Service) GeneratePasswordRecoveryToken(ctx context.Context, id uuid.UUI
|
||||
}
|
||||
|
||||
// ActivateAccount - is a method for activating user account after registration.
|
||||
func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (err error) {
|
||||
func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (token string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
token, err := consoleauth.FromBase64URLString(activationToken)
|
||||
parsedActivationToken, err := consoleauth.FromBase64URLString(activationToken)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
return "", Error.Wrap(err)
|
||||
}
|
||||
|
||||
claims, err := s.authenticate(ctx, token)
|
||||
claims, err := s.authenticate(ctx, parsedActivationToken)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = s.store.Users().GetByEmail(ctx, claims.Email)
|
||||
if err == nil {
|
||||
return ErrEmailUsed.New(emailUsedErrMsg)
|
||||
return "", ErrEmailUsed.New(emailUsedErrMsg)
|
||||
}
|
||||
|
||||
user, err := s.store.Users().Get(ctx, claims.ID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
return "", Error.Wrap(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if now.After(user.CreatedAt.Add(TokenExpirationTime)) {
|
||||
return ErrTokenExpiration.Wrap(err)
|
||||
return "", ErrTokenExpiration.Wrap(err)
|
||||
}
|
||||
|
||||
user.Status = Active
|
||||
err = s.store.Users().Update(ctx, user)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
return "", Error.Wrap(err)
|
||||
}
|
||||
s.auditLog(ctx, "activate account", &user.ID, user.Email)
|
||||
|
||||
s.analytics.TrackAccountVerified(user.ID, user.Email)
|
||||
|
||||
return nil
|
||||
// now that the account is activated, create a token to be stored in a cookie to log the user in.
|
||||
claims = &consoleauth.Claims{
|
||||
ID: user.ID,
|
||||
Expiration: time.Now().Add(TokenExpirationTime),
|
||||
}
|
||||
|
||||
token, err = s.createToken(ctx, claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.auditLog(ctx, "login", &user.ID, user.Email)
|
||||
|
||||
s.analytics.TrackSignedIn(user.ID, user.Email)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// ResetPassword - is a method for resetting user password.
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
@ -593,3 +594,37 @@ func TestResetPassword(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestActivateAccountToken ensures that the token returned after activating an account can be used to authorize user activity.
|
||||
// i.e. a user does not need to acquire an authorization separate from the account activation step.
|
||||
func TestActivateAccountToken(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
sat := planet.Satellites[0]
|
||||
service := sat.API.Console.Service
|
||||
|
||||
createUser := console.CreateUser{
|
||||
FullName: "Alice",
|
||||
ShortName: "Alice",
|
||||
Email: "alice@mail.test",
|
||||
Password: "123a123",
|
||||
}
|
||||
|
||||
regToken, err := service.CreateRegToken(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
rootUser, err := service.CreateUser(ctx, createUser, regToken.Secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
activationToken, err := service.GenerateActivationToken(ctx, rootUser.ID, rootUser.Email)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := service.ActivateAccount(ctx, activationToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Authorize(consoleauth.WithAPIKey(ctx, []byte(authToken)))
|
||||
require.NoError(t, err)
|
||||
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user