teleport/lib/auth/usertoken.go
Edoardo Spadolini 58b01b964b
Embed auth.Cache in auth.Server (#14698)
* Embed auth.Cache in auth.Server

* Hit the backend during Auth initialization

* Bypass the cache when rotating CAs

* Services.UpsertTrustedCluster is different

* Bypass the cache in waitForTunnelConnections

* Fix infinite recursion

* More cache bypassing during init and rotations

* Rename Services to Uncached in auth.Server

* Further cleanups

* Don't start the auth cache immediately

* Go back to Services rather than Uncached

* Comments and a missing method
2022-07-27 21:05:53 +00:00

593 lines
18 KiB
Go

/*
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"
"context"
"fmt"
"image/png"
"net/url"
"time"
"github.com/gravitational/trace"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
const (
// UserTokenTypeResetPasswordInvite is a token type used for the UI invite flow that
// allows users to change their password and set second factor (if enabled).
UserTokenTypeResetPasswordInvite = "invite"
// UserTokenTypeResetPassword is a token type used for the UI flow where user
// re-sets their password and second factor (if enabled).
UserTokenTypeResetPassword = "password"
// UserTokenTypeRecoveryStart describes a recovery token issued to users who
// successfully verified their recovery code.
UserTokenTypeRecoveryStart = "recovery_start"
// UserTokenTypeRecoveryApproved describes a recovery token issued to users who
// successfully verified their second auth credential (either password or a second factor) and
// can now start changing their password or add a new second factor device.
// This token is also used to allow users to delete exisiting second factor devices
// and retrieve their new set of recovery codes as part of the recovery flow.
UserTokenTypeRecoveryApproved = "recovery_approved"
// UserTokenTypePrivilege describes a token type that grants access to a privileged action
// that requires users to re-authenticate with their second factor while looged in. This
// token is issued to users who has successfully re-authenticated.
UserTokenTypePrivilege = "privilege"
// UserTokenTypePrivilegeException describes a token type that allowed a user to bypass
// second factor re-authentication which in other cases would be required eg:
// allowing user to add a mfa device if they don't have any registered.
UserTokenTypePrivilegeException = "privilege_exception"
)
// CreateUserTokenRequest is a request to create a new user token.
type CreateUserTokenRequest struct {
// Name is the user name for token.
Name string `json:"name"`
// TTL specifies how long the generated token is valid for.
TTL time.Duration `json:"ttl"`
// Type is the 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 = UserTokenTypeResetPassword
}
switch r.Type {
case UserTokenTypeResetPasswordInvite:
if r.TTL == 0 {
r.TTL = defaults.SignupTokenTTL
}
if r.TTL > defaults.MaxSignupTokenTTL {
return trace.BadParameter(
"failed to create user token for reset password invite: maximum token TTL is %v hours",
defaults.MaxSignupTokenTTL)
}
case UserTokenTypeResetPassword:
if r.TTL == 0 {
r.TTL = defaults.ChangePasswordTokenTTL
}
if r.TTL > defaults.MaxChangePasswordTokenTTL {
return trace.BadParameter(
"failed to create user token for reset password: maximum token TTL is %v hours",
defaults.MaxChangePasswordTokenTTL)
}
case UserTokenTypeRecoveryStart:
r.TTL = defaults.RecoveryStartTokenTTL
case UserTokenTypeRecoveryApproved:
r.TTL = defaults.RecoveryApprovedTokenTTL
case UserTokenTypePrivilege, UserTokenTypePrivilegeException:
r.TTL = defaults.PrivilegeTokenTTL
default:
return trace.BadParameter("unknown user token request type(%v)", r.Type)
}
return nil
}
// CreateResetPasswordToken creates a reset password token
func (s *Server) CreateResetPasswordToken(ctx context.Context, req CreateUserTokenRequest) (types.UserToken, error) {
err := req.CheckAndSetDefaults()
if err != nil {
return nil, trace.Wrap(err)
}
if req.Type != UserTokenTypeResetPassword && req.Type != UserTokenTypeResetPasswordInvite {
return nil, trace.BadParameter("invalid reset password token request type")
}
_, err = s.GetUser(req.Name, false)
if err != nil {
return nil, trace.Wrap(err)
}
_, err = s.ResetPassword(req.Name)
if err != nil {
return nil, trace.Wrap(err)
}
if err := s.resetMFA(ctx, req.Name); err != nil {
return nil, trace.Wrap(err)
}
token, err := s.newUserToken(req)
if err != nil {
return nil, trace.Wrap(err)
}
// remove any other existing tokens for this user
err = s.deleteUserTokens(ctx, req.Name)
if err != nil {
return nil, trace.Wrap(err)
}
_, err = s.CreateUserToken(ctx, token)
if err != nil {
return nil, trace.Wrap(err)
}
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserTokenCreate{
Metadata: apievents.Metadata{
Type: events.ResetPasswordTokenCreateEvent,
Code: events.ResetPasswordTokenCreateCode,
},
UserMetadata: ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: req.Name,
TTL: req.TTL.String(),
Expires: s.GetClock().Now().UTC().Add(req.TTL),
},
}); err != nil {
log.WithError(err).Warn("Failed to emit create reset password token event.")
}
return s.GetUserToken(ctx, token.GetName())
}
func (s *Server) resetMFA(ctx context.Context, user string) error {
devs, err := s.Services.GetMFADevices(ctx, user, false)
if err != nil {
return trace.Wrap(err)
}
var errs []error
for _, d := range devs {
errs = append(errs, s.DeleteMFADevice(ctx, user, d.Id))
}
return trace.NewAggregate(errs...)
}
// proxyDomainGetter is a reduced subset of the Auth API for formatAccountName.
type proxyDomainGetter interface {
GetProxies() ([]types.Server, error)
GetDomainName() (string, error)
}
// formatAccountName builds the account name to display in OTP applications.
// Format for accountName is user@address. User is passed in, this function
// tries to find the best available address.
func formatAccountName(s proxyDomainGetter, username string, authHostname string) (string, error) {
var err error
var proxyHost string
// Get a list of proxies.
proxies, err := s.GetProxies()
if err != nil {
return "", trace.Wrap(err)
}
// If no proxies were found, try and set address to the name of the cluster.
// If even the cluster name is not found (an unlikely) event, fallback to
// hostname of the auth server.
//
// If a proxy was found, and any of the proxies has a public address set,
// use that. If none of the proxies have a public address set, use the
// hostname of the first proxy found.
if len(proxies) == 0 {
proxyHost, err = s.GetDomainName()
if err != nil {
log.Errorf("Failed to retrieve cluster name, falling back to hostname: %v.", err)
proxyHost = authHostname
}
} else {
proxyHost, _, err = services.GuessProxyHostAndVersion(proxies)
if err != nil {
return "", trace.Wrap(err)
}
}
return fmt.Sprintf("%v@%v", username, proxyHost), nil
}
// DELETE IN 9.0.0 replaced by CreateRegisterChallenge.
//
// RotateUserTokenSecrets rotates secrets for a given tokenID.
// It gets called every time a user fetches 2nd-factor secrets during registration attempt.
// This ensures that an attacker that gains the user token 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 *Server) RotateUserTokenSecrets(ctx context.Context, tokenID string) (types.UserTokenSecrets, error) {
token, err := s.GetUserToken(ctx, tokenID)
if err != nil {
return nil, trace.Wrap(err)
}
otpKey, _, err := s.newTOTPKey(token.GetUser())
if err != nil {
return nil, trace.Wrap(err)
}
secrets, err := s.createTOTPUserTokenSecrets(ctx, token, otpKey)
return secrets, trace.Wrap(err)
}
// createTOTPUserTokenSecrets creates new UserTokenSecretes resource for the given token.
func (s *Server) createTOTPUserTokenSecrets(ctx context.Context, token types.UserToken, otpKey *otp.Key) (types.UserTokenSecrets, error) {
// Create QR code.
var otpQRBuf bytes.Buffer
otpImage, err := otpKey.Image(456, 456)
if err != nil {
return nil, trace.Wrap(err)
}
if err := png.Encode(&otpQRBuf, otpImage); err != nil {
return nil, trace.Wrap(err)
}
secrets, err := types.NewUserTokenSecrets(token.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
secrets.SetOTPKey(otpKey.Secret())
secrets.SetQRCode(otpQRBuf.Bytes())
secrets.SetExpiry(token.Expiry())
err = s.UpsertUserTokenSecrets(ctx, secrets)
if err != nil {
return nil, trace.Wrap(err)
}
return secrets, nil
}
func (s *Server) newTOTPKey(user string) (*otp.Key, *totp.GenerateOpts, error) {
// Fetch account name to display in OTP apps.
accountName, err := formatAccountName(s, user, s.AuthServiceName)
if err != nil {
return nil, nil, trace.Wrap(err)
}
clusterName, err := s.GetClusterName()
if err != nil {
return nil, nil, trace.Wrap(err)
}
opts := totp.GenerateOpts{
Issuer: clusterName.GetClusterName(),
AccountName: accountName,
Period: 30, // seconds
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
}
key, err := totp.Generate(opts)
if err != nil {
return nil, nil, trace.Wrap(err)
}
return key, &opts, nil
}
func (s *Server) newUserToken(req CreateUserTokenRequest) (types.UserToken, error) {
var err error
var proxyHost string
tokenLenBytes := TokenLenBytes
if req.Type == UserTokenTypeRecoveryStart {
tokenLenBytes = RecoveryTokenLenBytes
}
tokenID, err := utils.CryptoRandomHex(tokenLenBytes)
if err != nil {
return nil, trace.Wrap(err)
}
// Get the list of proxies and try and guess the address of the proxy. If
// failed to guess public address, use "<proxyhost>:3080" as a fallback.
proxies, err := s.GetProxies()
if err != nil {
return nil, trace.Wrap(err)
}
if len(proxies) == 0 {
proxyHost = fmt.Sprintf("<proxyhost>:%v", defaults.HTTPListenPort)
} else {
proxyHost, _, err = services.GuessProxyHostAndVersion(proxies)
if err != nil {
return nil, trace.Wrap(err)
}
}
url, err := formatUserTokenURL(proxyHost, tokenID, req.Type)
if err != nil {
return nil, trace.Wrap(err)
}
token, err := types.NewUserToken(tokenID)
if err != nil {
return nil, trace.Wrap(err)
}
token.SetSubKind(req.Type)
token.SetExpiry(s.clock.Now().UTC().Add(req.TTL))
token.SetUser(req.Name)
token.SetCreated(s.clock.Now().UTC())
token.SetURL(url)
return token, nil
}
func formatUserTokenURL(proxyHost string, tokenID string, reqType string) (string, error) {
u := &url.URL{
Scheme: "https",
Host: proxyHost,
}
// Defines different UI flows that process user tokens.
switch reqType {
case UserTokenTypeResetPasswordInvite:
u.Path = fmt.Sprintf("/web/invite/%v", tokenID)
case UserTokenTypeResetPassword:
u.Path = fmt.Sprintf("/web/reset/%v", tokenID)
case UserTokenTypeRecoveryStart:
u.Path = fmt.Sprintf("/web/recovery/steps/%v/verify", tokenID)
}
return u.String(), nil
}
// deleteUserTokens deletes all user tokens for the specified user.
func (s *Server) deleteUserTokens(ctx context.Context, username string) error {
tokens, err := s.GetUserTokens(ctx)
if err != nil {
return trace.Wrap(err)
}
for _, token := range tokens {
if token.GetUser() != username {
continue
}
err = s.DeleteUserToken(ctx, token.GetName())
if err != nil {
return trace.Wrap(err)
}
}
return nil
}
// getResetPasswordToken returns user token with subkind set to reset or invite, both
// types which allows users to change their password and set new second factors (if enabled).
func (s *Server) getResetPasswordToken(ctx context.Context, tokenID string) (types.UserToken, error) {
token, err := s.GetUserToken(ctx, tokenID)
if err != nil {
return nil, trace.Wrap(err)
}
// DELETE IN 9.0.0: remove checking for empty string.
if token.GetSubKind() != "" && token.GetSubKind() != UserTokenTypeResetPassword && token.GetSubKind() != UserTokenTypeResetPasswordInvite {
return nil, trace.BadParameter("invalid token")
}
return token, nil
}
// createRecoveryToken creates a user token for account recovery.
func (s *Server) createRecoveryToken(ctx context.Context, username, tokenType string, usage types.UserTokenUsage) (types.UserToken, error) {
if tokenType != UserTokenTypeRecoveryStart && tokenType != UserTokenTypeRecoveryApproved {
return nil, trace.BadParameter("invalid recovery token type: %s", tokenType)
}
if usage != types.UserTokenUsage_USER_TOKEN_RECOVER_MFA && usage != types.UserTokenUsage_USER_TOKEN_RECOVER_PASSWORD {
return nil, trace.BadParameter("invalid recovery token usage type %s", usage.String())
}
req := CreateUserTokenRequest{
Name: username,
Type: tokenType,
}
if err := req.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
newToken, err := s.newUserToken(req)
if err != nil {
return nil, trace.Wrap(err)
}
// Mark what recover type user requested.
newToken.SetUsage(usage)
if _, err := s.CreateUserToken(ctx, newToken); err != nil {
return nil, trace.Wrap(err)
}
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserTokenCreate{
Metadata: apievents.Metadata{
Type: events.RecoveryTokenCreateEvent,
Code: events.RecoveryTokenCreateCode,
},
UserMetadata: apievents.UserMetadata{
User: username,
},
ResourceMetadata: apievents.ResourceMetadata{
Name: req.Name,
TTL: req.TTL.String(),
Expires: s.GetClock().Now().UTC().Add(req.TTL),
},
}); err != nil {
log.WithError(err).Warn("Failed to emit create recovery token event.")
}
return newToken, nil
}
// CreatePrivilegeToken implements AuthService.CreatePrivilegeToken.
func (s *Server) CreatePrivilegeToken(ctx context.Context, req *proto.CreatePrivilegeTokenRequest) (*types.UserTokenV3, error) {
username, err := GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
authPref, err := s.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// For a user to add a device, second factor must be enabled.
// A nil request will be interpreted as a user who has second factor enabled
// but does not have any MFA registered, as can be the case with second factor optional.
if authPref.GetSecondFactor() == constants.SecondFactorOff {
return nil, trace.AccessDenied("second factor must be enabled")
}
tokenKind := UserTokenTypePrivilege
switch {
case req.GetExistingMFAResponse() == nil:
// Allows users with no devices to bypass second factor re-auth.
devices, err := s.Services.GetMFADevices(ctx, username, false /* withSecrets */)
switch {
case err != nil:
return nil, trace.Wrap(err)
case len(devices) > 0:
return nil, trace.BadParameter("second factor authentication required")
}
tokenKind = UserTokenTypePrivilegeException
default:
if err := s.WithUserLock(username, func() error {
_, _, err := s.validateMFAAuthResponse(
ctx, req.GetExistingMFAResponse(), username, false /* passwordless */)
return err
}); err != nil {
return nil, trace.Wrap(err)
}
}
// Delete any existing user tokens for user before creating.
if err := s.deleteUserTokens(ctx, username); err != nil {
return nil, trace.Wrap(err)
}
token, err := s.createPrivilegeToken(ctx, username, tokenKind)
return token, trace.Wrap(err)
}
func (s *Server) createPrivilegeToken(ctx context.Context, username, tokenKind string) (*types.UserTokenV3, error) {
if tokenKind != UserTokenTypePrivilege && tokenKind != UserTokenTypePrivilegeException {
return nil, trace.BadParameter("invalid privilege token type")
}
req := CreateUserTokenRequest{
Name: username,
Type: tokenKind,
}
if err := req.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
newToken, err := s.newUserToken(req)
if err != nil {
return nil, trace.Wrap(err)
}
token, err := s.CreateUserToken(ctx, newToken)
if err != nil {
return nil, trace.Wrap(err)
}
if err := s.emitter.EmitAuditEvent(ctx, &apievents.UserTokenCreate{
Metadata: apievents.Metadata{
Type: events.PrivilegeTokenCreateEvent,
Code: events.PrivilegeTokenCreateCode,
},
UserMetadata: apievents.UserMetadata{
User: username,
},
ResourceMetadata: apievents.ResourceMetadata{
Name: req.Name,
TTL: req.TTL.String(),
Expires: s.GetClock().Now().UTC().Add(req.TTL),
},
}); err != nil {
log.WithError(err).Warn("Failed to emit create privilege token event.")
}
convertedToken, ok := token.(*types.UserTokenV3)
if !ok {
return nil, trace.BadParameter("unexpected UserToken type %T", token)
}
return convertedToken, nil
}
// verifyUserToken verifies that the token is not expired and is of the allowed kinds.
func (s *Server) verifyUserToken(token types.UserToken, allowedKinds ...string) error {
if token.Expiry().Before(s.clock.Now().UTC()) {
// Provide obscure message on purpose, while logging the real error server side.
log.Debugf("Expired token(%s) type(%s)", token.GetName(), token.GetSubKind())
return trace.AccessDenied("invalid token")
}
for _, kind := range allowedKinds {
if token.GetSubKind() == kind {
return nil
}
}
log.Debugf("Invalid token(%s) type(%s), expected type: %v", token.GetName(), token.GetSubKind(), allowedKinds)
return trace.AccessDenied("invalid token")
}