diff --git a/integration/integration_test.go b/integration/integration_test.go index 67d05e702f322..61fb30cb31b48 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -44,7 +44,6 @@ import ( "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/testauthority" - "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/bpf" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/defaults" @@ -3712,70 +3711,6 @@ func (s *IntSuite) TestList(c *check.C) { } } -// TestMultipleSignup makes sure that multiple users can create Teleport accounts. -func (s *IntSuite) TestMultipleSignup(c *check.C) { - tr := utils.NewTracer(utils.ThisFunction()).Start() - defer tr.Stop() - - type createNewUserReq struct { - InviteToken string `json:"invite_token"` - Pass string `json:"pass"` - } - - // Create and start a Teleport cluster. - makeConfig := func() (*check.C, []string, []*InstanceSecrets, *service.Config) { - clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{ - SessionRecording: services.RecordAtNode, - LocalAuth: services.NewBool(true), - }) - c.Assert(err, check.IsNil) - - tconf := service.MakeDefaultConfig() - tconf.Auth.Preference.SetSecondFactor("off") - tconf.Auth.Enabled = true - tconf.Auth.ClusterConfig = clusterConfig - tconf.Proxy.Enabled = true - tconf.Proxy.DisableWebService = false - tconf.Proxy.DisableWebInterface = true - tconf.SSH.Enabled = true - return c, nil, nil, tconf - } - main := s.newTeleportWithConfig(makeConfig()) - defer main.Stop(true) - - mainAuth := main.Process.GetAuthServer() - - // Create a few users to make sure the proxy uses the correct identity - // when connecting to the auth server. - for i := 0; i < 5; i++ { - // Create a random username. - username, err := utils.CryptoRandomHex(16) - c.Assert(err, check.IsNil) - - // Create signup token, this is like doing "tctl users add foo foo". - token, err := mainAuth.CreateSignupToken(services.UserV1{ - Name: username, - AllowedLogins: []string{username}, - }, backend.Forever) - c.Assert(err, check.IsNil) - - // Create client that will simulate web browser. - clt, err := createWebClient(main) - c.Assert(err, check.IsNil) - - // Render the signup page. - _, err = clt.Get(context.Background(), clt.Endpoint("webapi", "users", "invites", token), url.Values{}) - c.Assert(err, check.IsNil) - - // Make sure signup is successful. - _, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "users"), createNewUserReq{ - InviteToken: token, - Pass: "fake-password-123", - }) - c.Assert(err, check.IsNil) - } -} - // TestDataTransfer makes sure that a "session.data" event is emitted at the // end of a session that matches the amount of data that was transferred. func (s *IntSuite) TestDataTransfer(c *check.C) { diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index 3a6e5e9b613d6..a4682737ad687 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -42,7 +42,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/julienschmidt/httprouter" - "github.com/tstranex/u2f" ) type APIConfig struct { @@ -103,9 +102,11 @@ func NewAPIServer(config *APIConfig) http.Handler { srv.POST("/:version/users/:user/ssh/authenticate", srv.withAuth(srv.authenticateSSHUser)) srv.GET("/:version/users/:user/web/sessions/:sid", srv.withAuth(srv.getWebSession)) srv.DELETE("/:version/users/:user/web/sessions/:sid", srv.withAuth(srv.deleteWebSession)) - srv.GET("/:version/signuptokens/:token", srv.withAuth(srv.getSignupTokenData)) - srv.POST("/:version/signuptokens/users", srv.withAuth(srv.createUserWithToken)) - srv.POST("/:version/signuptokens", srv.withAuth(srv.createSignupToken)) + + srv.POST("/:version/usertokens", srv.withAuth(srv.createUserToken)) + srv.POST("/:version/usertokens/password", srv.withRate(srv.withAuth(srv.changePasswordWithToken))) + srv.GET("/:version/usertokens/:token", srv.withAuth(srv.getUserToken)) + srv.GET("/:version/usertokens/:token/secrets", srv.withRate(srv.withAuth(srv.rotateUserTokenSecrets))) // Servers and presence heartbeat srv.POST("/:version/namespaces/:namespace/nodes", srv.withAuth(srv.upsertNode)) @@ -215,7 +216,6 @@ func NewAPIServer(config *APIConfig) http.Handler { // U2F srv.GET("/:version/u2f/signuptokens/:token", srv.withAuth(srv.getSignupU2FRegisterRequest)) - srv.POST("/:version/u2f/users", srv.withAuth(srv.createUserWithU2FToken)) srv.POST("/:version/u2f/users/:user/sign", srv.withAuth(srv.u2fSignRequest)) srv.GET("/:version/u2f/appid", srv.withAuth(srv.getU2FAppID)) @@ -1123,6 +1123,53 @@ func (s *APIServer) getClusterCACert(auth ClientI, w http.ResponseWriter, r *htt return localCA, nil } +func (s *APIServer) rotateUserTokenSecrets(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + secrets, err := auth.RotateUserTokenSecrets(p.ByName("token")) + if err != nil { + return nil, trace.Wrap(err) + } + + return secrets, nil +} + +func (s *APIServer) getUserToken(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + usertoken, err := auth.GetUserToken(p.ByName("token")) + if err != nil { + return nil, trace.Wrap(err) + } + + return usertoken, nil +} + +func (s *APIServer) createUserToken(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + var req CreateUserTokenRequest + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + + resetToken, err := auth.CreateUserToken(req) + if err != nil { + return nil, trace.Wrap(err) + } + + return resetToken, nil +} + +func (s *APIServer) changePasswordWithToken(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { + var req ChangePasswordWithTokenRequest + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + + webSession, err := auth.ChangePasswordWithToken(req) + if err != nil { + log.Debugf("failed to change user password with token: %v", err) + return nil, trace.Wrap(err) + } + + return rawMessage(services.GetWebSessionMarshaler().MarshalWebSession(webSession, services.WithVersion(version))) +} + // getU2FAppID returns the U2F AppID in the auth configuration func (s *APIServer) getU2FAppID(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { cap, err := auth.GetAuthPreference() @@ -1226,26 +1273,6 @@ func (s *APIServer) getSession(auth ClientI, w http.ResponseWriter, r *http.Requ return se, nil } -type getSignupTokenDataResponse struct { - User string `json:"user"` - QRImg []byte `json:"qrimg"` -} - -// getSignupTokenData returns the signup data for a token. -func (s *APIServer) getSignupTokenData(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { - token := p.ByName("token") - - user, otpQRCode, err := auth.GetSignupTokenData(token) - if err != nil { - return nil, trace.Wrap(err) - } - - return &getSignupTokenDataResponse{ - User: user, - QRImg: otpQRCode, - }, nil -} - func (s *APIServer) getSignupU2FRegisterRequest(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { token := p.ByName("token") u2fRegReq, err := auth.GetSignupU2FRegisterRequest(token) @@ -1255,83 +1282,6 @@ func (s *APIServer) getSignupU2FRegisterRequest(auth ClientI, w http.ResponseWri return u2fRegReq, nil } -type createSignupTokenReq struct { - User services.UserV1 `json:"user"` - TTL time.Duration `json:"ttl"` -} - -func (s *APIServer) createSignupToken(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { - var req *createSignupTokenReq - - if err := httplib.ReadJSON(r, &req); err != nil { - return nil, trace.Wrap(err) - } - - if err := req.User.Check(); err != nil { - return nil, trace.Wrap(err) - } - - token, err := auth.CreateSignupToken(req.User, req.TTL) - if err != nil { - return nil, trace.Wrap(err) - } - - return token, nil -} - -type createUserWithTokenReq struct { - Token string `json:"token"` - Password string `json:"password"` - OTPToken string `json:"otp_token"` -} - -func (s *APIServer) createUserWithToken(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { - var req *createUserWithTokenReq - if err := httplib.ReadJSON(r, &req); err != nil { - return nil, trace.Wrap(err) - } - - cap, err := auth.GetAuthPreference() - if err != nil { - return nil, trace.Wrap(err) - } - - var webSession services.WebSession - - switch cap.GetSecondFactor() { - case teleport.OFF: - webSession, err = auth.CreateUserWithoutOTP(req.Token, req.Password) - case teleport.OTP, teleport.TOTP, teleport.HOTP: - webSession, err = auth.CreateUserWithOTP(req.Token, req.Password, req.OTPToken) - } - if err != nil { - log.Warningf("failed to create user: %v", err.Error()) - return nil, trace.Wrap(err) - } - - return rawMessage(services.GetWebSessionMarshaler().MarshalWebSession(webSession, services.WithVersion(version))) -} - -type createUserWithU2FTokenReq struct { - Token string `json:"token"` - Password string `json:"password"` - U2FRegisterResponse u2f.RegisterResponse `json:"u2f_register_response"` -} - -func (s *APIServer) createUserWithU2FToken(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) { - var req *createUserWithU2FTokenReq - if err := httplib.ReadJSON(r, &req); err != nil { - return nil, trace.Wrap(err) - } - - sess, err := auth.CreateUserWithU2FToken(req.Token, req.Password, req.U2FRegisterResponse) - if err != nil { - log.Error(err) - return nil, trace.Wrap(err) - } - return rawMessage(services.GetWebSessionMarshaler().MarshalWebSession(sess, services.WithVersion(version))) -} - type upsertOIDCConnectorRawReq struct { Connector json.RawMessage `json:"connector"` TTL time.Duration `json:"ttl"` diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 4525a7b5f6e2f..c619fc5a547e8 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -1195,7 +1195,7 @@ func (s *AuthServer) DeleteToken(token string) (err error) { } } // delete user token: - if err = s.Identity.DeleteSignupToken(token); err == nil { + if err = s.Identity.DeleteUserToken(token); err == nil { return nil } // delete node token: @@ -1222,14 +1222,14 @@ func (s *AuthServer) GetTokens(opts ...services.MarshalOption) (tokens []service tokens = append(tokens, tkns.GetStaticTokens()...) } // get user tokens: - userTokens, err := s.Identity.GetSignupTokens() + userTokens, err := s.Identity.GetUserTokens() if err != nil { return nil, trace.Wrap(err) } // convert user tokens to machine tokens: for _, t := range userTokens { roles := teleport.Roles{teleport.RoleSignup} - tok, err := services.NewProvisionToken(t.Token, roles, t.Expires) + tok, err := services.NewProvisionToken(t.GetName(), roles, t.Expiry()) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 0ac5d6cef9cb8..14d1283bec40c 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -734,13 +734,6 @@ func (a *AuthWithRoles) UpsertTOTP(user string, otpSecret string) error { return a.authServer.UpsertTOTP(user, otpSecret) } -func (a *AuthWithRoles) GetOTPData(user string) (string, []byte, error) { - if err := a.currentUserAction(user); err != nil { - return "", nil, trace.Wrap(err) - } - return a.authServer.GetOTPData(user) -} - func (a *AuthWithRoles) PreAuthenticatedSignIn(user string) (services.WebSession, error) { if err := a.currentUserAction(user); err != nil { return nil, trace.Wrap(err) @@ -1041,41 +1034,37 @@ func (a *AuthWithRoles) GenerateUserCerts(ctx context.Context, req proto.UserCer }, nil } -func (a *AuthWithRoles) CreateSignupToken(user services.UserV1, ttl time.Duration) (token string, e error) { - if err := a.action(defaults.Namespace, services.KindUser, services.VerbCreate); err != nil { - return "", trace.Wrap(err) - } - return a.authServer.CreateSignupToken(user, ttl) -} - -func (a *AuthWithRoles) GetSignupTokenData(token string) (user string, otpQRCode []byte, err error) { +func (a *AuthWithRoles) GetSignupU2FRegisterRequest(token string) (u2fRegisterRequest *u2f.RegisterRequest, e error) { // signup token are their own authz resource - return a.authServer.GetSignupTokenData(token) + return a.authServer.CreateSignupU2FRegisterRequest(token) } -func (a *AuthWithRoles) GetSignupToken(token string) (*services.SignupToken, error) { - // signup token are their own authz resource - return a.authServer.GetSignupToken(token) -} +func (a *AuthWithRoles) CreateUserToken(req CreateUserTokenRequest) (services.UserToken, error) { + if err := a.action(defaults.Namespace, services.KindUser, services.VerbUpdate); err != nil { + return nil, trace.Wrap(err) + } -func (a *AuthWithRoles) GetSignupU2FRegisterRequest(token string) (u2fRegisterRequest *u2f.RegisterRequest, e error) { - // signup token are their own authz resource - return a.authServer.CreateSignupU2FRegisterRequest(token) + a.EmitAuditEvent(events.UserTokenCreated, events.EventFields{ + events.UserTokenFor: req.Name, + events.UserTokenTTL: req.TTL, + }) + + return a.authServer.CreateUserToken(req) } -func (a *AuthWithRoles) CreateUserWithOTP(token, password, otpToken string) (services.WebSession, error) { +func (a *AuthWithRoles) GetUserToken(tokenID string) (services.UserToken, error) { // tokens are their own authz mechanism, no need to double check - return a.authServer.CreateUserWithOTP(token, password, otpToken) + return a.authServer.GetUserToken(tokenID) } -func (a *AuthWithRoles) CreateUserWithoutOTP(token string, password string) (services.WebSession, error) { +func (a *AuthWithRoles) RotateUserTokenSecrets(tokenID string) (services.UserTokenSecrets, error) { // tokens are their own authz mechanism, no need to double check - return a.authServer.CreateUserWithoutOTP(token, password) + return a.authServer.RotateUserTokenSecrets(tokenID) } -func (a *AuthWithRoles) CreateUserWithU2FToken(token string, password string, u2fRegisterResponse u2f.RegisterResponse) (services.WebSession, error) { - // signup tokens are their own authz resource - return a.authServer.CreateUserWithU2FToken(token, password, u2fRegisterResponse) +func (a *AuthWithRoles) ChangePasswordWithToken(req ChangePasswordWithTokenRequest) (services.WebSession, error) { + // token is it's own auth, no need for extra auth + return a.authServer.ChangePasswordWithToken(req) } func (a *AuthWithRoles) UpsertUser(u services.User) error { diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 1cff25788c9b6..b91f9f5b4af2a 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -1577,41 +1577,6 @@ func (c *Client) GenerateHostCert( return []byte(cert), nil } -// CreateSignupToken creates one time token for creating account for the user -// For each token it creates username and otp generator -func (c *Client) CreateSignupToken(user services.UserV1, ttl time.Duration) (string, error) { - if err := user.Check(); err != nil { - return "", trace.Wrap(err) - } - out, err := c.PostJSON(c.Endpoint("signuptokens"), createSignupTokenReq{ - User: user, - TTL: ttl, - }) - if err != nil { - return "", trace.Wrap(err) - } - var token string - if err := json.Unmarshal(out.Bytes(), &token); err != nil { - return "", trace.Wrap(err) - } - return token, nil -} - -// GetSignupTokenData returns token data for a valid token -func (c *Client) GetSignupTokenData(token string) (user string, otpQRCode []byte, e error) { - out, err := c.Get(c.Endpoint("signuptokens", token), url.Values{}) - if err != nil { - return "", nil, err - } - - var tokenData getSignupTokenDataResponse - if err := json.Unmarshal(out.Bytes(), &tokenData); err != nil { - return "", nil, err - } - - return tokenData.User, tokenData.QRImg, nil -} - // GenerateUserCerts takes the public key in the OpenSSH `authorized_keys` plain // text format, signs it using User Certificate Authority signing key and // returns the resulting certificates. @@ -1640,41 +1605,54 @@ func (c *Client) GetSignupU2FRegisterRequest(token string) (u2fRegisterRequest * return &u2fRegReq, nil } -// CreateUserWithOTP creates account with provided token and password. -// Account username and OTP key are taken from token data. -// Deletes token after account creation. -func (c *Client) CreateUserWithOTP(token, password, otpToken string) (services.WebSession, error) { - out, err := c.PostJSON(c.Endpoint("signuptokens", "users"), createUserWithTokenReq{ - Token: token, - Password: password, - OTPToken: otpToken, - }) +// GetUserToken returns user token +func (c *Client) GetUserToken(tokenID string) (services.UserToken, error) { + out, err := c.Get(c.Endpoint("usertokens", tokenID), url.Values{}) if err != nil { return nil, trace.Wrap(err) } - return services.GetWebSessionMarshaler().UnmarshalWebSession(out.Bytes()) + + token, err := services.UnmarshalUserToken(out.Bytes()) + if err != nil { + return nil, trace.Wrap(err) + } + + return token, nil } -// CreateUserWithoutOTP validates a given token creates a user -// with the given password and deletes the token afterwards. -func (c *Client) CreateUserWithoutOTP(token string, password string) (services.WebSession, error) { - out, err := c.PostJSON(c.Endpoint("signuptokens", "users"), createUserWithTokenReq{ - Token: token, - Password: password, - }) +// RotateUserTokenSecrets creates new secrets and replaces the old ones with it +func (c *Client) RotateUserTokenSecrets(tokenID string) (services.UserTokenSecrets, error) { + out, err := c.Get(c.Endpoint("usertokens", tokenID, "secrets"), url.Values{}) if err != nil { return nil, trace.Wrap(err) } - return services.GetWebSessionMarshaler().UnmarshalWebSession(out.Bytes()) + + secrets, err := services.UnmarshalUserTokenSecrets(out.Bytes()) + if err != nil { + return nil, trace.Wrap(err) + } + + return secrets, nil } -// CreateUserWithU2FToken creates user account with provided token and U2F sign response -func (c *Client) CreateUserWithU2FToken(token string, password string, u2fRegisterResponse u2f.RegisterResponse) (services.WebSession, error) { - out, err := c.PostJSON(c.Endpoint("u2f", "users"), createUserWithU2FTokenReq{ - Token: token, - Password: password, - U2FRegisterResponse: u2fRegisterResponse, - }) +// CreateUserToken creates user token +func (c *Client) CreateUserToken(req CreateUserTokenRequest) (services.UserToken, error) { + out, err := c.PostJSON(c.Endpoint("usertokens"), req) + if err != nil { + return nil, trace.Wrap(err) + } + + token, err := services.UnmarshalUserToken(out.Bytes()) + if err != nil { + return nil, trace.Wrap(err) + } + + return token, nil +} + +// ChangePasswordWithToken changes user password with usertoken +func (c *Client) ChangePasswordWithToken(req ChangePasswordWithTokenRequest) (services.WebSession, error) { + out, err := c.PostJSON(c.Endpoint("usertokens", "password"), req) if err != nil { return nil, trace.Wrap(err) } @@ -2680,9 +2658,6 @@ type IdentityService interface { // GetSignupU2FRegisterRequest generates sign request for user trying to sign up with invite token GetSignupU2FRegisterRequest(token string) (*u2f.RegisterRequest, error) - // CreateUserWithU2FToken creates user account with provided token and U2F sign response - CreateUserWithU2FToken(token string, password string, u2fRegisterResponse u2f.RegisterResponse) (services.WebSession, error) - // GetUser returns user by name GetUser(name string, withSecrets bool) (services.User, error) @@ -2701,15 +2676,6 @@ type IdentityService interface { // CheckPassword checks if the suplied web access password is valid. CheckPassword(user string, password []byte, otpToken string) error - // CreateUserWithOTP creates account with provided token and password. - // Account username and OTP key are taken from token data. - // Deletes token after account creation. - CreateUserWithOTP(token, password, otpToken string) (services.WebSession, error) - - // CreateUserWithoutOTP validates a given token creates a user - // with the given password and deletes the token afterwards. - CreateUserWithoutOTP(token string, password string) (services.WebSession, error) - // GenerateToken creates a special provisioning token for a new SSH server // that is valid for ttl period seconds. // @@ -2735,15 +2701,20 @@ type IdentityService interface { // returns the resulting certificates. GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) - // GetSignupTokenData returns token data for a valid token - GetSignupTokenData(token string) (user string, otpQRCode []byte, e error) - - // CreateSignupToken creates one time token for creating account for the user - // For each token it creates username and OTP key - CreateSignupToken(user services.UserV1, ttl time.Duration) (string, error) - // DeleteAllUsers deletes all users DeleteAllUsers() error + + // CreateUserToken creates a new user reset token + CreateUserToken(req CreateUserTokenRequest) (services.UserToken, error) + + // ChangePasswordWithToken changes password with user token + ChangePasswordWithToken(req ChangePasswordWithTokenRequest) (services.WebSession, error) + + // GetUserToken returns user token + GetUserToken(username string) (services.UserToken, error) + + // RotateUserTokenSecrets rotates user token secrets + RotateUserTokenSecrets(tokenID string) (services.UserTokenSecrets, error) } // ProvisioningService is a service in control diff --git a/lib/auth/methods.go b/lib/auth/methods.go index 9d15929d26c35..8cdc6a77078cb 100644 --- a/lib/auth/methods.go +++ b/lib/auth/methods.go @@ -184,22 +184,21 @@ func (s *AuthServer) AuthenticateWebUser(req AuthenticateUserRequest) (services. } return session, nil } + if err := s.AuthenticateUser(req); err != nil { return nil, trace.Wrap(err) } + user, err := s.GetUser(req.Username, false) if err != nil { return nil, trace.Wrap(err) } - // It's safe to extract the roles and traits directly from services.User as - // this endpoint is only used for local accounts. - sess, err := s.NewWebSession(req.Username, user.GetRoles(), user.GetTraits()) + + sess, err := s.createUserWebSession(user) if err != nil { return nil, trace.Wrap(err) } - if err := s.UpsertWebSession(req.Username, sess); err != nil { - return nil, trace.Wrap(err) - } + sess, err = services.GetWebSessionMarshaler().GenerateWebSession(sess) if err != nil { return nil, trace.Wrap(err) @@ -365,4 +364,19 @@ func (s *AuthServer) emitNoLocalAuthEvent(username string) { s.IAuditLog.EmitAuditEvent(events.AuthAttemptFailure, fields) } +func (s *AuthServer) createUserWebSession(user services.User) (services.WebSession, error) { + // It's safe to extract the roles and traits directly from services.User as this method + // is only used for local accounts. + sess, err := s.NewWebSession(user.GetName(), user.GetRoles(), user.GetTraits()) + if err != nil { + return nil, trace.Wrap(err) + } + err = s.UpsertWebSession(user.GetName(), sess) + if err != nil { + return nil, trace.Wrap(err) + } + + return sess, nil +} + const noLocalAuth = "local auth disabled" diff --git a/lib/auth/new_web_user.go b/lib/auth/new_web_user.go deleted file mode 100644 index aa5c4f2e189dc..0000000000000 --- a/lib/auth/new_web_user.go +++ /dev/null @@ -1,467 +0,0 @@ -/* -Copyright 2015 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package auth implements certificate signing authority and access control server -// Authority server is composed of several parts: -// -// * Authority server itself that implements signing and acl logic -// * HTTP server wrapper for authority server -// * HTTP client wrapper -// -package auth - -import ( - "bytes" - "image/png" - "time" - - "github.com/gravitational/teleport" - "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/events" - "github.com/gravitational/teleport/lib/services" - "github.com/gravitational/teleport/lib/utils" - - "github.com/gravitational/trace" - "github.com/pquerna/otp/totp" - - "github.com/tstranex/u2f" -) - -// CreateSignupToken creates one time token for creating account for the user -// For each token it creates username and otp generator -func (s *AuthServer) CreateSignupToken(userv1 services.UserV1, ttl time.Duration) (string, error) { - clusterConfig, err := s.GetClusterConfig() - if err != nil { - return "", trace.Wrap(err) - } - if clusterConfig.GetLocalAuth() == false { - s.emitNoLocalAuthEvent(userv1.V2().GetName()) - return "", trace.AccessDenied(noLocalAuth) - } - - user := userv1.V2() - if err := user.Check(); err != nil { - return "", trace.Wrap(err) - } - - if ttl > defaults.MaxSignupTokenTTL { - return "", trace.BadParameter("failed to invite user: maximum signup token TTL is %v hours", int(defaults.MaxSignupTokenTTL/time.Hour)) - } - - // make sure that connectors actually exist - for _, id := range user.GetOIDCIdentities() { - if err := id.Check(); err != nil { - return "", trace.Wrap(err) - } - if _, err := s.GetOIDCConnector(id.ConnectorID, false); err != nil { - return "", trace.Wrap(err) - } - } - - for _, id := range user.GetSAMLIdentities() { - if err := id.Check(); err != nil { - return "", trace.Wrap(err) - } - if _, err := s.GetSAMLConnector(id.ConnectorID, false); err != nil { - return "", trace.Wrap(err) - } - } - - // TODO(rjones): TOCTOU, instead try to create signup token for user and fail - // when unable to. - _, err = s.GetPasswordHash(user.GetName()) - if err == nil { - return "", trace.BadParameter("user '%s' already exists", user.GetName()) - } - - token, err := utils.CryptoRandomHex(TokenLenBytes) - if err != nil { - return "", trace.Wrap(err) - } - - // This OTP secret and QR code are never actually used. The OTP secret and - // QR code are rotated every time the signup link is show to the user, see - // the "GetSignupTokenData" function for details on why this is done. We - // generate a OTP token because it causes no harm and makes tests easier to - // write. - accountName := user.GetName() + "@" + s.AuthServiceName - otpKey, otpQRCode, err := s.initializeTOTP(accountName) - if err != nil { - return "", trace.Wrap(err) - } - - // create and upsert signup token - tokenData := services.SignupToken{ - Token: token, - User: userv1, - OTPKey: otpKey, - OTPQRCode: otpQRCode, - } - - if ttl == 0 || ttl > defaults.MaxSignupTokenTTL { - ttl = defaults.SignupTokenTTL - } - - err = s.UpsertSignupToken(token, tokenData, ttl) - if err != nil { - return "", trace.Wrap(err) - } - - log.Infof("[AUTH API] created the signup token for %q", user) - return token, nil -} - -// initializeTOTP creates TOTP algorithm and returns the key and QR code. -func (s *AuthServer) initializeTOTP(accountName string) (key string, qr []byte, err error) { - // create totp key - otpKey, err := totp.Generate(totp.GenerateOpts{ - Issuer: "Teleport", - AccountName: accountName, - }) - if err != nil { - return "", nil, trace.Wrap(err) - } - - // create QR code - var otpQRBuf bytes.Buffer - otpImage, err := otpKey.Image(456, 456) - if err != nil { - return "", nil, trace.Wrap(err) - } - png.Encode(&otpQRBuf, otpImage) - - return otpKey.Secret(), otpQRBuf.Bytes(), nil -} - -// rotateAndFetchSignupToken rotates the signup token everytime it's fetched. -// This ensures that an attacker that gains the signup link can not view it, -// extract the OTP key from the QR code, then allow the user to signup with -// the same OTP token. -func (s *AuthServer) rotateAndFetchSignupToken(token string) (*services.SignupToken, error) { - var err error - - // Fetch original signup token. - st, err := s.GetSignupToken(token) - if err != nil { - return nil, trace.Wrap(err) - } - - // Generate and set new OTP code for user in *services.SignupToken. - accountName := st.User.V2().GetName() + "@" + s.AuthServiceName - st.OTPKey, st.OTPQRCode, err = s.initializeTOTP(accountName) - if err != nil { - return nil, trace.Wrap(err) - } - - // Upsert token into backend. - err = s.UpsertSignupToken(token, *st, st.Expires.Sub(s.clock.Now())) - if err != nil { - return nil, trace.Wrap(err) - } - - return st, nil -} - -// GetSignupTokenData returns token data (username and QR code bytes) for a -// valid signup token. -func (s *AuthServer) GetSignupTokenData(token string) (user string, qrCode []byte, err error) { - // Rotate OTP secret before the signup data is fetched (signup page is - // rendered). This mitigates attacks where an attacker just views the signup - // link, extracts the OTP secret from the QR code, then closes the window. - // Then when the user signs up later, the attacker has access to the OTP - // secret. - st, err := s.rotateAndFetchSignupToken(token) - if err != nil { - return "", nil, trace.Wrap(err) - } - - // TODO(rjones): Remove this check and use compare and swap in the Create* - // functions below. It's a TOCTOU bug in the making: - // https://en.wikipedia.org/wiki/Time_of_check_to_time_of_use - _, err = s.GetPasswordHash(st.User.Name) - if err == nil { - return "", nil, trace.Errorf("user %q already exists", st.User.Name) - } - - return st.User.Name, st.OTPQRCode, nil -} - -func (s *AuthServer) CreateSignupU2FRegisterRequest(token string) (u2fRegisterRequest *u2f.RegisterRequest, e error) { - cap, err := s.GetAuthPreference() - if err != nil { - return nil, trace.Wrap(err) - } - - universalSecondFactor, err := cap.GetU2F() - if err != nil { - return nil, trace.Wrap(err) - } - - tokenData, err := s.GetSignupToken(token) - if err != nil { - return nil, trace.Wrap(err) - } - - _, err = s.GetPasswordHash(tokenData.User.Name) - if err == nil { - return nil, trace.AlreadyExists("user %q already exists", tokenData.User.Name) - } - - c, err := u2f.NewChallenge(universalSecondFactor.AppID, universalSecondFactor.Facets) - if err != nil { - return nil, trace.Wrap(err) - } - - request := c.RegisterRequest() - - err = s.UpsertU2FRegisterChallenge(token, c) - if err != nil { - return nil, trace.Wrap(err) - } - - return request, nil -} - -// CreateUserWithOTP creates account with provided token and password. -// Account username and hotp generator are taken from token data. -// Deletes token after account creation. -func (s *AuthServer) CreateUserWithOTP(token string, password string, otpToken string) (services.WebSession, error) { - clusterConfig, err := s.GetClusterConfig() - if err != nil { - return nil, trace.Wrap(err) - } - if clusterConfig.GetLocalAuth() == false { - s.emitNoLocalAuthEvent("") - return nil, trace.AccessDenied(noLocalAuth) - } - - tokenData, err := s.GetSignupToken(token) - if err != nil { - log.Debugf("failed to get signup token: %v", err) - return nil, trace.AccessDenied("expired or incorrect signup token") - } - - err = s.UpsertTOTP(tokenData.User.Name, tokenData.OTPKey) - if err != nil { - return nil, trace.Wrap(err) - } - - err = s.CheckOTP(tokenData.User.Name, otpToken) - if err != nil { - log.Debugf("failed to validate a token: %v", err) - return nil, trace.AccessDenied("failed to validate a token") - } - - err = s.UpsertPassword(tokenData.User.Name, []byte(password)) - if err != nil { - return nil, trace.Wrap(err) - } - - // create services.User and services.WebSession - webSession, err := s.createUserAndSession(tokenData) - if err != nil { - return nil, trace.Wrap(err) - } - - return webSession, nil -} - -// CreateUserWithoutOTP creates an account with the provided password and deletes the token afterwards. -func (s *AuthServer) CreateUserWithoutOTP(token string, password string) (services.WebSession, error) { - clusterConfig, err := s.GetClusterConfig() - if err != nil { - return nil, trace.Wrap(err) - } - if clusterConfig.GetLocalAuth() == false { - s.emitNoLocalAuthEvent("") - return nil, trace.AccessDenied(noLocalAuth) - } - - authPreference, err := s.GetAuthPreference() - if err != nil { - return nil, trace.Wrap(err) - } - if authPreference.GetSecondFactor() != teleport.OFF { - return nil, trace.AccessDenied("missing second factor") - } - tokenData, err := s.GetSignupToken(token) - if err != nil { - log.Warningf("failed to get signup token: %v", err) - return nil, trace.AccessDenied("expired or incorrect signup token") - } - - err = s.UpsertPassword(tokenData.User.Name, []byte(password)) - if err != nil { - return nil, trace.Wrap(err) - } - - // create services.User and services.WebSession - webSession, err := s.createUserAndSession(tokenData) - if err != nil { - return nil, trace.Wrap(err) - } - - return webSession, nil -} - -func (s *AuthServer) CreateUserWithU2FToken(token string, password string, response u2f.RegisterResponse) (services.WebSession, error) { - clusterConfig, err := s.GetClusterConfig() - if err != nil { - return nil, trace.Wrap(err) - } - if clusterConfig.GetLocalAuth() == false { - s.emitNoLocalAuthEvent("") - return nil, trace.AccessDenied(noLocalAuth) - } - - // before trying to create a user, see U2F is actually setup on the backend - cap, err := s.GetAuthPreference() - if err != nil { - return nil, trace.Wrap(err) - } - _, err = cap.GetU2F() - if err != nil { - return nil, trace.Wrap(err) - } - - tokenData, err := s.GetSignupToken(token) - if err != nil { - log.Warningf("failed to get signup token: %v", err) - return nil, trace.AccessDenied("expired or incorrect signup token") - } - - challenge, err := s.GetU2FRegisterChallenge(token) - if err != nil { - return nil, trace.Wrap(err) - } - - reg, err := u2f.Register(response, *challenge, &u2f.Config{SkipAttestationVerify: true}) - if err != nil { - log.Error(trace.DebugReport(err)) - return nil, trace.Wrap(err) - } - - err = s.UpsertU2FRegistration(tokenData.User.Name, reg) - if err != nil { - return nil, trace.Wrap(err) - } - err = s.UpsertU2FRegistrationCounter(tokenData.User.Name, 0) - if err != nil { - return nil, trace.Wrap(err) - } - - err = s.UpsertPassword(tokenData.User.Name, []byte(password)) - if err != nil { - return nil, trace.Wrap(err) - } - - // create services.User and services.WebSession - webSession, err := s.createUserAndSession(tokenData) - if err != nil { - return nil, trace.Wrap(err) - } - - return webSession, nil -} - -// createUserAndSession takes a signup token and creates services.User (either -// with the passed in roles, or if no role, the default role) and -// services.WebSession in the backend and returns the new services.WebSession. -func (a *AuthServer) createUserAndSession(stoken *services.SignupToken) (services.WebSession, error) { - // extract user from signup token. if no roles have been passed along, create - // user with default role. note: during the conversion from services.UserV1 - // to services.UserV2 we convert allowed logins to traits. - user := stoken.User.V2() - if len(user.GetRoles()) == 0 { - user.SetRoles([]string{teleport.AdminRoleName}) - } - - // upsert user into the backend - err := a.UpsertUser(user) - if err != nil { - return nil, trace.Wrap(err) - } - log.Infof("[AUTH] Created user: %v", user) - - // remove the token once the user has been created - err = a.DeleteSignupToken(stoken.Token) - if err != nil { - return nil, trace.Wrap(err) - } - - // It's safe to extract the roles and traits directly from services.User as - // this endpoint is only used for local accounts. - sess, err := a.NewWebSession(user.GetName(), user.GetRoles(), user.GetTraits()) - if err != nil { - return nil, trace.Wrap(err) - } - err = a.UpsertWebSession(user.GetName(), sess) - if err != nil { - return nil, trace.Wrap(err) - } - - return sess, nil -} - -func (a *AuthServer) UpsertUser(user services.User) error { - err := a.Identity.UpsertUser(user) - if err != nil { - return trace.Wrap(err) - } - - // If the user was successfully upserted, emit an event. - var connectorName string - if user.GetCreatedBy().Connector == nil { - connectorName = teleport.Local - } else { - connectorName = user.GetCreatedBy().Connector.ID - } - a.EmitAuditEvent(events.UserUpdate, events.EventFields{ - events.EventUser: user.GetName(), - events.UserExpires: user.Expiry(), - events.UserRoles: user.GetRoles(), - events.UserConnector: connectorName, - }) - - return nil -} - -func (a *AuthServer) DeleteUser(user string) error { - role, err := a.Access.GetRole(services.RoleNameForUser(user)) - if err != nil { - if !trace.IsNotFound(err) { - return trace.Wrap(err) - } - } else { - if err := a.Access.DeleteRole(role.GetName()); err != nil { - if !trace.IsNotFound(err) { - return trace.Wrap(err) - } - } - } - - err = a.Identity.DeleteUser(user) - if err != nil { - return trace.Wrap(err) - } - - // If the user was successfully deleted, emit an event. - a.EmitAuditEvent(events.UserDelete, events.EventFields{ - events.EventUser: user, - }) - - return nil -} diff --git a/lib/auth/password.go b/lib/auth/password.go index d89d69ee9bc75..fe7ffa539934c 100644 --- a/lib/auth/password.go +++ b/lib/auth/password.go @@ -10,6 +10,7 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/trace" + "github.com/tstranex/u2f" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" @@ -17,6 +18,53 @@ import ( var fakePasswordHash = []byte(`$2a$10$Yy.e6BmS2SrGbBDsyDLVkOANZmvjjMR890nUGSXFJHBXWzxe7T44m`) +// ChangePasswordWithTokenRequest defines a request to change user password +type ChangePasswordWithTokenRequest struct { + // SecondFactorToken is 2nd factor token value + SecondFactorToken string `json:"second_factor_token"` + // TokenID is this token ID + TokenID string `json:"token"` + // Password is user password + Password []byte `json:"password"` + // U2FRegisterResponse is U2F register response + U2FRegisterResponse u2f.RegisterResponse `json:"u2f_register_response"` +} + +// ChangePasswordWithToken changes password with user token +func (s *AuthServer) ChangePasswordWithToken(req ChangePasswordWithTokenRequest) (services.WebSession, error) { + user, err := s.changePasswordWithToken(req) + if err != nil { + return nil, trace.Wrap(err) + } + + sess, err := s.createUserWebSession(user) + if err != nil { + return nil, trace.Wrap(err) + } + + return sess, nil +} + +// ResetPassword resets the user password and returns the new one +func (s *AuthServer) ResetPassword(email string) (string, error) { + user, err := s.GetUser(email, false) + if err != nil { + return "", trace.Wrap(err) + } + + password, err := utils.CryptoRandomHex(defaults.ResetPasswordLength) + if err != nil { + return "", trace.Wrap(err) + } + + err = s.UpsertPassword(user.GetName(), []byte(password)) + if err != nil { + return "", trace.Wrap(err) + } + + return password, nil +} + // ChangePassword changes user passsword func (s *AuthServer) ChangePassword(req services.ChangePasswordReq) error { // validate new password @@ -165,8 +213,39 @@ func (s *AuthServer) CheckOTP(user string, otpToken string) error { return nil } +// CreateSignupU2FRegisterRequest creates U2F requests +func (s *AuthServer) CreateSignupU2FRegisterRequest(tokenID string) (u2fRegisterRequest *u2f.RegisterRequest, e error) { + cap, err := s.GetAuthPreference() + if err != nil { + return nil, trace.Wrap(err) + } + + universalSecondFactor, err := cap.GetU2F() + if err != nil { + return nil, trace.Wrap(err) + } + + _, err = s.GetUserToken(tokenID) + if err != nil { + return nil, trace.Wrap(err) + } + + c, err := u2f.NewChallenge(universalSecondFactor.AppID, universalSecondFactor.Facets) + if err != nil { + return nil, trace.Wrap(err) + } + + err = s.UpsertU2FRegisterChallenge(tokenID, c) + if err != nil { + return nil, trace.Wrap(err) + } + + request := c.RegisterRequest() + return request, nil +} + // getOTPType returns the type of OTP token used, HOTP or TOTP. -// Deprecated: Remove this method once HOTP support has been removed. +// Deprecated: Remove this method once HOTP support has been removed from Gravity. func (s *AuthServer) getOTPType(user string) (string, error) { _, err := s.GetHOTP(user) if err != nil { @@ -178,23 +257,110 @@ func (s *AuthServer) getOTPType(user string) (string, error) { return teleport.HOTP, nil } -// GetOTPData returns the OTP Key, Key URL, and the QR code. -func (s *AuthServer) GetOTPData(user string) (string, []byte, error) { - // get otp key from backend - otpSecret, err := s.GetTOTP(user) +func (s *AuthServer) changePasswordWithToken(req ChangePasswordWithTokenRequest) (services.User, error) { + clusterConfig, err := s.GetClusterConfig() + if err != nil { + return nil, trace.Wrap(err) + } + if clusterConfig.GetLocalAuth() == false { + return nil, trace.AccessDenied(noLocalAuth) + } + + err = services.VerifyPassword(req.Password) + if err != nil { + return nil, trace.Wrap(err) + } + + userToken, err := s.GetUserToken(req.TokenID) + if err != nil { + return nil, trace.Wrap(err) + } + + if userToken.Expiry().Before(s.clock.Now().UTC()) { + return nil, trace.BadParameter("expired token") + } + + username := userToken.GetUser() + err = s.changeUserSecondFactor(req, userToken) if err != nil { - return "", nil, trace.Wrap(err) + return nil, trace.Wrap(err) } - // create otp url - params := map[string][]byte{"secret": []byte(otpSecret)} - otpURL := utils.GenerateOTPURL("totp", user, params) + err = s.deleteUserTokens(username) + if err != nil { + return nil, trace.Wrap(err) + } - // create the qr code - otpQR, err := utils.GenerateQRCode(otpURL) + err = s.UpsertPassword(username, []byte(req.Password)) if err != nil { - return "", nil, trace.Wrap(err) + return nil, trace.Wrap(err) + } + + user, err := s.GetUser(username, false) + if err != nil { + return nil, trace.Wrap(err) + } + + return user, nil +} + +func (s *AuthServer) changeUserSecondFactor(req ChangePasswordWithTokenRequest, userToken services.UserToken) error { + username := userToken.GetUser() + cap, err := s.GetAuthPreference() + if err != nil { + return trace.Wrap(err) + } + + switch cap.GetSecondFactor() { + case teleport.OFF: + return nil + case teleport.OTP, teleport.TOTP, teleport.HOTP: + secrets, err := s.Identity.GetUserTokenSecrets(req.TokenID) + if err != nil { + return trace.Wrap(err) + } + + // TODO: create a separate method to validate TOTP without inserting it first + err = s.UpsertTOTP(username, secrets.GetOTPKey()) + if err != nil { + return trace.Wrap(err) + } + + err = s.CheckOTP(username, req.SecondFactorToken) + if err != nil { + return trace.Wrap(err) + } + + return nil + case teleport.U2F: + _, err = cap.GetU2F() + if err != nil { + return trace.Wrap(err) + } + + challenge, err := s.GetU2FRegisterChallenge(req.TokenID) + if err != nil { + return trace.Wrap(err) + } + + u2fRes := req.U2FRegisterResponse + reg, err := u2f.Register(u2fRes, *challenge, &u2f.Config{SkipAttestationVerify: true}) + if err != nil { + return trace.Wrap(err) + } + + err = s.UpsertU2FRegistration(username, reg) + if err != nil { + return trace.Wrap(err) + } + + err = s.UpsertU2FRegistrationCounter(username, 0) + if err != nil { + return trace.Wrap(err) + } + + return nil } - return otpURL, otpQR, nil + return trace.BadParameter("unknown second factor type %q", cap.GetSecondFactor()) } diff --git a/lib/auth/password_test.go b/lib/auth/password_test.go index 673952241de50..34c183346fd53 100644 --- a/lib/auth/password_test.go +++ b/lib/auth/password_test.go @@ -76,6 +76,14 @@ func (s *PasswordSuite) SetUpTest(c *C) { err = s.a.SetClusterName(clusterName) c.Assert(err, IsNil) + clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{ + LocalAuth: services.NewBool(true), + }) + c.Assert(err, IsNil) + + err = s.a.SetClusterConfig(clusterConfig) + c.Assert(err, IsNil) + // set static tokens staticTokens, err := services.NewStaticTokens(services.StaticTokensSpecV2{ StaticTokens: []services.ProvisionTokenV1{}, @@ -171,6 +179,162 @@ func (s *PasswordSuite) TestChangePasswordWithOTP(c *C) { c.Assert(err, IsNil) } +func (s *PasswordSuite) TestChangePasswordWithToken(c *C) { + authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ + Type: teleport.Local, + SecondFactor: teleport.OFF, + }) + c.Assert(err, IsNil) + + err = s.a.SetAuthPreference(authPreference) + c.Assert(err, IsNil) + + username := "joe@example.com" + password := []byte("qweqweqwe") + _, _, err = CreateUserAndRole(s.a, username, []string{username}) + c.Assert(err, IsNil) + + resetToken, err := s.a.CreateUserToken(CreateUserTokenRequest{ + Name: username, + }) + c.Assert(err, IsNil) + + _, err = s.a.changePasswordWithToken(ChangePasswordWithTokenRequest{ + TokenID: resetToken.GetName(), + Password: password, + }) + c.Assert(err, IsNil) + + // password should be updated + err = s.a.CheckPasswordWOToken(username, password) + c.Assert(err, IsNil) +} + +func (s *PasswordSuite) TestChangePasswordWithTokenOTP(c *C) { + authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ + Type: teleport.Local, + SecondFactor: teleport.OTP, + }) + c.Assert(err, IsNil) + + err = s.a.SetAuthPreference(authPreference) + c.Assert(err, IsNil) + + username := "joe@example.com" + password := []byte("qweqweqwe") + _, _, err = CreateUserAndRole(s.a, username, []string{username}) + c.Assert(err, IsNil) + + userToken, err := s.a.CreateUserToken(CreateUserTokenRequest{ + Name: username, + }) + + secrets, err := s.a.RotateUserTokenSecrets(userToken.GetName()) + c.Assert(err, IsNil) + + otpToken, err := totp.GenerateCode(secrets.GetOTPKey(), s.bk.Clock().Now()) + c.Assert(err, IsNil) + + _, err = s.a.changePasswordWithToken(ChangePasswordWithTokenRequest{ + TokenID: userToken.GetName(), + Password: password, + SecondFactorToken: otpToken, + }) + c.Assert(err, IsNil) + + err = s.a.CheckPasswordWOToken(username, password) + c.Assert(err, IsNil) +} + +func (s *PasswordSuite) TestChangePasswordWithTokenErrors(c *C) { + authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ + Type: teleport.Local, + SecondFactor: teleport.OTP, + }) + c.Assert(err, IsNil) + + username := "joe@example.com" + _, _, err = CreateUserAndRole(s.a, username, []string{username}) + c.Assert(err, IsNil) + + userToken, err := s.a.CreateUserToken(CreateUserTokenRequest{ + Name: username, + }) + c.Assert(err, IsNil) + + validPassword := []byte("qweQWE1") + validTokenID := userToken.GetName() + + type testCase struct { + desc string + secondFactor string + req ChangePasswordWithTokenRequest + } + + testCases := []testCase{ + { + secondFactor: teleport.OFF, + desc: "invalid tokenID value", + req: ChangePasswordWithTokenRequest{ + TokenID: "what_token", + Password: validPassword, + }, + }, + { + secondFactor: teleport.OFF, + desc: "invalid password", + req: ChangePasswordWithTokenRequest{ + TokenID: validTokenID, + Password: []byte("short"), + }, + }, + { + secondFactor: teleport.OTP, + desc: "missing second factor", + req: ChangePasswordWithTokenRequest{ + TokenID: validTokenID, + Password: validPassword, + }, + }, + { + secondFactor: teleport.OTP, + desc: "invalid OTP value", + req: ChangePasswordWithTokenRequest{ + TokenID: validTokenID, + Password: validPassword, + SecondFactorToken: "invalid", + }, + }, + } + + for _, tc := range testCases { + // set new auth preference settings + authPreference.SetSecondFactor(tc.secondFactor) + err = s.a.SetAuthPreference(authPreference) + c.Assert(err, IsNil) + + _, err = s.a.changePasswordWithToken(tc.req) + c.Assert(err, NotNil, Commentf("test case %q", tc.desc)) + } + + authPreference.SetSecondFactor(teleport.OFF) + err = s.a.SetAuthPreference(authPreference) + c.Assert(err, IsNil) + + _, err = s.a.changePasswordWithToken(ChangePasswordWithTokenRequest{ + TokenID: validTokenID, + Password: validPassword, + }) + c.Assert(err, IsNil) + + // invite token cannot be reused + _, err = s.a.changePasswordWithToken(ChangePasswordWithTokenRequest{ + TokenID: validTokenID, + Password: validPassword, + }) + c.Assert(err, NotNil) +} + func (s *PasswordSuite) shouldLockAfterFailedAttempts(c *C, req services.ChangePasswordReq) { loginAttempts, _ := s.a.GetUserLoginAttempts(req.User) c.Assert(len(loginAttempts), Equals, 0) diff --git a/lib/auth/tls_test.go b/lib/auth/tls_test.go index 9f3576cf7bbd9..3c8217662ba4c 100644 --- a/lib/auth/tls_test.go +++ b/lib/auth/tls_test.go @@ -27,7 +27,6 @@ import ( "fmt" "io" "io/ioutil" - "net/url" "os" "path/filepath" "testing" @@ -50,7 +49,6 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/pquerna/otp/totp" - "github.com/tstranex/u2f" "gopkg.in/check.v1" ) @@ -1206,13 +1204,6 @@ func (s *TLSSuite) TestOTPCRUD(c *check.C) { err = s.server.Auth().UpsertTOTP(user, otpSecret) c.Assert(err, check.IsNil) - // make sure the otp url we get back is valid url issued to the correct user - otpURL, _, err := s.server.Auth().GetOTPData(user) - c.Assert(err, check.IsNil) - u, err := url.Parse(otpURL) - c.Assert(err, check.IsNil) - c.Assert(u.Path, check.Equals, "/user1") - // a completely invalid token should return access denied err = clt.CheckPassword("user1", pass, "123456") c.Assert(err, check.NotNil) @@ -1765,14 +1756,6 @@ func (s *TLSSuite) TestAuthenticateWebUserOTP(c *check.C) { err = s.server.Auth().UpsertTOTP(user, otpSecret) c.Assert(err, check.IsNil) - otpURL, _, err := s.server.Auth().GetOTPData(user) - c.Assert(err, check.IsNil) - - // make sure label in url is correct - u, err := url.Parse(otpURL) - c.Assert(err, check.IsNil) - c.Assert(u.Path, check.Equals, "/ws-test") - // create a valid otp token validToken, err := totp.GenerateCode(otpSecret, s.server.Clock().Now()) c.Assert(err, check.IsNil) @@ -1831,68 +1814,6 @@ func (s *TLSSuite) TestAuthenticateWebUserOTP(c *check.C) { c.Assert(err, check.NotNil) } -// TestTokenSignupFlow tests signup flow using invite token -func (s *TLSSuite) TestTokenSignupFlow(c *check.C) { - authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ - Type: teleport.Local, - SecondFactor: teleport.OTP, - }) - c.Assert(err, check.IsNil) - err = s.server.Auth().SetAuthPreference(authPreference) - c.Assert(err, check.IsNil) - - user := "foobar" - mappings := []string{"admin", "db"} - - token, err := s.server.Auth().CreateSignupToken(services.UserV1{Name: user, AllowedLogins: mappings}, 0) - c.Assert(err, check.IsNil) - - proxy, err := s.server.NewClient(TestBuiltin(teleport.RoleProxy)) - c.Assert(err, check.IsNil) - - // invalid token - _, _, err = proxy.GetSignupTokenData("bad_token_data") - c.Assert(err, check.NotNil) - - // valid token - success - _, _, err = proxy.GetSignupTokenData(token) - c.Assert(err, check.IsNil) - - signupToken, err := s.server.Auth().GetSignupToken(token) - c.Assert(err, check.IsNil) - - otpToken, err := totp.GenerateCode(signupToken.OTPKey, s.server.Clock().Now()) - c.Assert(err, check.IsNil) - - // valid token, but missing second factor - newPassword := "abc123" - _, err = proxy.CreateUserWithoutOTP(token, newPassword) - fixtures.ExpectAccessDenied(c, err) - - // invalid signup token - _, err = proxy.CreateUserWithOTP("what_token?", newPassword, otpToken) - fixtures.ExpectAccessDenied(c, err) - - // valid signup token, invalid otp token - _, err = proxy.CreateUserWithOTP(token, newPassword, "badotp") - fixtures.ExpectAccessDenied(c, err) - - // success - ws, err := proxy.CreateUserWithOTP(token, newPassword, otpToken) - c.Assert(err, check.IsNil) - - // attempt to reuse token fails - _, err = proxy.CreateUserWithOTP(token, newPassword, otpToken) - fixtures.ExpectAccessDenied(c, err) - - // can login with web session credentials now - userClient, err := s.server.NewClientFromWebSession(ws) - c.Assert(err, check.IsNil) - - _, err = userClient.GetWebSessionInfo(user, ws.GetName()) - c.Assert(err, check.IsNil) -} - // TestLoginAttempts makes sure the login attempt counter is incremented and // reset correctly. func (s *TLSSuite) TestLoginAttempts(c *check.C) { @@ -1937,46 +1858,50 @@ func (s *TLSSuite) TestLoginAttempts(c *check.C) { c.Assert(loginAttempts, check.HasLen, 0) } -// TestSignupNoLocalAuth make sure that signup tokens can not be created -// when local auth is disabled. -func (s *TLSSuite) TestSignupNoLocalAuth(c *check.C) { - // Set services.ClusterConfig to disallow local auth. +func (s *TLSSuite) TestChangePasswordWithToken(c *check.C) { clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{ - LocalAuth: services.NewBool(false), + LocalAuth: services.NewBool(true), }) c.Assert(err, check.IsNil) + err = s.server.Auth().SetClusterConfig(clusterConfig) c.Assert(err, check.IsNil) - // Make sure access is denied when trying to create a signup token. - _, err = s.server.Auth().CreateSignupToken(services.UserV1{ - Name: "foo", - AllowedLogins: []string{"admin"}, - }, backend.Forever) - fixtures.ExpectAccessDenied(c, err) -} + authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ + Type: teleport.Local, + SecondFactor: teleport.OTP, + }) + c.Assert(err, check.IsNil) -// TestCreateUserNoLocalAuth make sure that local users can not be created -// when local auth is disabled. -func (s *TLSSuite) TestCreateUserNoLocalAuth(c *check.C) { - // Set services.ClusterConfig to disallow local auth. - clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{ - LocalAuth: services.NewBool(false), + err = s.server.Auth().SetAuthPreference(authPreference) + c.Assert(err, check.IsNil) + + username := "user1" + // Create a local user. + clt, err := s.server.NewClient(TestAdmin()) + c.Assert(err, check.IsNil) + + _, _, err = CreateUserAndRole(clt, username, []string{"role1"}) + c.Assert(err, check.IsNil) + + token, err := s.server.Auth().CreateUserToken(CreateUserTokenRequest{ + Name: username, + TTL: time.Hour, }) c.Assert(err, check.IsNil) - err = s.server.Auth().SetClusterConfig(clusterConfig) + + secrets, err := s.server.Auth().RotateUserTokenSecrets(token.GetName()) c.Assert(err, check.IsNil) - // Make sure access is denied when trying to create a user. - _, err = s.server.Auth().CreateUserWithoutOTP("foo", "bar") - fixtures.ExpectAccessDenied(c, err) - _, err = s.server.Auth().CreateUserWithOTP("foo", "bar", "123456") - fixtures.ExpectAccessDenied(c, err) - _, err = s.server.Auth().CreateUserWithU2FToken("foo", "bar", u2f.RegisterResponse{ - ClientData: "baz", - RegistrationData: "qux", + otpToken, err := totp.GenerateCode(secrets.GetOTPKey(), s.server.Clock().Now()) + c.Assert(err, check.IsNil) + + _, err = s.server.Auth().ChangePasswordWithToken(ChangePasswordWithTokenRequest{ + TokenID: token.GetName(), + Password: []byte("qweqweqwe"), + SecondFactorToken: otpToken, }) - fixtures.ExpectAccessDenied(c, err) + c.Assert(err, check.IsNil) } // TestLoginNoLocalAuth makes sure that logins for local accounts can not be diff --git a/lib/auth/user.go b/lib/auth/user.go new file mode 100644 index 0000000000000..0ff678fe48037 --- /dev/null +++ b/lib/auth/user.go @@ -0,0 +1,84 @@ +/* +Copyright 2015 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package auth implements certificate signing authority and access control server +// Authority server is composed of several parts: +// +// * Authority server itself that implements signing and acl logic +// * HTTP server wrapper for authority server +// * HTTP client wrapper +// +package auth + +import ( + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/events" + "github.com/gravitational/teleport/lib/services" + + "github.com/gravitational/trace" +) + +// UpsertUser upserts user +func (s *AuthServer) UpsertUser(user services.User) error { + err := s.Identity.UpsertUser(user) + if err != nil { + return trace.Wrap(err) + } + + // If the user was successfully upserted, emit an event. + var connectorName string + if user.GetCreatedBy().Connector == nil { + connectorName = teleport.Local + } else { + connectorName = user.GetCreatedBy().Connector.ID + } + s.EmitAuditEvent(events.UserUpdate, events.EventFields{ + events.EventUser: user.GetName(), + events.UserExpires: user.Expiry(), + events.UserRoles: user.GetRoles(), + events.UserConnector: connectorName, + }) + + return nil +} + +// DeleteUser deletes user +func (s *AuthServer) DeleteUser(user string) error { + role, err := s.Access.GetRole(services.RoleNameForUser(user)) + if err != nil { + if !trace.IsNotFound(err) { + return trace.Wrap(err) + } + } else { + if err := s.Access.DeleteRole(role.GetName()); err != nil { + if !trace.IsNotFound(err) { + return trace.Wrap(err) + } + } + } + + err = s.Identity.DeleteUser(user) + if err != nil { + return trace.Wrap(err) + } + + // If the user was successfully deleted, emit an event. + s.EmitAuditEvent(events.UserDelete, events.EventFields{ + events.EventUser: user, + }) + + return nil +} diff --git a/lib/auth/usertoken.go b/lib/auth/usertoken.go new file mode 100644 index 0000000000000..9c269aa62a203 --- /dev/null +++ b/lib/auth/usertoken.go @@ -0,0 +1,251 @@ +/* +Copyright 2017-2020 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "bytes" + "fmt" + "image/png" + "net/url" + "time" + + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/utils" + "github.com/pquerna/otp/totp" + + "github.com/gravitational/trace" +) + +const ( + // UserTokenTypeInvite indicates invite UI flow + UserTokenTypeInvite = "invite" + // UserTokenTypePasswordChange indicates change password UI flow + UserTokenTypePasswordChange = "reset-password" +) + +// CreateUserTokenRequest is a request to create a new user token +type CreateUserTokenRequest struct { + // Name is the user name to reset. + Name string `json:"name"` + // TTL specifies how long the generated reset token is valid for. + TTL time.Duration `json:"ttl"` + // Type is user token type. + Type string `json:"type"` +} + +// CheckAndSetDefaults checks and sets the defaults +func (r *CreateUserTokenRequest) CheckAndSetDefaults() error { + if r.Name == "" { + return trace.BadParameter("user name can't be empty") + } + if r.TTL < 0 { + return trace.BadParameter("ttl can't be negative") + } + + if r.Type == "" { + r.Type = UserTokenTypePasswordChange + } + + // We use the same mechanism to handle invites and password resets + // as both allow setting up a new password based on auth preferences. + // The only difference is default TTL values and URLs to web UI. + switch r.Type { + case UserTokenTypeInvite: + if r.TTL == 0 { + r.TTL = defaults.SignupTokenTTL + } + + if r.TTL > defaults.MaxSignupTokenTTL { + return trace.BadParameter( + "failed to create user invite token: maximum token TTL is %v hours", + defaults.MaxSignupTokenTTL) + } + case UserTokenTypePasswordChange: + if r.TTL == 0 { + r.TTL = defaults.ChangePasswordTokenTTL + } + if r.TTL > defaults.MaxChangePasswordTokenTTL { + return trace.BadParameter( + "failed to create reset password token: maximum token TTL is %v hours", + defaults.MaxChangePasswordTokenTTL) + } + default: + return trace.BadParameter("unknown user token request type(%v)", r.Type) + } + + return nil +} + +// CreateUserToken creates user token +func (s *AuthServer) CreateUserToken(req CreateUserTokenRequest) (services.UserToken, error) { + err := req.CheckAndSetDefaults() + if err != nil { + return nil, trace.Wrap(err) + } + + _, err = s.GetUser(req.Name, false) + if err != nil { + return nil, trace.Wrap(err) + } + + // TODO: check if some users cannot be reset + _, err = s.ResetPassword(req.Name) + if err != nil { + return nil, trace.Wrap(err) + } + + token, err := s.newUserToken(req) + if err != nil { + return nil, trace.Wrap(err) + } + + // remove any other invite tokens for this user + err = s.deleteUserTokens(req.Name) + if err != nil { + return nil, trace.Wrap(err) + } + + _, err = s.Identity.CreateUserToken(token) + if err != nil { + return nil, trace.Wrap(err) + } + + return s.GetUserToken(token.GetName()) +} + +// RotateUserTokenSecrets rotates user token secrets +func (s *AuthServer) RotateUserTokenSecrets(tokenID string) (services.UserTokenSecrets, error) { + userToken, err := s.GetUserToken(tokenID) + if err != nil { + return nil, trace.Wrap(err) + } + + key, qr, err := newTOTPKeys("Teleport", userToken.GetUser()+"@"+s.AuthServiceName) + if err != nil { + return nil, trace.Wrap(err) + } + + secrets, err := services.NewUserTokenSecrets(tokenID) + if err != nil { + return nil, trace.Wrap(err) + } + + secrets.Spec.OTPKey = key + secrets.Spec.QRCode = string(qr) + err = s.UpsertUserTokenSecrets(&secrets) + if err != nil { + return nil, trace.Wrap(err) + } + + return &secrets, nil +} + +func (s *AuthServer) newUserToken(req CreateUserTokenRequest) (services.UserToken, error) { + tokenID, err := utils.CryptoRandomHex(TokenLenBytes) + if err != nil { + return nil, trace.Wrap(err) + } + + url, err := formatUserTokenURL(s.publicURL(), tokenID, req.Type) + if err != nil { + return nil, trace.Wrap(err) + } + + userToken := services.NewUserToken(tokenID) + userToken.Metadata.SetExpiry(s.clock.Now().UTC().Add(req.TTL)) + userToken.Spec.User = req.Name + userToken.Spec.Created = s.clock.Now().UTC() + userToken.Spec.URL = url + return &userToken, nil +} + +func (s *AuthServer) publicURL() string { + proxyHost := ":3080" + proxies, err := s.GetProxies() + if err != nil { + log.Errorf("Unable to retrieve proxy list: %v", err) + } + + if len(proxies) > 0 { + proxyHost = proxies[0].GetPublicAddr() + if proxyHost == "" { + proxyHost = fmt.Sprintf("%v:%v", proxies[0].GetHostname(), defaults.HTTPListenPort) + log.Debugf("public_address not set for proxy, returning proxyHost: %q", proxyHost) + } + } + + return fmt.Sprintf("https://" + proxyHost) +} + +func formatUserTokenURL(advertiseURL string, tokenID string, reqType string) (string, error) { + u, err := url.Parse(advertiseURL) + if err != nil { + return "", trace.Wrap(err) + } + + u.RawQuery = "" + // We have 2 differen UI flows to process user tokens + if reqType == UserTokenTypeInvite { + u.Path = fmt.Sprintf("/web/invite/%v", tokenID) + } else if reqType == UserTokenTypePasswordChange { + u.Path = fmt.Sprintf("/web/reset/%v", tokenID) + } + + return u.String(), nil +} + +func (s *AuthServer) deleteUserTokens(username string) error { + userTokens, err := s.GetUserTokens() + if err != nil { + return trace.Wrap(err) + } + + for _, token := range userTokens { + if token.GetUser() != username { + continue + } + + err = s.DeleteUserToken(token.GetName()) + if err != nil { + return trace.Wrap(err) + } + } + + return nil +} + +func newTOTPKeys(issuer string, accountName string) (key string, qr []byte, err error) { + // create totp key + otpKey, err := totp.Generate(totp.GenerateOpts{ + Issuer: issuer, + AccountName: accountName, + }) + if err != nil { + return "", nil, trace.Wrap(err) + } + + // create QR code + var otpQRBuf bytes.Buffer + otpImage, err := otpKey.Image(456, 456) + if err != nil { + return "", nil, trace.Wrap(err) + } + png.Encode(&otpQRBuf, otpImage) + + return otpKey.Secret(), otpQRBuf.Bytes(), nil +} diff --git a/lib/auth/usertoken_test.go b/lib/auth/usertoken_test.go new file mode 100644 index 0000000000000..82d88408ad84b --- /dev/null +++ b/lib/auth/usertoken_test.go @@ -0,0 +1,168 @@ +/* +Copyright 2020 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "context" + "fmt" + "time" + + authority "github.com/gravitational/teleport/lib/auth/testauthority" + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/backend/lite" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/utils" + + . "gopkg.in/check.v1" +) + +type UserTokenTest struct { + bk backend.Backend + a *AuthServer +} + +var _ = fmt.Printf +var _ = Suite(&UserTokenTest{}) + +func (s *UserTokenTest) SetUpSuite(c *C) { + utils.InitLoggerForTests() +} + +func (s *UserTokenTest) TearDownSuite(c *C) { +} + +func (s *UserTokenTest) SetUpTest(c *C) { + var err error + c.Assert(err, IsNil) + s.bk, err = lite.New(context.TODO(), backend.Params{"path": c.MkDir()}) + c.Assert(err, IsNil) + + // set cluster name + clusterName, err := services.NewClusterName(services.ClusterNameSpecV2{ + ClusterName: "me.localhost", + }) + c.Assert(err, IsNil) + authConfig := &InitConfig{ + ClusterName: clusterName, + Backend: s.bk, + Authority: authority.New(), + SkipPeriodicOperations: true, + } + s.a, err = NewAuthServer(authConfig) + c.Assert(err, IsNil) + + err = s.a.SetClusterName(clusterName) + c.Assert(err, IsNil) + + // Set services.ClusterConfig to disallow local auth. + clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{ + LocalAuth: services.NewBool(true), + }) + c.Assert(err, IsNil) + + err = s.a.SetClusterConfig(clusterConfig) + c.Assert(err, IsNil) +} + +func (s *UserTokenTest) TearDownTest(c *C) { +} + +func (s *UserTokenTest) TestCreateUserToken(c *C) { + username := "joe@example.com" + pass := "pass123" + _, _, err := CreateUserAndRole(s.a, username, []string{username}) + c.Assert(err, IsNil) + + req := CreateUserTokenRequest{ + Name: username, + TTL: time.Hour, + } + + token, err := s.a.CreateUserToken(req) + c.Assert(err, IsNil) + c.Assert(token.GetUser(), Equals, username) + c.Assert(token.GetURL(), Equals, "https://:3080/web/reset/"+token.GetName()) + + // verify that password was reset + err = s.a.CheckPasswordWOToken(username, []byte(pass)) + c.Assert(err, NotNil) + + // create another reset token for the same user + token, err = s.a.CreateUserToken(req) + c.Assert(err, IsNil) + + // previous token must be deleted + tokens, err := s.a.GetUserTokens() + c.Assert(err, IsNil) + c.Assert(len(tokens), Equals, 1) + c.Assert(tokens[0].GetName(), Equals, token.GetName()) +} + +func (s *UserTokenTest) TestCreateUserTokenErrors(c *C) { + username := "joe@example.com" + _, _, err := CreateUserAndRole(s.a, username, []string{username}) + c.Assert(err, IsNil) + + type testCase struct { + desc string + req CreateUserTokenRequest + } + + testCases := []testCase{ + { + desc: "Reset Password: TTL < 0", + req: CreateUserTokenRequest{ + Name: username, + TTL: -1, + }, + }, + { + desc: "Reset Password: TTL > max", + req: CreateUserTokenRequest{ + Name: username, + TTL: defaults.MaxChangePasswordTokenTTL + time.Hour, + }, + }, + { + desc: "Reset Password: empty user name", + req: CreateUserTokenRequest{ + TTL: time.Hour, + }, + }, + { + desc: "Reset Password: user does not exist", + req: CreateUserTokenRequest{ + Name: "doesnotexist@example.com", + TTL: time.Hour, + }, + }, + { + desc: "Invite: TTL > max", + req: CreateUserTokenRequest{ + Name: username, + TTL: defaults.MaxSignupTokenTTL + time.Hour, + Type: UserTokenTypeInvite, + }, + }, + } + + for _, tc := range testCases { + _, err := s.a.CreateUserToken(tc.req) + c.Assert(err, NotNil, Commentf("test case %q", tc.desc)) + } +} diff --git a/lib/defaults/defaults.go b/lib/defaults/defaults.go index a23ff1661a47c..7b2549e812ae9 100644 --- a/lib/defaults/defaults.go +++ b/lib/defaults/defaults.go @@ -130,6 +130,15 @@ const ( // clients can reduce this time, not increase it MaxSignupTokenTTL = 48 * time.Hour + // MaxChangePasswordTokenTTL is a maximum TTL for password change token + MaxChangePasswordTokenTTL = 24 * time.Hour + + // ChangePasswordTokenTTL is a default password change token expiry time + ChangePasswordTokenTTL = 8 * time.Hour + + // ResetPasswordLength is the length of the reset user password + ResetPasswordLength = 10 + // ProvisioningTokenTTL is a the default TTL for server provisioning // tokens. When a user generates a token without an explicit TTL, this // value is used. diff --git a/lib/events/api.go b/lib/events/api.go index 42fb898ca8577..a9622ee17915f 100644 --- a/lib/events/api.go +++ b/lib/events/api.go @@ -173,6 +173,13 @@ const ( // AccessRequestID is the ID of an access request. AccessRequestID = "id" + // UserTokenCreateEvent is emitted when a new user token created. + UserTokenCreateEvent + // UserTokenTTL is TTL of user token. + UserTokenTTL = "ttl" + // UserTokenFor is a user name + UserTokenFor = "for" + // ExecEvent is an exec command executed by script or user on // the server side ExecEvent = "exec" diff --git a/lib/events/codes.go b/lib/events/codes.go index c2969548772e2..5ff3f609b2ad4 100644 --- a/lib/events/codes.go +++ b/lib/events/codes.go @@ -176,6 +176,11 @@ var ( Name: SessionNetworkEvent, Code: SessionNetworkCode, } + // UserTokenCreated is emitted when user token is created. + UserTokenCreated = Event{ + Name: UserTokenCreateEvent, + Code: UserTokenCreateCode, + } ) var ( @@ -239,4 +244,6 @@ var ( AccessRequestCreateCode = "T5000I" // AccessRequestUpdateCode is the access request state update code. AccessRequestUpdateCode = "T5001I" + // UserTokenCreateCode is the user token create event code. + UserTokenCreateCode = "T6000I" ) diff --git a/lib/services/identity.go b/lib/services/identity.go index 564617f819a3d..2a86326f10cdb 100644 --- a/lib/services/identity.go +++ b/lib/services/identity.go @@ -124,18 +124,6 @@ type Identity interface { // UpsertPassword upserts new password and OTP token UpsertPassword(user string, password []byte) error - // UpsertSignupToken upserts signup token - one time token that lets user to create a user account - UpsertSignupToken(token string, tokenData SignupToken, ttl time.Duration) error - - // GetSignupToken returns signup token data - GetSignupToken(token string) (*SignupToken, error) - - // GetSignupTokens returns a list of signup tokens - GetSignupTokens() ([]SignupToken, error) - - // DeleteSignupToken deletes signup token from the storage - DeleteSignupToken(token string) error - // UpsertU2FRegisterChallenge upserts a U2F challenge for a new user corresponding to the token UpsertU2FRegisterChallenge(token string, u2fChallenge *u2f.Challenge) error @@ -201,18 +189,42 @@ type Identity interface { // CreateGithubConnector creates a new Github connector CreateGithubConnector(connector GithubConnector) error + // UpsertGithubConnector creates or updates a new Github connector UpsertGithubConnector(connector GithubConnector) error + // GetGithubConnectors returns all configured Github connectors GetGithubConnectors(withSecrets bool) ([]GithubConnector, error) + // GetGithubConnector returns a Github connector by its name GetGithubConnector(name string, withSecrets bool) (GithubConnector, error) + // DeleteGithubConnector deletes a Github connector by its name DeleteGithubConnector(name string) error + // CreateGithubAuthRequest creates a new auth request for Github OAuth2 flow CreateGithubAuthRequest(req GithubAuthRequest) error + // GetGithubAuthRequest retrieves Github auth request by the token GetGithubAuthRequest(stateToken string) (*GithubAuthRequest, error) + + // DeleteUserToken deletes user token + CreateUserToken(usertoken UserToken) (UserToken, error) + + // DeleteUserToken deletes user token + DeleteUserToken(tokenID string) error + + // GetUserTokens returns user tokens + GetUserTokens() ([]UserToken, error) + + // GetUserToken returns user token + GetUserToken(tokenID string) (UserToken, error) + + // UpsertUserTokenSecrets upserts token secrets + UpsertUserTokenSecrets(secrets UserTokenSecrets) error + + // GetUserTokenSecrets returns user token secrets + GetUserTokenSecrets(tokenID string) (UserTokenSecrets, error) } // VerifyPassword makes sure password satisfies our requirements (relaxed), @@ -229,16 +241,6 @@ func VerifyPassword(password []byte) error { return nil } -// SignupToken stores metadata about user signup token -// is stored and generated when tctl add user is executed -type SignupToken struct { - Token string `json:"token"` - User UserV1 `json:"user"` - OTPKey string `json:"otp_key"` - OTPQRCode []byte `json:"otp_qr_code"` - Expires time.Time `json:"expires"` -} - const ExternalIdentitySchema = `{ "type": "object", "additionalProperties": false, @@ -308,7 +310,7 @@ func (r *GithubAuthRequest) SetExpiry(expires time.Time) { r.Expires = &expires } -// Expires returns object expiry setting. +// Expiry returns object expiry setting. func (r *GithubAuthRequest) Expiry() time.Time { if r.Expires == nil { return time.Time{} diff --git a/lib/services/invite.go b/lib/services/invite.go deleted file mode 100644 index ed803b5ccea56..0000000000000 --- a/lib/services/invite.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2019 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package services - -import ( - "time" -) - -// InviteTokenV3 is an invite token spec format V3 -type InviteTokenV3 struct { - // Kind is a resource kind - always resource. - Kind string `json:"kind"` - - // SubKind is a resource sub kind - SubKind string `json:"sub_kind,omitempty"` - - // Version is a resource version. - Version string `json:"version"` - - // Metadata is metadata about the resource. - Metadata Metadata `json:"metadata"` - - // Spec is a spec of the invite token - Spec InviteTokenSpecV3 `json:"spec"` -} - -// InviteTokenSpecV3 is a spec for invite token -type InviteTokenSpecV3 struct { - // URL is a helper invite token URL - URL string `json:"url"` -} - -// NewInviteToken returns a new instance of the invite token -func NewInviteToken(token, signupURL string, expires time.Time) *InviteTokenV3 { - tok := InviteTokenV3{ - Kind: KindInviteToken, - Version: V3, - Metadata: Metadata{ - Name: token, - }, - Spec: InviteTokenSpecV3{ - URL: signupURL, - }, - } - if !expires.IsZero() { - tok.Metadata.SetExpiry(expires) - } - return &tok -} diff --git a/lib/services/local/users.go b/lib/services/local/users.go index b16167a2f25f8..721f90de7df5d 100644 --- a/lib/services/local/users.go +++ b/lib/services/local/users.go @@ -414,7 +414,7 @@ func (s *IdentityService) GetTOTP(user string) (string, error) { item, err := s.Get(context.TODO(), backend.Key(webPrefix, usersPrefix, user, totpPrefix)) if err != nil { if trace.IsNotFound(err) { - return "", trace.NotFound("user %q is not found", user) + return "", trace.NotFound("OTP key for user(%q) is not found", user) } return "", trace.Wrap(err) } @@ -593,74 +593,6 @@ func (s *IdentityService) UpsertPassword(user string, password []byte) error { return nil } -// UpsertSignupToken upserts signup token - one time token that lets user to create a user account -func (s *IdentityService) UpsertSignupToken(token string, tokenData services.SignupToken, ttl time.Duration) error { - if ttl < time.Second || ttl > defaults.MaxSignupTokenTTL { - ttl = defaults.MaxSignupTokenTTL - } - tokenData.Expires = time.Now().UTC().Add(ttl) - value, err := json.Marshal(tokenData) - if err != nil { - return trace.Wrap(err) - } - item := backend.Item{ - Key: backend.Key(userTokensPrefix, token), - Value: value, - Expires: tokenData.Expires, - } - _, err = s.Put(context.TODO(), item) - if err != nil { - return trace.Wrap(err) - } - return nil - -} - -// GetSignupToken returns signup token data -func (s *IdentityService) GetSignupToken(token string) (*services.SignupToken, error) { - if token == "" { - return nil, trace.BadParameter("missing token") - } - item, err := s.Get(context.TODO(), backend.Key(userTokensPrefix, token)) - if err != nil { - return nil, trace.Wrap(err) - } - var signupToken services.SignupToken - err = json.Unmarshal(item.Value, &signupToken) - if err != nil { - return nil, trace.Wrap(err) - } - return &signupToken, nil -} - -// GetSignupTokens returns all non-expired user tokens -func (s *IdentityService) GetSignupTokens() ([]services.SignupToken, error) { - startKey := backend.Key(userTokensPrefix) - result, err := s.GetRange(context.TODO(), startKey, backend.RangeEnd(startKey), backend.NoLimit) - if err != nil { - return nil, trace.Wrap(err) - } - tokens := make([]services.SignupToken, len(result.Items)) - for i, item := range result.Items { - var signupToken services.SignupToken - err = json.Unmarshal(item.Value, &signupToken) - if err != nil { - return nil, trace.Wrap(err) - } - tokens[i] = signupToken - } - return tokens, nil -} - -// DeleteSignupToken deletes signup token from the storage -func (s *IdentityService) DeleteSignupToken(token string) error { - if token == "" { - return trace.BadParameter("missing parameter token") - } - err := s.Delete(context.TODO(), backend.Key(userTokensPrefix, token)) - return trace.Wrap(err) -} - func (s *IdentityService) UpsertU2FRegisterChallenge(token string, u2fChallenge *u2f.Challenge) error { if token == "" { return trace.BadParameter("missing parmeter token") @@ -1243,7 +1175,6 @@ const ( samlPrefix = "saml" githubPrefix = "github" requestsPrefix = "requests" - userTokensPrefix = "addusertokens" u2fRegChalPrefix = "adduseru2fchallenges" usedTOTPPrefix = "used_totp" usedTOTPTTL = 30 * time.Second diff --git a/lib/services/local/usertokens.go b/lib/services/local/usertokens.go new file mode 100644 index 0000000000000..60b5e4876cb1c --- /dev/null +++ b/lib/services/local/usertokens.go @@ -0,0 +1,149 @@ +/* +Copyright 2015 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package local + +import ( + "bytes" + "context" + + "github.com/gravitational/teleport/lib/backend" + "github.com/gravitational/teleport/lib/services" + + "github.com/gravitational/trace" +) + +// GetUserTokens returns all UserTokens +func (s *IdentityService) GetUserTokens() ([]services.UserToken, error) { + startKey := backend.Key(userTokensPrefix) + result, err := s.GetRange(context.TODO(), startKey, backend.RangeEnd(startKey), backend.NoLimit) + if err != nil { + return nil, trace.Wrap(err) + } + + var usertokens []services.UserToken + for _, item := range result.Items { + if !bytes.HasSuffix(item.Key, []byte(paramsPrefix)) { + continue + } + + usertoken, err := services.UnmarshalUserToken(item.Value) + if err != nil { + return nil, trace.Wrap(err) + } + + usertokens = append(usertokens, usertoken) + } + + return usertokens, nil +} + +// DeleteUserToken deletes UserToken by ID +func (s *IdentityService) DeleteUserToken(tokenID string) error { + _, err := s.GetUserToken(tokenID) + if err != nil { + return trace.Wrap(err) + } + + startKey := backend.Key(userTokensPrefix, tokenID) + err = s.DeleteRange(context.TODO(), startKey, backend.RangeEnd(startKey)) + return trace.Wrap(err) +} + +// GetUserToken returns a token by its ID +func (s *IdentityService) GetUserToken(tokenID string) (services.UserToken, error) { + item, err := s.Get(context.TODO(), backend.Key(userTokensPrefix, tokenID, paramsPrefix)) + if err != nil { + if trace.IsNotFound(err) { + return nil, trace.NotFound("user token(%v) not found", tokenID) + } + return nil, trace.Wrap(err) + } + + usertoken, err := services.UnmarshalUserToken(item.Value) + if err != nil { + return nil, trace.Wrap(err) + } + + return usertoken, nil +} + +// CreateUserToken creates a token that is used for signups and resets +func (s *IdentityService) CreateUserToken(usertoken services.UserToken) (services.UserToken, error) { + if err := usertoken.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + value, err := services.MarshalUserToken(usertoken) + if err != nil { + return nil, trace.Wrap(err) + } + + item := backend.Item{ + Key: backend.Key(userTokensPrefix, usertoken.GetName(), paramsPrefix), + Value: value, + Expires: usertoken.Expiry(), + } + _, err = s.Create(context.TODO(), item) + if err != nil { + return nil, trace.Wrap(err) + } + + return usertoken, nil +} + +// GetUserTokenSecrets returns user token secrets +func (s *IdentityService) GetUserTokenSecrets(tokenID string) (services.UserTokenSecrets, error) { + item, err := s.Get(context.TODO(), backend.Key(userTokensPrefix, tokenID, secretsPrefix)) + if err != nil { + if trace.IsNotFound(err) { + return nil, trace.NotFound("user token(%v) secrets not found", tokenID) + } + return nil, trace.Wrap(err) + } + + secrets, err := services.UnmarshalUserTokenSecrets(item.Value) + if err != nil { + return nil, trace.Wrap(err) + } + + return secrets, nil +} + +// UpsertUserTokenSecrets upserts user token secrets +func (s *IdentityService) UpsertUserTokenSecrets(secrets services.UserTokenSecrets) error { + if err := secrets.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + + value, err := services.MarshalUserTokenSecrets(secrets) + if err != nil { + return trace.Wrap(err) + } + item := backend.Item{ + Key: backend.Key(userTokensPrefix, secrets.GetName(), secretsPrefix), + Value: value, + Expires: secrets.Expiry(), + } + _, err = s.Put(context.TODO(), item) + + return trace.Wrap(err) +} + +const ( + userTokensPrefix = "usertokens" + secretsPrefix = "secrets" +) diff --git a/lib/services/resource.go b/lib/services/resource.go index 869ae4ae9dd7e..9c700df7ed186 100644 --- a/lib/services/resource.go +++ b/lib/services/resource.go @@ -162,8 +162,11 @@ const ( // to proxy KindRemoteCluster = "remote_cluster" - // KindInviteToken is a local user invite token - KindInviteToken = "invite_token" + // KindUserToken is a token used to change user passwords + KindUserToken = "user_token" + + // KindUserTokenSecrets is user token secrets used to change passwords + KindUserTokenSecrets = "user_token_secrets" // KindIdentity is local on disk identity resource KindIdentity = "identity" diff --git a/lib/services/usertoken.go b/lib/services/usertoken.go new file mode 100644 index 0000000000000..db8cc04d1993b --- /dev/null +++ b/lib/services/usertoken.go @@ -0,0 +1,207 @@ +/* +Copyright 2020 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "fmt" + "time" + + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" +) + +// UserToken represents a temporary token used to reset passwords +type UserToken interface { + // Resource provides common resource properties + Resource + // GetUser returns User + GetUser() string + // GetCreated returns Created + GetCreated() time.Time + // GetURL returns URL + GetURL() string + // SetURL returns URL + SetURL(string) + // CheckAndSetDefaults checks and set default values for any missing fields. + CheckAndSetDefaults() error +} + +// UserTokenV3 is an user token format V3 +type UserTokenV3 struct { + // Kind is a resource kind - always resource. + Kind string `json:"kind"` + + // SubKind is a resource sub kind + SubKind string `json:"sub_kind,omitempty"` + + // Version is a resource version. + Version string `json:"version"` + + // Metadata is metadata about the resource. + Metadata Metadata `json:"metadata"` + + // Spec is a spec of the invite token + Spec UserTokenSpecV3 `json:"spec"` +} + +// GetName returns Name +func (u *UserTokenV3) GetName() string { + return u.Metadata.Name +} + +// GetUser returns User +func (u *UserTokenV3) GetUser() string { + return u.Spec.User +} + +// GetCreated returns Created +func (u *UserTokenV3) GetCreated() time.Time { + return u.Spec.Created +} + +// GetURL returns URL +func (u *UserTokenV3) GetURL() string { + return u.Spec.URL +} + +// SetURL sets URL +func (u *UserTokenV3) SetURL(url string) { + u.Spec.URL = url +} + +// Expiry returns object expiry setting +func (u *UserTokenV3) Expiry() time.Time { + return u.Metadata.Expiry() +} + +// SetExpiry sets object expiry +func (u *UserTokenV3) SetExpiry(t time.Time) { + u.Metadata.SetExpiry(t) +} + +// SetTTL sets Expires header using current clock +func (u *UserTokenV3) SetTTL(clock clockwork.Clock, ttl time.Duration) { + u.Metadata.SetTTL(clock, ttl) +} + +// GetMetadata returns object metadata +func (u *UserTokenV3) GetMetadata() Metadata { + return u.Metadata +} + +// GetVersion returns resource version +func (u *UserTokenV3) GetVersion() string { + return u.Version +} + +// GetKind returns resource kind +func (u *UserTokenV3) GetKind() string { + return u.Kind +} + +// SetName sets the name of the resource +func (u *UserTokenV3) SetName(name string) { + u.Metadata.Name = name +} + +// GetResourceID returns resource ID +func (u *UserTokenV3) GetResourceID() int64 { + return u.Metadata.ID +} + +// SetResourceID sets resource ID +func (u *UserTokenV3) SetResourceID(id int64) { + u.Metadata.ID = id +} + +// GetSubKind returns resource sub kind +func (u *UserTokenV3) GetSubKind() string { + return u.SubKind +} + +// SetSubKind sets resource subkind +func (u *UserTokenV3) SetSubKind(s string) { + u.SubKind = s +} + +// CheckAndSetDefaults checks and set default values for any missing fields. +func (u UserTokenV3) CheckAndSetDefaults() error { + return u.Metadata.CheckAndSetDefaults() +} + +// UserTokenSpecV3 is a spec for invite token +type UserTokenSpecV3 struct { + // User is user name associated with this token + User string `json:"user"` + // Created holds information about when the token was created + Created time.Time `json:"created"` + // URL is this token URL + URL string `json:"url"` +} + +// NewUserToken is a convenience wa to create a RemoteCluster resource. +func NewUserToken(tokenID string) UserTokenV3 { + return UserTokenV3{ + Kind: KindUserToken, + Version: V3, + Metadata: Metadata{ + Name: tokenID, + Namespace: defaults.Namespace, + }, + } +} + +// UserTokenSpecV3Template is a template for V3 UserToken JSON schema +const UserTokenSpecV3Template = `{ + "type": "object", + "additionalProperties": false, + "properties": { + "user": { + "type": ["string"] + }, + "created": { + "type": ["string"] + }, + "url": { + "type": ["string"] + } + } +}` + +// UnmarshalUserToken unmarshals UserToken +func UnmarshalUserToken(bytes []byte) (UserToken, error) { + if len(bytes) == 0 { + return nil, trace.BadParameter("missing resource data") + } + + schema := fmt.Sprintf(V2SchemaTemplate, MetadataSchema, UserTokenSpecV3Template, DefaultDefinitions) + + var usertoken UserTokenV3 + err := utils.UnmarshalWithSchema(schema, &usertoken, bytes) + if err != nil { + return nil, trace.BadParameter(err.Error()) + } + + return &usertoken, nil +} + +// MarshalUserToken marshals role to JSON or YAML. +func MarshalUserToken(usertoken UserToken, opts ...MarshalOption) ([]byte, error) { + return utils.FastMarshal(usertoken) +} diff --git a/lib/services/usertoken_test.go b/lib/services/usertoken_test.go new file mode 100644 index 0000000000000..80c74c02d7d9d --- /dev/null +++ b/lib/services/usertoken_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2020 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package services + +import ( + "fmt" + "time" + + "github.com/gravitational/teleport/lib/fixtures" + "github.com/gravitational/teleport/lib/utils" + + "gopkg.in/check.v1" +) + +type UserTokenSuite struct{} + +var _ = check.Suite(&UserTokenSuite{}) +var _ = fmt.Printf + +func (s *UserTokenSuite) SetUpSuite(c *check.C) { + utils.InitLoggerForTests() +} + +func (s *UserTokenSuite) TestUnmarshal(c *check.C) { + created, err := time.Parse(time.RFC3339, "2020-01-14T18:52:39.523076855Z") + c.Assert(err, check.IsNil) + + type testCase struct { + description string + input string + expected UserToken + } + + testCases := []testCase{ + { + description: "simple case", + input: ` + { + "kind": "user_token", + "version": "v3", + "metadata": { + "name": "tokenId" + }, + "spec": { + "user": "example@example.com", + "created": "2020-01-14T18:52:39.523076855Z", + "url": "https://localhost" + } + } + `, + expected: &UserTokenV3{ + Kind: KindUserToken, + Version: V3, + Metadata: Metadata{ + Name: "tokenId", + }, + Spec: UserTokenSpecV3{ + Created: created, + User: "example@example.com", + URL: "https://localhost", + }, + }, + }, + } + + for _, tc := range testCases { + comment := check.Commentf("test case %q", tc.description) + out, err := UnmarshalUserToken([]byte(tc.input)) + c.Assert(err, check.IsNil, comment) + fixtures.DeepCompare(c, tc.expected, out) + data, err := MarshalUserToken(out) + c.Assert(err, check.IsNil, comment) + out2, err := UnmarshalUserToken(data) + c.Assert(err, check.IsNil, comment) + fixtures.DeepCompare(c, tc.expected, out2) + } +} diff --git a/lib/services/usertokensecrets.go b/lib/services/usertokensecrets.go new file mode 100644 index 0000000000000..835149588f69b --- /dev/null +++ b/lib/services/usertokensecrets.go @@ -0,0 +1,205 @@ +/* +Copyright 2020 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "fmt" + "time" + + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" +) + +// UserTokenSecrets contains secrets of user token +type UserTokenSecrets interface { + // Resource provides common resource properties + Resource + // GetCreated returns Created + GetCreated() time.Time + // GetQRCode returns QRCode + GetQRCode() []byte + // GetOTPKey returns OTP key + GetOTPKey() string + // CheckAndSetDefaults checks and set default values for any missing fields. + CheckAndSetDefaults() error +} + +// UserTokenSecretsV3 is an user token spec format V3 +type UserTokenSecretsV3 struct { + // Kind is a resource kind - always resource. + Kind string `json:"kind"` + + // SubKind is a resource sub kind + SubKind string `json:"sub_kind,omitempty"` + + // Version is a resource version. + Version string `json:"version"` + + // Metadata is metadata about the resource. + Metadata Metadata `json:"metadata"` + + // Spec is a spec of the user token secretes + Spec UserTokenSecretsSpecV3 `json:"spec"` +} + +// GetName returns Name +func (u *UserTokenSecretsV3) GetName() string { + return u.Metadata.Name +} + +// GetCreated returns Created +func (u *UserTokenSecretsV3) GetCreated() time.Time { + return u.Spec.Created +} + +// GetOTPKey returns OTP Key +func (u *UserTokenSecretsV3) GetOTPKey() string { + return u.Spec.OTPKey +} + +// GetQRCode returns QRCode +func (u *UserTokenSecretsV3) GetQRCode() []byte { + return []byte(u.Spec.QRCode) +} + +// Expiry returns object expiry setting +func (u *UserTokenSecretsV3) Expiry() time.Time { + return u.Metadata.Expiry() +} + +// SetExpiry sets object expiry +func (u *UserTokenSecretsV3) SetExpiry(t time.Time) { + u.Metadata.SetExpiry(t) +} + +// SetTTL sets Expires header using current clock +func (u *UserTokenSecretsV3) SetTTL(clock clockwork.Clock, ttl time.Duration) { + u.Metadata.SetTTL(clock, ttl) +} + +// GetMetadata returns object metadata +func (u *UserTokenSecretsV3) GetMetadata() Metadata { + return u.Metadata +} + +// GetVersion returns resource version +func (u *UserTokenSecretsV3) GetVersion() string { + return u.Version +} + +// GetKind returns resource kind +func (u *UserTokenSecretsV3) GetKind() string { + return u.Kind +} + +// SetName sets the name of the resource +func (u *UserTokenSecretsV3) SetName(name string) { + u.Metadata.Name = name +} + +// GetResourceID returns resource ID +func (u *UserTokenSecretsV3) GetResourceID() int64 { + return u.Metadata.ID +} + +// SetResourceID sets resource ID +func (u *UserTokenSecretsV3) SetResourceID(id int64) { + u.Metadata.ID = id +} + +// GetSubKind returns resource sub kind +func (u *UserTokenSecretsV3) GetSubKind() string { + return u.SubKind +} + +// SetSubKind sets resource subkind +func (u *UserTokenSecretsV3) SetSubKind(s string) { + u.SubKind = s +} + +// CheckAndSetDefaults checks and set default values for any missing fields. +func (u UserTokenSecretsV3) CheckAndSetDefaults() error { + return u.Metadata.CheckAndSetDefaults() +} + +// UserTokenSecretsSpecV3 is a spec for user token secrets +type UserTokenSecretsSpecV3 struct { + // Created holds information about when the token was created + Created time.Time `json:"created"` + // OTPKey is is a secret value of one time password secret generator + OTPKey string `json:"opt_key,omitempty"` + // QRCode is a QR code value + QRCode string `json:"qr_code,omitempty"` +} + +// NewUserTokenSecrets creates an instance of UserTokenSecrets +func NewUserTokenSecrets(tokenID string) (UserTokenSecretsV3, error) { + secrets := UserTokenSecretsV3{ + Kind: KindUserTokenSecrets, + Version: V3, + Metadata: Metadata{ + Name: tokenID, + }, + } + + err := secrets.CheckAndSetDefaults() + if err != nil { + return UserTokenSecretsV3{}, trace.Wrap(err) + } + + return secrets, nil +} + +// UserTokenSecretsSpecV3Template is a template for V3 UserTokenSecrets JSON schema +const UserTokenSecretsSpecV3Template = `{ + "type": "object", + "additionalProperties": false, + "properties": { + "opt_key": { + "type": ["string"] + }, + "qr_code": { + "type": ["string"] + }, + "created": { + "type": ["string"] + } + } +}` + +// UnmarshalUserTokenSecrets unmarshals UserTokenSecrets +func UnmarshalUserTokenSecrets(bytes []byte) (UserTokenSecrets, error) { + if len(bytes) == 0 { + return nil, trace.BadParameter("missing resource data") + } + + schema := fmt.Sprintf(V2SchemaTemplate, MetadataSchema, UserTokenSecretsSpecV3Template, DefaultDefinitions) + + var usertokenSecrets UserTokenSecretsV3 + err := utils.UnmarshalWithSchema(schema, &usertokenSecrets, bytes) + if err != nil { + return nil, trace.BadParameter(err.Error()) + } + + return &usertokenSecrets, nil +} + +// MarshalUserTokenSecrets marshals role to JSON or YAML. +func MarshalUserTokenSecrets(usertokenSecrets UserTokenSecrets, opts ...MarshalOption) ([]byte, error) { + return utils.FastMarshal(usertokenSecrets) +} diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 6f9c8f1a1ea92..1ab2e6dcced88 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -167,10 +167,10 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { h.DELETE("/webapi/sessions", h.WithAuth(h.deleteSession)) h.POST("/webapi/sessions/renew", h.WithAuth(h.renewSession)) - // Users - h.GET("/webapi/users/invites/:token", httplib.MakeHandler(h.renderUserInvite)) - h.POST("/webapi/users", httplib.MakeHandler(h.createNewUser)) + h.GET("/webapi/usertokens/:token", httplib.MakeHandler(h.getUserTokenHandle)) + h.PUT("/webapi/users/password/usertoken", httplib.WithCSRFProtection(h.changePasswordWithToken)) h.PUT("/webapi/users/password", h.WithAuth(h.changePassword)) + h.POST("/webapi/sites/:site/namespaces/:namespace/usertokens", h.WithClusterAuth(h.createUserToken)) // Issues SSH temp certificates based on 2FA access creds h.POST("/webapi/ssh/certs", httplib.MakeHandler(h.createSSHCert)) @@ -222,7 +222,6 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { // U2F related APIs h.GET("/webapi/u2f/signuptokens/:token", httplib.MakeHandler(h.u2fRegisterRequest)) - h.POST("/webapi/u2f/users", httplib.MakeHandler(h.createNewU2FUser)) h.POST("/webapi/u2f/password/changerequest", h.WithAuth(h.u2fChangePasswordRequest)) h.POST("/webapi/u2f/signrequest", httplib.MakeHandler(h.u2fSignRequest)) h.POST("/webapi/u2f/sessions", httplib.MakeHandler(h.createSessionWithU2FSignResponse)) @@ -1027,19 +1026,6 @@ type CreateSessionResponse struct { ExpiresIn int `json:"expires_in"` } -type createSessionResponseRaw struct { - // Type is token type (bearer) - Type string `json:"type"` - // Token value - Token string `json:"token"` - // ExpiresIn sets seconds before this token is not valid - ExpiresIn int `json:"expires_in"` -} - -func (r createSessionResponseRaw) response() (*CreateSessionResponse, error) { - return &CreateSessionResponse{Type: r.Type, Token: r.Token, ExpiresIn: r.ExpiresIn}, nil -} - func NewSessionResponse(ctx *SessionContext) (*CreateSessionResponse, error) { clt, err := ctx.GetClient() if err != nil { @@ -1177,32 +1163,76 @@ func (h *Handler) renewSession(w http.ResponseWriter, r *http.Request, _ httprou return NewSessionResponse(newContext) } -type renderUserInviteResponse struct { - InviteToken string `json:"invite_token"` - User string `json:"user"` - QR []byte `json:"qr"` +func (h *Handler) changePasswordWithToken(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + var req auth.ChangePasswordWithTokenRequest + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + + sess, err := h.auth.proxyClient.ChangePasswordWithToken(req) + if err != nil { + return nil, trace.Wrap(err) + } + ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName()) + if err != nil { + return nil, trace.Wrap(err) + } + if err := SetSession(w, sess.GetUser(), sess.GetName()); err != nil { + return nil, trace.Wrap(err) + } + + return NewSessionResponse(ctx) } -// renderUserInvite is called to show user the new user invitation page -// -// GET /v1/webapi/users/invites/:token -// -// Response: -// -// {"invite_token": "token", "user": "alex", qr: "base64-encoded-qr-code image"} -// -// -func (h *Handler) renderUserInvite(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - token := p[0].Value - user, qrCodeBytes, err := h.auth.GetUserInviteInfo(token) +func (h *Handler) createUserToken(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) { + clt, err := ctx.GetUserClient(site) + if err != nil { + return nil, trace.Wrap(err) + } + + var req auth.CreateUserTokenRequest + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + + userToken, err := clt.CreateUserToken(req) if err != nil { return nil, trace.Wrap(err) } - return &renderUserInviteResponse{ - InviteToken: token, - User: user, - QR: qrCodeBytes, + return ui.UserToken{ + URL: userToken.GetURL(), + Expiry: userToken.Expiry(), + }, nil +} + +func (h *Handler) getUserTokenHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + result, err := h.getUserToken(p[0].Value) + if err != nil { + log.Warnf("failed to fetch user token: %v", err) + // we hide the error from the remote user to avoid giving any hints + return nil, trace.AccessDenied("bad or expired token") + } + + return result, nil +} + +func (h *Handler) getUserToken(tokenID string) (interface{}, error) { + userToken, err := h.auth.proxyClient.GetUserToken(tokenID) + if err != nil { + return nil, trace.Wrap(err) + } + + // rotate secrets each time when requested (security) + secrets, err := h.auth.proxyClient.RotateUserTokenSecrets(tokenID) + if err != nil { + return nil, trace.Wrap(err) + } + + return ui.UserToken{ + TokenID: userToken.GetName(), + User: userToken.GetUser(), + QRCode: secrets.GetQRCode(), }, nil } @@ -1283,76 +1313,6 @@ func (h *Handler) createSessionWithU2FSignResponse(w http.ResponseWriter, r *htt return NewSessionResponse(ctx) } -// createNewUser req is a request to create a new Teleport user -type createNewUserReq struct { - InviteToken string `json:"invite_token"` - Pass string `json:"pass"` - SecondFactorToken string `json:"second_factor_token,omitempty"` -} - -// createNewUser creates new user entry based on the invite token -// -// POST /v1/webapi/users -// -// {"invite_token": "unique invite token", "pass": "user password", "second_factor_token": "valid second factor token"} -// -// Successful response: (session cookie is set) -// -// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20} -func (h *Handler) createNewUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - var req *createNewUserReq - if err := httplib.ReadJSON(r, &req); err != nil { - return nil, trace.Wrap(err) - } - sess, err := h.auth.CreateNewUser(req.InviteToken, req.Pass, req.SecondFactorToken) - if err != nil { - return nil, trace.Wrap(err) - } - ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName()) - if err != nil { - return nil, trace.Wrap(err) - } - if err := SetSession(w, sess.GetUser(), sess.GetName()); err != nil { - return nil, trace.Wrap(err) - } - return NewSessionResponse(ctx) -} - -// A request to create a new user which uses U2F as the second factor -type createNewU2FUserReq struct { - InviteToken string `json:"invite_token"` - Pass string `json:"pass"` - U2FRegisterResponse u2f.RegisterResponse `json:"u2f_register_response"` -} - -// createNewU2FUser creates a new user configured to use U2F as the second factor -// -// POST /webapi/u2f/users -// -// {"invite_token": "unique invite token", "pass": "user password", "u2f_register_response": {"registrationData":"verylongbase64string","clientData":"longbase64string"}} -// -// Successful response: (session cookie is set) -// -// {"type": "bearer", "token": "bearer token", "user": "alex", "expires_in": 20} -func (h *Handler) createNewU2FUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - var req *createNewU2FUserReq - if err := httplib.ReadJSON(r, &req); err != nil { - return nil, trace.Wrap(err) - } - sess, err := h.auth.CreateNewU2FUser(req.InviteToken, req.Pass, req.U2FRegisterResponse) - if err != nil { - return nil, trace.Wrap(err) - } - ctx, err := h.auth.ValidateSession(sess.GetUser(), sess.GetName()) - if err != nil { - return nil, trace.Wrap(err) - } - if err := SetSession(w, sess.GetUser(), sess.GetName()); err != nil { - return nil, trace.Wrap(err) - } - return NewSessionResponse(ctx) -} - // getClusters returns a list of clusters // // GET /v1/webapi/sites diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index bb293eec970e2..bad6a0b845695 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -276,7 +276,7 @@ type authPack struct { } func (s *WebSuite) authPackFromResponse(c *C, re *roundtrip.Response) *authPack { - var sess *createSessionResponseRaw + var sess *CreateSessionResponse c.Assert(json.Unmarshal(re.Bytes(), &sess), IsNil) jar, err := cookiejar.New(nil) @@ -332,7 +332,7 @@ func (s *WebSuite) authPack(c *C, user string) *authPack { re, err := s.login(clt, csrfToken, csrfToken, req) c.Assert(err, IsNil) - var rawSess *createSessionResponseRaw + var rawSess *CreateSessionResponse c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil) sess, err := rawSess.response() @@ -376,83 +376,6 @@ func (s *WebSuite) createUser(c *C, user string, login string, pass string, otpS c.Assert(err, IsNil) } -func (s *WebSuite) TestNewUser(c *C) { - token, err := s.server.Auth().CreateSignupToken(services.UserV1{Name: "bob", AllowedLogins: []string{s.user}}, 0) - c.Assert(err, IsNil) - - // Save the original signup token, after GET /v2/webapi/users/invites/ - // this should change. - ost, err := s.server.Auth().GetSignupToken(token) - c.Assert(err, IsNil) - - tokens, err := s.server.Auth().GetTokens() - c.Assert(err, IsNil) - c.Assert(len(tokens), Equals, 1) - c.Assert(tokens[0].GetName(), Equals, token) - - clt := s.client() - re, err := clt.Get(context.Background(), clt.Endpoint("webapi", "users", "invites", token), url.Values{}) - c.Assert(err, IsNil) - - var out *renderUserInviteResponse - c.Assert(json.Unmarshal(re.Bytes(), &out), IsNil) - c.Assert(out.User, Equals, "bob") - c.Assert(out.InviteToken, Equals, token) - - st, err := s.server.Auth().GetSignupToken(token) - c.Assert(err, IsNil) - - // Make sure that the signup token changed after rending the endpoint - // GET /v2/webapi/users/invites/ above. - c.Assert(st, Not(Equals), ost) - - validToken, err := totp.GenerateCode(st.OTPKey, time.Now()) - c.Assert(err, IsNil) - - tempPass := "abc123" - - re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "users"), createNewUserReq{ - InviteToken: token, - Pass: tempPass, - SecondFactorToken: validToken, - }) - c.Assert(err, IsNil) - - var rawSess *createSessionResponseRaw - c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil) - cookies := re.Cookies() - c.Assert(len(cookies), Equals, 1) - - // now make sure we are logged in by calling authenticated method - // we need to supply both session cookie and bearer token for - // request to succeed - jar, err := cookiejar.New(nil) - c.Assert(err, IsNil) - - clt = s.client(roundtrip.BearerAuth(rawSess.Token), roundtrip.CookieJar(jar)) - jar.SetCookies(s.url(), re.Cookies()) - - re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{}) - c.Assert(err, IsNil) - - var clusters *ui.AvailableClusters - c.Assert(json.Unmarshal(re.Bytes(), &clusters), IsNil) - - // in absence of session cookie or bearer auth the same request fill fail - - // no session cookie: - clt = s.client(roundtrip.BearerAuth(rawSess.Token)) - re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{}) - c.Assert(err, NotNil) - c.Assert(trace.IsAccessDenied(err), Equals, true) - - // no bearer token: - clt = s.client(roundtrip.CookieJar(jar)) - re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{}) - c.Assert(err, NotNil) - c.Assert(trace.IsAccessDenied(err), Equals, true) -} - func (s *WebSuite) TestSAMLSuccess(c *C) { input := fixtures.SAMLOktaConnectorV2 @@ -559,7 +482,7 @@ func (s *WebSuite) TestWebSessionsCRUD(c *C) { re, err := pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites"), url.Values{}) c.Assert(err, IsNil) - var clusters *ui.AvailableClusters + var clusters []ui.Cluster c.Assert(json.Unmarshal(re.Bytes(), &clusters), IsNil) // now delete session @@ -1153,43 +1076,37 @@ func (s *WebSuite) TestPlayback(c *C) { defer ws.Close() } -func (s *WebSuite) TestNewU2FUser(c *C) { - // configure cluster authentication preferences - cap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ +func (s *WebSuite) TestLogin(c *C) { + ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.U2F, - U2F: &services.U2F{ - AppID: "https://" + s.server.ClusterName(), - Facets: []string{"https://" + s.server.ClusterName()}, - }, + SecondFactor: teleport.OFF, }) c.Assert(err, IsNil) - err = s.server.AuthServer.AuthServer.SetAuthPreference(cap) - c.Assert(err, IsNil) - - token, err := s.server.Auth().CreateSignupToken(services.UserV1{Name: "bob", AllowedLogins: []string{s.user}}, 0) + err = s.server.Auth().SetAuthPreference(ap) c.Assert(err, IsNil) - clt := s.client() - re, err := clt.Get(context.Background(), clt.Endpoint("webapi", "u2f", "signuptokens", token), url.Values{}) - c.Assert(err, IsNil) + // create user + s.createUser(c, "user1", "root", "password", "") - var u2fRegReq u2f.RegisterRequest - c.Assert(json.Unmarshal(re.Bytes(), &u2fRegReq), IsNil) + loginReq, err := json.Marshal(createSessionReq{ + User: "user1", + Pass: "password", + }) - u2fRegResp, err := s.mockU2F.RegisterResponse(&u2fRegReq) + clt := s.client() + req, err := http.NewRequest("POST", clt.Endpoint("webapi", "sessions"), bytes.NewBuffer(loginReq)) c.Assert(err, IsNil) - tempPass := "abc123" + csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992" + addCSRFCookieToReq(req, csrfToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(csrf.HeaderName, csrfToken) - re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "users"), createNewU2FUserReq{ - InviteToken: token, - Pass: tempPass, - U2FRegisterResponse: *u2fRegResp, + re, err := clt.Client.RoundTrip(func() (*http.Response, error) { + return clt.Client.HTTPClient().Do(req) }) - c.Assert(err, IsNil) - var rawSess *createSessionResponseRaw + var rawSess *CreateSessionResponse c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil) cookies := re.Cookies() c.Assert(len(cookies), Equals, 1) @@ -1206,7 +1123,7 @@ func (s *WebSuite) TestNewU2FUser(c *C) { re, err = clt.Get(context.Background(), clt.Endpoint("webapi", "sites"), url.Values{}) c.Assert(err, IsNil) - var clusters *ui.AvailableClusters + var clusters []ui.Cluster c.Assert(json.Unmarshal(re.Bytes(), &clusters), IsNil) // in absence of session cookie or bearer auth the same request fill fail @@ -1224,6 +1141,116 @@ func (s *WebSuite) TestNewU2FUser(c *C) { c.Assert(trace.IsAccessDenied(err), Equals, true) } +func (s *WebSuite) TestChangePasswordWithTokenOTP(c *C) { + ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ + Type: teleport.Local, + SecondFactor: teleport.OTP, + }) + c.Assert(err, IsNil) + err = s.server.Auth().SetAuthPreference(ap) + c.Assert(err, IsNil) + + // create user + s.createUser(c, "user1", "root", "password", "") + + // create password change token + token, err := s.server.Auth().CreateUserToken(auth.CreateUserTokenRequest{ + Name: "user1", + }) + c.Assert(err, IsNil) + + clt := s.client() + re, err := clt.Get(context.Background(), clt.Endpoint("webapi", "usertokens", token.GetName()), url.Values{}) + c.Assert(err, IsNil) + + var uiToken *ui.UserToken + c.Assert(json.Unmarshal(re.Bytes(), &uiToken), IsNil) + c.Assert(uiToken.User, Equals, token.GetUser()) + c.Assert(uiToken.TokenID, Equals, token.GetName()) + + secrets, err := s.server.Auth().RotateUserTokenSecrets(token.GetName()) + c.Assert(err, IsNil) + + secondFactorToken, err := totp.GenerateCode(secrets.GetOTPKey(), time.Now()) + c.Assert(err, IsNil) + + data, err := json.Marshal(auth.ChangePasswordWithTokenRequest{ + TokenID: token.GetName(), + Password: []byte("abc123"), + SecondFactorToken: secondFactorToken, + }) + + req, err := http.NewRequest("PUT", clt.Endpoint("webapi", "users", "password", "usertoken"), bytes.NewBuffer(data)) + c.Assert(err, IsNil) + + csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992" + addCSRFCookieToReq(req, csrfToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(csrf.HeaderName, csrfToken) + + re, err = clt.Client.RoundTrip(func() (*http.Response, error) { + return clt.Client.HTTPClient().Do(req) + }) + + var rawSess *CreateSessionResponse + c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil) + c.Assert(rawSess.Token != "", Equals, true) +} + +func (s *WebSuite) TestChangePasswordWithTokenU2F(c *C) { + ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ + Type: teleport.Local, + SecondFactor: teleport.U2F, + U2F: &services.U2F{ + AppID: "https://" + s.server.ClusterName(), + Facets: []string{"https://" + s.server.ClusterName()}, + }, + }) + c.Assert(err, IsNil) + err = s.server.Auth().SetAuthPreference(ap) + c.Assert(err, IsNil) + + s.createUser(c, "user2", "root", "password", "") + + // create reset password token + token, err := s.server.Auth().CreateUserToken(auth.CreateUserTokenRequest{ + Name: "user2", + }) + c.Assert(err, IsNil) + + clt := s.client() + re, err := clt.Get(context.Background(), clt.Endpoint("webapi", "u2f", "signuptokens", token.GetName()), url.Values{}) + c.Assert(err, IsNil) + + var u2fRegReq u2f.RegisterRequest + c.Assert(json.Unmarshal(re.Bytes(), &u2fRegReq), IsNil) + + u2fRegResp, err := s.mockU2F.RegisterResponse(&u2fRegReq) + c.Assert(err, IsNil) + + data, err := json.Marshal(auth.ChangePasswordWithTokenRequest{ + TokenID: token.GetName(), + Password: []byte("qweQWE"), + U2FRegisterResponse: *u2fRegResp, + }) + + req, err := http.NewRequest("PUT", clt.Endpoint("webapi", "users", "password", "usertoken"), bytes.NewBuffer(data)) + c.Assert(err, IsNil) + + csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992" + addCSRFCookieToReq(req, csrfToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(csrf.HeaderName, csrfToken) + + re, err = clt.Client.RoundTrip(func() (*http.Response, error) { + return clt.Client.HTTPClient().Do(req) + }) + + var rawSess *CreateSessionResponse + c.Assert(json.Unmarshal(re.Bytes(), &rawSess), IsNil) + c.Assert(rawSess.Token != "", Equals, true) +} + func (s *WebSuite) TestU2FLogin(c *C) { // configure cluster authentication preferences cap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ @@ -1238,25 +1265,34 @@ func (s *WebSuite) TestU2FLogin(c *C) { err = s.server.Auth().SetAuthPreference(cap) c.Assert(err, IsNil) - token, err := s.server.Auth().CreateSignupToken(services.UserV1{Name: "bob", AllowedLogins: []string{s.user}}, 0) + // create user + s.createUser(c, "bob", "root", "password", "") + + // create password change token + token, err := s.server.Auth().CreateUserToken(auth.CreateUserTokenRequest{ + Name: "bob", + }) c.Assert(err, IsNil) - u2fRegReq, err := s.proxyClient.GetSignupU2FRegisterRequest(token) + u2fRegReq, err := s.proxyClient.GetSignupU2FRegisterRequest(token.GetName()) c.Assert(err, IsNil) u2fRegResp, err := s.mockU2F.RegisterResponse(u2fRegReq) c.Assert(err, IsNil) - tempPass := "abc123" - - _, err = s.proxyClient.CreateUserWithU2FToken(token, tempPass, *u2fRegResp) + tempPass := []byte("abc123") + _, err = s.proxyClient.ChangePasswordWithToken(auth.ChangePasswordWithTokenRequest{ + TokenID: token.GetName(), + U2FRegisterResponse: *u2fRegResp, + Password: tempPass, + }) c.Assert(err, IsNil) // normal login clt := s.client() re, err := clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{ User: "bob", - Pass: tempPass, + Pass: string(tempPass), }) c.Assert(err, IsNil) var u2fSignReq u2f.SignRequest @@ -1272,10 +1308,9 @@ func (s *WebSuite) TestU2FLogin(c *C) { c.Assert(err, IsNil) // bad login: corrupted sign responses, should fail - re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{ User: "bob", - Pass: tempPass, + Pass: string(tempPass), }) c.Assert(err, IsNil) c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil) @@ -1286,7 +1321,6 @@ func (s *WebSuite) TestU2FLogin(c *C) { // corrupted KeyHandle u2fSignRespCopy := u2fSignResp u2fSignRespCopy.KeyHandle = u2fSignRespCopy.KeyHandle + u2fSignRespCopy.KeyHandle - _, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{ User: "bob", U2FSignResponse: *u2fSignRespCopy, @@ -1314,12 +1348,10 @@ func (s *WebSuite) TestU2FLogin(c *C) { c.Assert(err, NotNil) // bad login: counter not increasing, should fail - s.mockU2F.SetCounter(0) - re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{ User: "bob", - Pass: tempPass, + Pass: string(tempPass), }) c.Assert(err, IsNil) c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil) @@ -1819,3 +1851,7 @@ func newTerminalHandler() TerminalHandler { decoder: unicode.UTF8.NewDecoder(), } } + +func (r CreateSessionResponse) response() (*CreateSessionResponse, error) { + return &CreateSessionResponse{Type: r.Type, Token: r.Token, ExpiresIn: r.ExpiresIn}, nil +} diff --git a/lib/web/sessions.go b/lib/web/sessions.go index dfeb1aecdd3f5..0217dca184044 100644 --- a/lib/web/sessions.go +++ b/lib/web/sessions.go @@ -29,7 +29,6 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/reversetunnel" @@ -469,39 +468,10 @@ func (s *sessionCache) GetCertificateWithU2F(c client.CreateSSHCertWithU2FReq) ( }) } -func (s *sessionCache) GetUserInviteInfo(token string) (user string, otpQRCode []byte, err error) { - return s.proxyClient.GetSignupTokenData(token) -} - func (s *sessionCache) GetUserInviteU2FRegisterRequest(token string) (*u2f.RegisterRequest, error) { return s.proxyClient.GetSignupU2FRegisterRequest(token) } -func (s *sessionCache) CreateNewUser(token, password, otpToken string) (services.WebSession, error) { - cap, err := s.proxyClient.GetAuthPreference() - if err != nil { - return nil, trace.Wrap(err) - } - - var webSession services.WebSession - - switch cap.GetSecondFactor() { - case teleport.OFF: - webSession, err = s.proxyClient.CreateUserWithoutOTP(token, password) - case teleport.OTP, teleport.TOTP, teleport.HOTP: - webSession, err = s.proxyClient.CreateUserWithOTP(token, password, otpToken) - } - if err != nil { - return nil, trace.Wrap(err) - } - - return webSession, nil -} - -func (s *sessionCache) CreateNewU2FUser(token string, password string, u2fRegisterResponse u2f.RegisterResponse) (services.WebSession, error) { - return s.proxyClient.CreateUserWithU2FToken(token, password, u2fRegisterResponse) -} - func (s *sessionCache) ValidateTrustedCluster(validateRequest *auth.ValidateTrustedClusterRequest) (*auth.ValidateTrustedClusterResponse, error) { return s.proxyClient.ValidateTrustedCluster(validateRequest) } diff --git a/lib/web/ui/usertoken.go b/lib/web/ui/usertoken.go new file mode 100644 index 0000000000000..72c19fbcdfcc0 --- /dev/null +++ b/lib/web/ui/usertoken.go @@ -0,0 +1,33 @@ +/* +Copyright 2020 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ui + +import "time" + +// UserToken describes UserToken UI object +type UserToken struct { + // TokenID is token ID + TokenID string `json:"tokenId"` + // User is user name associated with this token + User string `json:"user"` + // QRCode is a QR code value + QRCode []byte `json:"qrCode,omitempty"` + // URL is token URL + URL string `json:"url,omitempty"` + // Expiry is token expiration time + Expiry time.Time `json:"expiry,omitempty"` +} diff --git a/tool/tctl/common/user_command.go b/tool/tctl/common/user_command.go index 3e5c7f96d765e..76a8eefaa300c 100644 --- a/tool/tctl/common/user_command.go +++ b/tool/tctl/common/user_command.go @@ -19,6 +19,7 @@ package common import ( "encoding/json" "fmt" + "net/url" "strings" "time" @@ -29,7 +30,6 @@ import ( "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/services" - "github.com/gravitational/teleport/lib/web" "github.com/gravitational/trace" ) @@ -47,10 +47,11 @@ type UserCommand struct { // format is the output format, e.g. text or json format string - userAdd *kingpin.CmdClause - userUpdate *kingpin.CmdClause - userList *kingpin.CmdClause - userDelete *kingpin.CmdClause + userAdd *kingpin.CmdClause + userUpdate *kingpin.CmdClause + userList *kingpin.CmdClause + userDelete *kingpin.CmdClause + userChangePassword *kingpin.CmdClause } // Initialize allows UserCommand to plug itself into the CLI parser @@ -64,8 +65,8 @@ func (u *UserCommand) Initialize(app *kingpin.Application, config *service.Confi Default("").StringVar(&u.allowedLogins) u.userAdd.Flag("k8s-groups", "Kubernetes groups to assign to a user."). Default("").StringVar(&u.kubeGroups) - u.userAdd.Flag("ttl", fmt.Sprintf("Set expiration time for token, default is %v hour, maximum is %v hours", - int(defaults.SignupTokenTTL/time.Hour), int(defaults.MaxSignupTokenTTL/time.Hour))). + u.userAdd.Flag("ttl", fmt.Sprintf("Set expiration time for token, default is %v, maximum is %v", + defaults.SignupTokenTTL, defaults.MaxSignupTokenTTL)). Default(fmt.Sprintf("%v", defaults.SignupTokenTTL)).DurationVar(&u.ttl) u.userAdd.Flag("format", "Output format, 'text' or 'json'").Hidden().Default(teleport.Text).StringVar(&u.format) u.userAdd.Alias(AddUserHelp) @@ -81,6 +82,12 @@ func (u *UserCommand) Initialize(app *kingpin.Application, config *service.Confi u.userDelete = users.Command("rm", "Deletes user accounts").Alias("del") u.userDelete.Arg("logins", "Comma-separated list of user logins to delete"). Required().StringVar(&u.login) + + u.userChangePassword = users.Command("reset", "Reset user password and generate a new token") + u.userChangePassword.Arg("account", "Teleport user account name").Required().StringVar(&u.login) + u.userChangePassword.Flag("ttl", fmt.Sprintf("Set expiration time for token, default is %v, maximum is %v", + defaults.ChangePasswordTokenTTL, defaults.MaxChangePasswordTokenTTL)). + Default(fmt.Sprintf("%v", defaults.ChangePasswordTokenTTL)).DurationVar(&u.ttl) } // TryRun takes the CLI command as an argument (like "users add") and executes it. @@ -94,12 +101,46 @@ func (u *UserCommand) TryRun(cmd string, client auth.ClientI) (match bool, err e err = u.List(client) case u.userDelete.FullCommand(): err = u.Delete(client) + case u.userChangePassword.FullCommand(): + err = u.ChangePassword(client) default: return false, nil } return true, trace.Wrap(err) } +// ChangePassword resets user password and generates a token to setup new password +func (u *UserCommand) ChangePassword(client auth.ClientI) error { + req := auth.CreateUserTokenRequest{ + Name: u.login, + TTL: u.ttl, + Type: auth.UserTokenTypePasswordChange, + } + token, err := client.CreateUserToken(req) + if err != nil { + return err + } + + url, err := url.Parse(token.GetURL()) + if err != nil { + return trace.Wrap(err) + } + + if u.format == teleport.Text { + ttl := token.Expiry().Sub(time.Now().UTC()).Round(time.Second) + fmt.Printf("Reset password token has been created and is valid for %v. Share this URL with the user to complete password reset process:\n%v\n\n", ttl, url) + fmt.Printf("NOTE: Make sure %v points at a Teleport proxy which users can access.\n", url.Host) + return nil + } + + err = printTokenAsJSON(token) + if err != nil { + return trace.Wrap(err) + } + + return nil +} + // Add creates a new sign-up token and prints a token URL to stdout. // A user is not created until he visits the sign-up URL and completes the process func (u *UserCommand) Add(client auth.ClientI) error { @@ -111,35 +152,66 @@ func (u *UserCommand) Add(client auth.ClientI) error { if u.kubeGroups != "" { kubeGroups = strings.Split(u.kubeGroups, ",") } - user := services.UserV1{ - Name: u.login, - AllowedLogins: strings.Split(u.allowedLogins, ","), - KubeGroups: kubeGroups, + + // make sure that user does not exist + _, err := client.GetUser(u.login, false) + if err == nil { + if err == nil { + return trace.BadParameter("user(%v) already registered", u.login) + } } - token, err := client.CreateSignupToken(user, u.ttl) + + user, err := services.NewUser(u.login) + if err != nil { + return trace.Wrap(err) + } + + traits := map[string][]string{ + teleport.TraitLogins: strings.Split(u.allowedLogins, ","), + teleport.TraitKubeGroups: kubeGroups, + } + + user.SetTraits(traits) + user.AddRole(teleport.AdminRoleName) + err = client.UpsertUser(user) if err != nil { return err } - // try to auto-suggest the activation link - return u.PrintSignupURL(client, token, u.ttl, u.format) -} + token, err := client.CreateUserToken(auth.CreateUserTokenRequest{ + Name: u.login, + TTL: u.ttl, + Type: auth.UserTokenTypeInvite, + }) + if err != nil { + return err + } -// PrintSignupURL prints signup URL -func (u *UserCommand) PrintSignupURL(client auth.ClientI, token string, ttl time.Duration, format string) error { - signupURL, proxyHost := web.CreateSignupLink(client, token) + url, err := url.Parse(token.GetURL()) + if err != nil { + return trace.Wrap(err) + } - if format == teleport.Text { - fmt.Printf("Signup token has been created and is valid for %v hours. Share this URL with the user:\n%v\n\n", - int(ttl/time.Hour), signupURL) - fmt.Printf("NOTE: Make sure %v points at a Teleport proxy which users can access.\n", proxyHost) - } else { - out, err := json.MarshalIndent(services.NewInviteToken(token, signupURL, time.Now().Add(ttl).UTC()), "", " ") - if err != nil { - return trace.Wrap(err, "failed to marshal signup infos") - } - fmt.Printf(string(out)) + if u.format == teleport.Text { + ttl := token.Expiry().Sub(time.Now().UTC()).Round(time.Second) + fmt.Printf("Invite token has been created and is valid for %v. Share this URL with the user to complete user setup:\n%v\n\n", ttl, url) + fmt.Printf("NOTE: Make sure %v points at a Teleport proxy which users can access.\n", url.Host) + } + + err = printTokenAsJSON(token) + if err != nil { + return trace.Wrap(err) + } + + return nil +} + +func printTokenAsJSON(token services.UserToken) error { + out, err := json.MarshalIndent(token, "", " ") + if err != nil { + return trace.Wrap(err, "failed to marshal user token") } + fmt.Printf(string(out)) return nil }