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:
Moby von Briesen 2021-10-06 09:33:54 -04:00 committed by Maximillian von Briesen
parent b8dd35ceaf
commit 5b729779a2
7 changed files with 87 additions and 23 deletions

View File

@ -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
}

View File

@ -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"

View File

@ -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"
})

View File

@ -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) {

View File

@ -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)
})
}

View File

@ -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.

View File

@ -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)
})
}