Files
teleport/lib/auth/methods.go

1059 lines
37 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth
import (
"bytes"
"context"
"crypto/x509"
"encoding/pem"
"errors"
"net"
"strings"
"time"
"github.com/coreos/go-semver/semver"
"github.com/gravitational/trace"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/userexternalsecrets/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/internal/cert"
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/scopes"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
const (
// maxUserAgentLen is the maximum length of a user agent that will be logged.
// There is no current consensus on what the maximum length of a User-Agent
// should be and there were reports of extremely large UAs especially from
// older versions of IE. 2048 was picked because it still allowed for very
// large UAs but keeps from causing logging issues. For reference Nginx
// defaults to 4k or 8k header size limits for ALL headers so 2k seems more
// than sufficient.
maxUserAgentLen = 2048
)
func (a *Server) AccessCheckerForScope(ctx context.Context, scope string, userState services.UserState, allowedResourceAccessIDs []types.ResourceAccessID) (*services.ScopedAccessCheckerContext, error) {
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// scoped and unscoped logins use different access checkers and different underlying role types. the scope
// parameter is untrusted user input, but we reject attempts to login to a scope for which a user has no
// assigned privileges.
if scope == "" {
// this is an unscoped login attempt, so the user's capabilities are determined solely by their user state.
accessInfo := services.AccessInfoFromUserState(userState)
if len(allowedResourceAccessIDs) > 0 {
accessInfo.AllowedResourceAccessIDs = allowedResourceAccessIDs
}
unscopedChecker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), a)
if err != nil {
return nil, trace.Wrap(err)
}
return services.NewScopedAccessCheckerContextFromUnscoped(unscopedChecker), nil
}
// req.Scope is untrusted user input, so perform strong validation before proceeding.
if err := scopes.StrongValidate(scope); err != nil {
return nil, trace.Wrap(err)
}
// set up scope pin (invalid until populated)
scopePin := &scopesv1.Pin{
Kind: scopesv1.PinKind_PIN_KIND_USER,
Scope: scope,
}
if userState.IsBot() {
botScope, _ := userState.GetLabel(types.BotScopeLabel)
botName, _ := userState.GetLabel(types.BotLabel)
if botScope == "" {
return nil, trace.BadParameter("unscoped bot may not generate scoped certs")
}
if err := a.ScopedAccessCache.PopulatePinnedAssignmentsForBot(
ctx, botName, botScope, scopePin,
); err != nil {
return nil, trace.Wrap(err)
}
} else {
if err := a.ScopedAccessCache.PopulatePinnedAssignmentsForUser(
ctx, userState.GetName(), scopePin,
); err != nil {
return nil, trace.Wrap(err)
}
}
// build the user's access info based on the scope pin and userState
accessInfo := services.ScopePinnedAccessInfoFromUserState(userState, scopePin)
if len(allowedResourceAccessIDs) > 0 {
accessInfo.AllowedResourceAccessIDs = allowedResourceAccessIDs
}
// create a scoped access checker context. Note that scoped certificate parameters differ from unscoped
// certificate parameters in that they are generally not role-determined. We still need to create the full
// checker context as there are select exceptions where scoped roles do matter, and because the checker context
// abstraction also handles non-role-determined certificate perameter calculation.
scopedCheckerContext, err := services.NewScopedAccessCheckerContext(ctx, accessInfo, clusterName.GetClusterName(), a.ScopedAccessCache)
if err != nil {
return nil, trace.Wrap(err)
}
return scopedCheckerContext, nil
}
// authenticateUserLogin implements the bulk of user login authentication.
// Used by the top-level local login methods, [Server.AuthenticateSSHUser] and
// [Server.AuthenticateWebUser]
func (a *Server) authenticateUserLogin(ctx context.Context, req authclient.AuthenticateUserRequest) (services.UserState, *services.ScopedAccessCheckerContext, error) {
username := req.Username
requiredExt := mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
}
verifyMFALocks, mfaDev, actualUsername, err := a.authenticateUser(
ctx, req, requiredExt)
if err != nil {
// Log event after authentication failure
if err := a.emitAuthAuditEvent(ctx, authAuditProps{
username: req.Username,
clientMetadata: req.ClientMetadata,
authErr: err,
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit login event", "error", err)
}
return nil, nil, trace.Wrap(err)
}
switch {
case username != "" && actualUsername != "" && username != actualUsername:
a.logger.WarnContext(ctx, "Authenticate user mismatch, using request user",
"username", username,
"request_user", actualUsername,
)
case username == "" && actualUsername != "":
a.logger.DebugContext(ctx, "User authenticated via passwordless", "username", actualUsername)
username = actualUsername
}
user, err := a.GetUser(ctx, username, false /* withSecrets */)
if err != nil {
return nil, nil, trace.Wrap(err)
}
// After we're sure that the user has been logged in successfully, we should call
// the registered login hooks. Login hooks can be registered by other processes to
// execute arbitrary operations after a successful login.
if err := a.CallLoginHooks(ctx, user); err != nil {
return nil, nil, trace.Wrap(err)
}
userState, err := a.GetUserOrLoginState(ctx, user.GetName())
if err != nil {
return nil, nil, trace.Wrap(err)
}
checker, err := a.AccessCheckerForScope(ctx, req.Scope, userState, nil)
if err != nil {
return nil, nil, trace.Wrap(err)
}
// Verify if the MFA device is locked.
if err := verifyMFALocks(verifyMFADeviceLocksParams{
CheckerContext: checker,
}); err != nil {
// Log MFA lock failure as an authn failure.
if err := a.emitAuthAuditEvent(ctx, authAuditProps{
username: req.Username,
clientMetadata: req.ClientMetadata,
mfaDevice: mfaDev,
checkerContext: checker,
authErr: err,
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit login event", "error", err)
}
return nil, nil, trace.Wrap(err)
}
// Log event after authentication success
if err := a.emitAuthAuditEvent(ctx, authAuditProps{
username: username,
clientMetadata: req.ClientMetadata,
mfaDevice: mfaDev,
checkerContext: checker,
userOrigin: userOrigin(user),
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit login event", "error", err)
}
return userState, checker, trace.Wrap(err)
}
type authAuditProps struct {
username string
clientMetadata *authclient.ForwardedClientMetadata
mfaDevice *types.MFADevice
checkerContext *services.ScopedAccessCheckerContext
authErr error
userOrigin apievents.UserOrigin
}
// trimStr truncates the length of a string, appending '...' for strings
// that are truncated. If the string is truncated, the result will have
// len of max + 3 (for the ellipsis).
func trimStr(str string, max int) string {
if len(str) <= max {
return str
}
return str[:max] + "..."
}
func (a *Server) emitAuthAuditEvent(ctx context.Context, props authAuditProps) error {
event := &apievents.UserLogin{
Metadata: apievents.Metadata{
Type: events.UserLoginEvent,
Code: events.UserLocalLoginCode,
},
Status: apievents.Status{
Success: true,
},
UserMetadata: apievents.UserMetadata{
User: trimStr(props.username, teleport.MaxUsernameLength),
UserOrigin: props.userOrigin,
},
Method: events.LoginMethodLocal,
}
if props.authErr != nil {
event.Code = events.UserLocalLoginFailureCode
event.Status.Success = false
event.Status.Error = props.authErr.Error()
}
if props.clientMetadata != nil {
event.RemoteAddr = props.clientMetadata.RemoteAddr
event.UserAgent = trimUserAgent(props.clientMetadata.UserAgent)
}
if props.mfaDevice != nil {
m := mfaDeviceEventMetadata(props.mfaDevice)
event.MFADevice = &m
}
// Add required key policy to the event.
if props.checkerContext != nil {
authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}
// TODO(fspmarshall/scopes): its a little strange to use CertParams here as this isn't really a certificate
// specific calculation. Once we have a clearer idea of what PrivateKeyPolicy means in a scoped context,
// consider moving this method to a more appropriate location.
privateKeyPolicy, err := props.checkerContext.CertParams().PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
if err != nil {
return trace.Wrap(err)
}
event.RequiredPrivateKeyPolicy = string(privateKeyPolicy)
}
return trace.Wrap(a.emitter.EmitAuditEvent(a.closeCtx, event))
}
var (
// authenticateWebauthnError is the generic error returned for failed WebAuthn
// authentication attempts.
authenticateWebauthnError = &trace.AccessDeniedError{Message: "invalid credentials"}
// errSSOUserLocalAuth is issued for SSO users attempting local authentication
// or related actions (like trying to set a password)
// Kept purposefully vague, as such actions don't happen during normal
// utilization of the system.
errSSOUserLocalAuth = &trace.AccessDeniedError{Message: "invalid credentials"}
)
type verifyMFADeviceLocksParams struct {
// CheckerContext is used to verify locks. Lock verification varies depending on whether the context is
// scoped or unscoped. If no context is provided, a default unscoped checker context is created from
// the user backend state.
CheckerContext *services.ScopedAccessCheckerContext
// ClusterLockingMode used to verify locks.
// Optional, acquired from [Server.GetAuthPreference] if nil.
ClusterLockingMode constants.LockingMode
}
// authenticateUser authenticates a user through various methods (password, MFA,
// passwordless). Note that "authenticate" may mean different things, depending
// on the request and required extensions. The caller is responsible for making
// sure that the request and extensions correctly describe the conditions that
// are being asserted during the authentication process. One important caveat is
// that if the request contains a WebAuthn challenge response, the user name is
// known, and `authenticateUser` is called without a password, the function only
// performs a second factor check; in this case, the caller must provide
// appropriate required extensions to validate the WebAuthn challenge response.
//
// The `requiredExt` parameter is used to assert that the MFA challenge has a
// correct extensions. It is ignored for passwordless authentication, where we
// override it with an extension that has scope set to
// `mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN`.
//
// Returns a callback to verify MFA device locks, the MFA device used to
// authenticate (if applicable), and the authenticated user name.
//
// Callers MUST call the verifyLocks callback.
func (a *Server) authenticateUser(
ctx context.Context,
req authclient.AuthenticateUserRequest,
requiredExt mfav1.ChallengeExtensions,
) (verifyLocks func(verifyMFADeviceLocksParams) error, mfaDev *types.MFADevice, user string, err error) {
mfaDev, user, err = a.authenticateUserInternal(ctx, req, requiredExt)
if err != nil || mfaDev == nil {
return func(verifyMFADeviceLocksParams) error { return nil }, mfaDev, user, trace.Wrap(err)
}
verifyLocks = func(p verifyMFADeviceLocksParams) error {
if p.CheckerContext == nil {
userState, err := a.GetUserOrLoginState(ctx, user)
if err != nil {
return trace.Wrap(err)
}
accessInfo := services.AccessInfoFromUserState(userState)
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return trace.Wrap(err)
}
unscopedChecker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), a)
if err != nil {
return trace.Wrap(err)
}
p.CheckerContext = services.NewScopedAccessCheckerContextFromUnscoped(unscopedChecker)
}
if p.ClusterLockingMode == "" {
authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}
p.ClusterLockingMode = authPref.GetLockingMode()
}
// The MFA device needs to be explicitly verified, as it won't be verified
// as part of certificate issuance in various scenarios (password change,
// non-session certificates, etc)
return a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{
checkerContext: p.CheckerContext,
defaultMode: p.ClusterLockingMode,
username: user,
mfaVerified: mfaDev.Id,
})
}
return verifyLocks, mfaDev, user, nil
}
// Do not use this method directly, use authenticateUser instead.
func (a *Server) authenticateUserInternal(
ctx context.Context,
req authclient.AuthenticateUserRequest,
requiredExt mfav1.ChallengeExtensions,
) (mfaDev *types.MFADevice, user string, err error) {
if err := req.CheckAndSetDefaults(); err != nil {
return nil, "", trace.Wrap(err)
}
user = req.Username
passwordless := user == ""
// Headless auth is treated more like sso login or access requests than local login,
// meaning it should be allowed for non-local users and failed headless auth attempts
// should not lock out the user (we skip the WithUserLock wrapper).
if req.HeadlessAuthenticationID != "" {
mfaDev, err = a.authenticateHeadless(ctx, req)
if err != nil {
a.logger.DebugContext(ctx, "Headless authenticate failed while waiting for approval",
"user", user,
"error", err,
)
return nil, "", trace.Wrap(err)
}
return mfaDev, user, nil
}
// Only one path if passwordless, other variants shouldn't see an empty user.
if passwordless {
if requiredExt.Scope != mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN {
return nil, "", trace.BadParameter(
"passwordless authentication can only be used with the PASSWORDLESS scope")
}
return a.authenticatePasswordless(ctx, req)
}
// Disallow non-local users from local authentication.
// Passwordless does its own check, as the user is only known after the
// webauthn assertion is cleared.
switch u, err := a.GetUser(ctx, user, false /* withSecrets */); {
case trace.IsNotFound(err):
// Keep going if the user is not known.
case err != nil:
return nil, "", trace.Wrap(err)
case u.GetUserType() != types.UserTypeLocal:
a.logger.WarnContext(ctx, "Non-local user attempted local authentication",
"user", user,
"user_type", u.GetUserType(),
)
return nil, "", trace.Wrap(errSSOUserLocalAuth)
}
// Try 2nd-factor-enabled authentication schemes first.
var authenticateFn func() (*types.MFADevice, error)
var authErr error // error message kept obscure on purpose, use logging for details
switch {
// cases in order of preference
case req.Webauthn != nil:
authenticateFn = func() (*types.MFADevice, error) {
if req.Pass != nil {
if err = a.checkPasswordWOToken(ctx, user, req.Pass.Password); err != nil {
return nil, trace.Wrap(err)
}
}
mfaResponse := &proto.MFAAuthenticateResponse{
Response: &proto.MFAAuthenticateResponse_Webauthn{
Webauthn: wantypes.CredentialAssertionResponseToProto(req.Webauthn),
},
}
mfaData, err := a.ValidateMFAAuthResponse(ctx, mfaResponse, user, &requiredExt)
if err != nil {
return nil, trace.Wrap(err)
}
return mfaData.Device, nil
}
authErr = authenticateWebauthnError
case req.BrowserMFA != nil:
authenticateFn = func() (*types.MFADevice, error) {
if req.Pass != nil {
if err = a.checkPasswordWOToken(ctx, user, req.Pass.Password); err != nil {
return nil, trace.Wrap(err)
}
}
mfaResponse := &proto.MFAAuthenticateResponse{
Response: &proto.MFAAuthenticateResponse_Browser{
Browser: req.BrowserMFA,
},
}
mfaData, err := a.ValidateMFAAuthResponse(ctx, mfaResponse, user, &requiredExt)
if err != nil {
return nil, trace.Wrap(err)
}
return mfaData.Device, nil
}
authErr = authenticateWebauthnError
case req.OTP != nil:
authenticateFn = func() (*types.MFADevice, error) {
// OTP cannot be validated by validateMFAAuthResponse because we need to
// check the user's password too.
res, err := a.checkPassword(ctx, user, req.OTP.Password, req.OTP.Token)
if err != nil {
return nil, trace.Wrap(err)
}
return res.mfaDev, nil
}
authErr = authclient.InvalidUserPass2FError
}
if authenticateFn != nil {
err := a.WithUserLock(ctx, user, func() error {
var err error
mfaDev, err = authenticateFn()
return err
})
switch {
case err != nil:
a.logger.DebugContext(ctx, "User failed to authenticate",
"user", user,
"error", err,
)
if fieldErr := getErrorByTraceField(err); fieldErr != nil {
return nil, "", trace.Wrap(fieldErr)
}
return nil, "", trace.Wrap(authErr)
case mfaDev == nil:
a.logger.DebugContext(ctx, "MFA authentication returned nil device",
"webauthn", req.Webauthn != nil,
"totp", req.OTP != nil,
"headless", req.HeadlessAuthenticationID != "",
"error", err,
)
return nil, "", trace.Wrap(authErr)
default:
return mfaDev, user, nil
}
}
// Try password-only authentication last.
if req.Pass == nil {
return nil, "", trace.AccessDenied("unsupported authentication method")
}
authPreference, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, "", trace.Wrap(err)
}
// When using password only make sure that auth preference does not require
// second factor, otherwise users could bypass it.
switch {
case authPreference.IsSecondFactorEnforced():
// Some form of MFA is required but none provided. Either client is
// buggy (didn't send MFA response) or someone is trying to bypass
// MFA.
a.logger.WarnContext(ctx, "MFA bypass attempt, access denied", "user", user)
return nil, "", trace.AccessDenied("missing second factor")
case authPreference.IsSecondFactorEnabled():
// 2FA is optional. Make sure that a user does not have MFA devices
// registered.
devs, err := a.Services.GetMFADevices(ctx, user, false /* withSecrets */)
if err != nil && !trace.IsNotFound(err) {
return nil, "", trace.Wrap(err)
}
if len(devs) != 0 {
a.logger.WarnContext(ctx, "MFA bypass attempt, access denied", "user", user)
return nil, "", trace.AccessDenied("missing second factor authentication")
}
default:
// No 2FA required, check password only.
}
if err = a.WithUserLock(ctx, user, func() error {
return a.checkPasswordWOToken(ctx, user, req.Pass.Password)
}); err != nil {
if fieldErr := getErrorByTraceField(err); fieldErr != nil {
return nil, "", trace.Wrap(fieldErr)
}
// provide obscure message on purpose, while logging the real
// error server side
a.logger.DebugContext(ctx, "User failed to authenticate",
"user", user,
"error", err,
)
return nil, "", trace.Wrap(authclient.InvalidUserPassError)
}
return nil, user, nil
}
func (a *Server) authenticatePasswordless(ctx context.Context, req authclient.AuthenticateUserRequest) (*types.MFADevice, string, error) {
mfaResponse := &proto.MFAAuthenticateResponse{
Response: &proto.MFAAuthenticateResponse_Webauthn{
Webauthn: wantypes.CredentialAssertionResponseToProto(req.Webauthn),
},
}
requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN}
mfaData, err := a.ValidateMFAAuthResponse(ctx, mfaResponse, "" /* user */, requiredExt)
switch {
// Don't obfuscate the SSO error.
case errors.Is(err, types.ErrPassswordlessLoginBySSOUser):
return nil, "", trace.Wrap(err)
case err != nil:
a.logger.DebugContext(ctx, "Passwordless authentication failed", "error", err)
return nil, "", trace.Wrap(authenticateWebauthnError)
}
// A distinction between passwordless and "plain" MFA is that we can't
// acquire the user lock beforehand (or at all on failures!)
// We do grab it here so successful logins go through the regular process.
if err := a.WithUserLock(ctx, mfaData.User, func() error { return nil }); err != nil {
a.logger.DebugContext(ctx, "WithUserLock failed during passwordless authentication",
"user", mfaData.User,
"error", err,
)
return nil, mfaData.User, trace.Wrap(authenticateWebauthnError)
}
return mfaData.Device, mfaData.User, nil
}
func (a *Server) authenticateHeadless(ctx context.Context, req authclient.AuthenticateUserRequest) (mfa *types.MFADevice, err error) {
// Delete the headless authentication upon failure.
defer func() {
if err != nil {
if err := a.DeleteHeadlessAuthentication(a.CloseContext(), req.Username, req.HeadlessAuthenticationID); err != nil && !trace.IsNotFound(err) {
a.logger.DebugContext(ctx, "Failed to delete headless authentication", "error", err)
}
}
}()
// this authentication requires two client callbacks to create a headless authentication
// stub and approve/deny the headless authentication.
ctx, cancel := context.WithTimeout(ctx, defaults.HeadlessLoginTimeout)
defer cancel()
// Headless Authentication should expire when the callback expires.
expires := a.clock.Now().Add(defaults.HeadlessLoginTimeout)
// Create the headless authentication and validate request details.
ha, err := types.NewHeadlessAuthentication(req.Username, req.HeadlessAuthenticationID, expires)
if err != nil {
return nil, trace.Wrap(err)
}
ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING
ha.SshPublicKey = req.SSHPublicKey
ha.TlsPublicKey = req.TLSPublicKey
ha.ClientIpAddress = req.ClientMetadata.RemoteAddr
if err := services.ValidateHeadlessAuthentication(ha); err != nil {
return nil, trace.Wrap(err)
}
emitHeadlessLoginEvent(ctx, events.UserHeadlessLoginRequestedCode, a.emitter, ha, nil)
// HTTP server has shorter WriteTimeout than is needed, so we override WriteDeadline of the connection.
if conn, err := authz.ConnFromContext(ctx); err == nil {
if err := conn.SetWriteDeadline(a.GetClock().Now().Add(defaults.HeadlessLoginTimeout)); err != nil {
return nil, trace.Wrap(err)
}
}
// Headless authentication requests are made without any prior authentication. To avoid DDos
// attacks on the Auth server's backend, we don't create the headless authentication in the
// backend until an authenticated client creates a headless authentication stub. This serves
// as indirect authorization to insert the full headless authentication details into the backend.
if _, err := a.waitForHeadlessStub(ctx, ha); err != nil {
return nil, trace.Wrap(err)
}
if err := a.UpsertHeadlessAuthentication(ctx, ha); err != nil {
return nil, trace.Wrap(err)
}
// Wait for the request to be approved/denied.
approvedHeadlessAuthn, err := a.waitForHeadlessApproval(ctx, req.Username, req.HeadlessAuthenticationID)
if err != nil {
return nil, trace.Wrap(err)
}
// Verify that the headless authentication has not been tampered with.
if approvedHeadlessAuthn.User != req.Username {
return nil, trace.AccessDenied("headless authentication user mismatch")
}
if !bytes.Equal(req.SSHPublicKey, ha.SshPublicKey) {
return nil, trace.AccessDenied("headless authentication SSH public key mismatch")
}
if !bytes.Equal(req.TLSPublicKey, ha.TlsPublicKey) {
return nil, trace.AccessDenied("headless authentication TLS public key mismatch")
}
return approvedHeadlessAuthn.MfaDevice, nil
}
func (a *Server) waitForHeadlessStub(ctx context.Context, ha *types.HeadlessAuthentication) (*types.HeadlessAuthentication, error) {
sub, err := a.headlessAuthenticationWatcher.Subscribe(ctx, ha.User, services.HeadlessAuthenticationUserStubID)
if err != nil {
return nil, trace.Wrap(err)
}
defer sub.Close()
stub, err := sub.WaitForUpdate(ctx, func(ha *types.HeadlessAuthentication) (bool, error) {
return true, nil
})
if err != nil {
return nil, trace.Wrap(err)
}
return stub, nil
}
func (a *Server) waitForHeadlessApproval(ctx context.Context, username, reqID string) (*types.HeadlessAuthentication, error) {
sub, err := a.headlessAuthenticationWatcher.Subscribe(ctx, username, reqID)
if err != nil {
return nil, trace.Wrap(err)
}
defer sub.Close()
headlessAuthn, err := sub.WaitForUpdate(ctx, func(ha *types.HeadlessAuthentication) (bool, error) {
switch ha.State {
case types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED:
if ha.MfaDevice == nil {
return false, trace.AccessDenied("expected mfa approval for headless authentication approval")
}
return true, nil
case types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED:
return false, trace.AccessDenied("headless authentication denied")
}
return false, nil
})
if err != nil {
return nil, trace.Wrap(err)
}
return headlessAuthn, nil
}
// AuthenticateWebUser authenticates web user, creates and returns a web session
// if authentication is successful. In case the existing session ID is used to authenticate,
// returns the existing session instead of creating a new one
func (a *Server) AuthenticateWebUser(ctx context.Context, req authclient.AuthenticateUserRequest) (types.WebSession, error) {
username := req.Username // Empty if passwordless.
authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// Disable all local auth requests,
// except session ID renewal requests that are using the same method.
// This condition uses Session as a blanket check, because any new method added
// to the local auth will be disabled by default.
if !authPref.GetAllowLocalAuth() && req.Session == nil {
a.emitNoLocalAuthEvent(username)
return nil, trace.AccessDenied("%s", noLocalAuth)
}
if req.Session != nil {
session, err := a.GetWebSession(ctx, types.GetWebSessionRequest{
User: username,
SessionID: req.Session.ID,
})
if err != nil {
return nil, trace.AccessDenied("session is invalid or has expired")
}
return session, nil
}
user, _, err := a.authenticateUserLogin(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
var loginIP, userAgent, proxyGroupID string
var maxTouchPoints int
if cm := req.ClientMetadata; cm != nil {
loginIP, _, err = net.SplitHostPort(cm.RemoteAddr)
if err != nil {
return nil, trace.Wrap(err)
}
userAgent = cm.UserAgent
proxyGroupID = cm.ProxyGroupID
maxTouchPoints = cm.MaxTouchPoints
}
sess, err := a.CreateWebSessionFromReq(ctx, NewWebSessionRequest{
User: user.GetName(),
LoginIP: loginIP,
LoginUserAgent: userAgent,
LoginMaxTouchPoints: maxTouchPoints,
ProxyGroupID: proxyGroupID,
Roles: user.GetRoles(),
Traits: user.GetTraits(),
LoginTime: a.clock.Now().UTC(),
AttestWebSession: true,
CreateDeviceWebToken: true,
Scope: req.Scope,
})
if err != nil {
return nil, trace.Wrap(err)
}
return sess, nil
}
// AuthenticateSSHUser authenticates an SSH user and returns SSH and TLS
// certificates for the public key in req.
func (a *Server) AuthenticateSSHUser(ctx context.Context, req authclient.AuthenticateSSHRequest) (*authclient.CLILoginResponse, error) {
if err := req.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
username := req.Username // Empty if passwordless.
authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// Disable all local auth requests, except headless requests.
if !authPref.GetAllowLocalAuth() && req.HeadlessAuthenticationID == "" {
a.emitNoLocalAuthEvent(username)
return nil, trace.AccessDenied("%s", noLocalAuth)
}
clusterName, err := a.GetClusterName(ctx)
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.
user, checker, err := a.authenticateUserLogin(ctx, req.AuthenticateUserRequest)
if err != nil {
return nil, trace.Wrap(err)
}
// Return the host CA for this cluster only.
authority, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: clusterName.GetClusterName(),
}, false)
if err != nil {
return nil, trace.Wrap(err)
}
hostCertAuthorities := []types.CertAuthority{
authority,
}
clientIP := ""
if req.ClientMetadata != nil && req.ClientMetadata.RemoteAddr != "" {
host, err := utils.Host(req.ClientMetadata.RemoteAddr)
if err != nil {
return nil, trace.Wrap(err)
}
clientIP = host
}
if checker.CertParams().PinSourceIP() && clientIP == "" {
return nil, trace.BadParameter("source IP pinning is enabled but client IP is unknown")
}
var encryptionKeyID string
if len(req.EncryptionPublicKey) > 0 {
encryptionKeyID = sessionIDFromPubKey(req.EncryptionPublicKey)
}
certReq := cert.Request{
User: user,
TTL: req.TTL,
SSHPublicKey: req.SSHPublicKey,
TLSPublicKey: req.TLSPublicKey,
Compatibility: req.CompatibilityMode,
CheckerContext: checker,
Traits: user.GetTraits(),
RouteToCluster: req.RouteToCluster,
KubernetesCluster: req.KubernetesCluster,
LoginIP: clientIP,
EncryptionKeyID: encryptionKeyID,
SSHPublicKeyAttestationStatement: req.SSHAttestationStatement,
TLSPublicKeyAttestationStatement: req.TLSAttestationStatement,
}
// For headless authentication, a short-lived mfa-verified cert should be generated.
if req.HeadlessAuthenticationID != "" {
ha, err := a.GetHeadlessAuthentication(ctx, req.Username, req.HeadlessAuthenticationID)
if err != nil {
return nil, trace.Wrap(err)
}
if !bytes.Equal(req.SSHPublicKey, ha.SshPublicKey) {
return nil, trace.AccessDenied("headless authentication SSH public key mismatch")
}
if !bytes.Equal(req.TLSPublicKey, ha.TlsPublicKey) {
return nil, trace.AccessDenied("headless authentication TLS public key mismatch")
}
certReq.MFAVerified = ha.MfaDevice.Metadata.Name
certReq.TTL = time.Minute
certReq.HeadlessAuthenticationID = req.HeadlessAuthenticationID
}
certs, err := a.GenerateUserCerts(ctx, certReq)
if err != nil {
return nil, trace.Wrap(err)
}
var userAgent, proxyGroupID string
if req.ClientMetadata != nil {
userAgent = req.ClientMetadata.UserAgent
proxyGroupID = req.ClientMetadata.ProxyGroupID
}
UserLoginCount.Inc()
userAgentType, userAgentVersion := splitClientUserAgent(userAgent)
userLoginCountPerClient.With(prometheus.Labels{
tagUserAgentType: userAgentType,
tagVersion: userAgentVersion,
tagProxyGroupID: proxyGroupID,
}).Inc()
// Register the session encryption key if provided. Use the actual cert
// expiry (from the issued TLS cert) rather than req.TTL, since auth adjusts
// the TTL based on role limits.
if len(req.EncryptionPublicKey) > 0 {
sessionID := sessionIDFromPubKey(req.EncryptionPublicKey)
certExpiry := a.clock.Now().Add(req.TTL)
a.logger.DebugContext(ctx, "Encryption key TTL from req.TTL",
"req_ttl", req.TTL,
"fallback_expiry", certExpiry,
)
if len(certs.TLS) > 0 {
block, _ := pem.Decode(certs.TLS)
if block == nil {
a.logger.WarnContext(ctx, "Failed to PEM-decode TLS cert for encryption key TTL")
} else {
tlsCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
a.logger.WarnContext(ctx, "Failed to parse TLS cert for encryption key TTL", "error", err)
} else {
certExpiry = tlsCert.NotAfter
a.logger.DebugContext(ctx, "Encryption key TTL from TLS cert",
"cert_not_after", tlsCert.NotAfter,
)
}
}
} else {
a.logger.WarnContext(ctx, "No TLS cert available for encryption key TTL")
}
credsSvc := a.Services.UserSessionCredentials
resource := pb.UserSessionCredentials_builder{
Kind: types.KindUserSessionCredentials,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: sessionID,
Expires: timestamppb.New(certExpiry),
},
Spec: pb.UserSessionCredentialsSpec_builder{
User: user.GetName(),
EncryptionKey: pb.EncryptionKey_builder{
KeyId: sessionID,
PublicKey: req.EncryptionPublicKey,
}.Build(),
}.Build(),
}.Build()
if _, err := credsSvc.Create(ctx, resource); err != nil {
if trace.IsAlreadyExists(err) {
if _, err := credsSvc.Put(ctx, resource); err != nil {
a.logger.WarnContext(ctx, "Failed to update session credentials", "error", err)
}
} else {
a.logger.WarnContext(ctx, "Failed to register session credentials", "error", err)
}
}
}
var clientOptions authclient.ClientOptions
if o, err := a.ClientOptionsForLogin(user); err == nil {
clientOptions = o
} else {
a.logger.WarnContext(ctx, "Failed to calculate client options for local login", "username", user.GetName(), "error", err)
}
return &authclient.CLILoginResponse{
Username: user.GetName(),
Cert: certs.SSH,
TLSCert: certs.TLS,
HostSigners: authclient.AuthoritiesToTrustedCerts(hostCertAuthorities),
ClientOptions: clientOptions,
}, nil
}
// ClientOptionsForLogin returns the client options for a user that has just
// logged in.
func (a *Server) ClientOptionsForLogin(userState services.UserState) (authclient.ClientOptions, error) {
var opts authclient.ClientOptions
if t := userState.GetTraits()[constants.TraitDefaultRelayAddr]; len(t) > 0 {
opts.DefaultRelayAddr = t[0]
}
return opts, nil
}
// emitNoLocalAuthEvent creates and emits a local authentication is disabled message.
func (a *Server) emitNoLocalAuthEvent(username string) {
if err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.AuthAttempt{
Metadata: apievents.Metadata{
Type: events.AuthAttemptEvent,
Code: events.AuthAttemptFailureCode,
},
UserMetadata: apievents.UserMetadata{
User: username,
},
Status: apievents.Status{
Success: false,
Error: noLocalAuth,
},
}); err != nil {
a.logger.WarnContext(a.closeCtx, "Failed to emit no local auth event", "error", err)
}
}
func (a *Server) createUserWebSession(ctx context.Context, user services.UserState, loginIP string) (types.WebSession, error) {
// It's safe to extract the roles and traits directly from services.User as this method
// is only used for local accounts.
return a.CreateWebSessionFromReq(ctx, NewWebSessionRequest{
User: user.GetName(),
LoginIP: loginIP,
Roles: user.GetRoles(),
Traits: user.GetTraits(),
LoginTime: a.clock.Now().UTC(),
})
}
func getErrorByTraceField(err error) error {
var traceErr trace.Error
ok := errors.As(err, &traceErr)
switch {
case !ok:
logger.WarnContext(context.Background(), "Unexpected error type, wanted TraceError", "error", err)
return trace.AccessDenied("an error has occurred")
case traceErr.GetFields()[ErrFieldKeyUserMaxedAttempts] != nil:
return trace.AccessDenied("%s", MaxFailedAttemptsErrMsg)
}
return nil
}
func trimUserAgent(userAgent string) string {
if len(userAgent) > maxUserAgentLen {
return userAgent[:maxUserAgentLen-3] + "..."
}
return userAgent
}
// splitClientUserAgent strictly splits the user agent into a type and a semantic version.
// Any other formatting is not allowed and is treated as a third-party client (to be ignored).
func splitClientUserAgent(userAgent string) (string, string) {
agent := strings.SplitN(userAgent, "/", 2)
if len(agent) != 2 {
return "", ""
}
ver, err := semver.NewVersion(agent[1])
if err != nil {
return "", ""
}
return agent[0], ver.String()
}
const noLocalAuth = "local auth disabled"
func userOrigin(user types.User) apievents.UserOrigin {
userOrigin := apievents.UserOriginFromOriginLabel(user.Origin())
if userOrigin == apievents.UserOrigin_USER_ORIGIN_UNSPECIFIED {
userOrigin = apievents.UserOriginFromUserType(user.GetUserType())
}
return userOrigin
}