mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
* * Add proper context handling to auth.AuthenticateUser. * Move PublicKey field to AuthenticateUserRequest where it can be used for actual authentication. * Use a simple switch statement in /webapi/ssh/certs logic to switch between password, otp, and eventually headless login. * Add Headless flow to /webapi/ssh/certs login enpdoint. * Add 3 minute callback timeout.
580 lines
20 KiB
Go
580 lines
20 KiB
Go
/*
|
|
Copyright 2017 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"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"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/api/utils/keys"
|
|
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
|
|
"github.com/gravitational/teleport/lib/events"
|
|
"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
|
|
)
|
|
|
|
// AuthenticateUserRequest is a request to authenticate interactive user
|
|
type AuthenticateUserRequest struct {
|
|
// Username is a username
|
|
Username string `json:"username"`
|
|
// PublicKey is a public key in ssh authorized_keys format
|
|
PublicKey []byte `json:"public_key"`
|
|
// Pass is a password used in local authentication schemes
|
|
Pass *PassCreds `json:"pass,omitempty"`
|
|
// Webauthn is a signed credential assertion, used in MFA authentication
|
|
Webauthn *wanlib.CredentialAssertionResponse `json:"webauthn,omitempty"`
|
|
// OTP is a password and second factor, used for MFA authentication
|
|
OTP *OTPCreds `json:"otp,omitempty"`
|
|
// Session is a web session credential used to authenticate web sessions
|
|
Session *SessionCreds `json:"session,omitempty"`
|
|
// ClientMetadata includes forwarded information about a client
|
|
ClientMetadata *ForwardedClientMetadata `json:"client_metadata,omitempty"`
|
|
// HeadlessAuthenticationID is the ID for a headless authentication resource.
|
|
HeadlessAuthenticationID string `json:"headless_authentication_id"`
|
|
}
|
|
|
|
// ForwardedClientMetadata can be used by the proxy web API to forward information about
|
|
// the client to the auth service for logging purposes.
|
|
type ForwardedClientMetadata struct {
|
|
UserAgent string `json:"user_agent,omitempty"`
|
|
RemoteAddr string `json:"remote_addr,omitempty"`
|
|
}
|
|
|
|
// CheckAndSetDefaults checks and sets defaults
|
|
func (a *AuthenticateUserRequest) CheckAndSetDefaults() error {
|
|
switch {
|
|
case a.Username == "" && a.Webauthn != nil: // OK, passwordless.
|
|
case a.Username == "":
|
|
return trace.BadParameter("missing parameter 'username'")
|
|
case a.Pass == nil && a.Webauthn == nil && a.OTP == nil && a.Session == nil:
|
|
return trace.BadParameter("at least one authentication method is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PassCreds is a password credential
|
|
type PassCreds struct {
|
|
// Password is a user password
|
|
Password []byte `json:"password"`
|
|
}
|
|
|
|
// OTPCreds is a two-factor authentication credentials
|
|
type OTPCreds struct {
|
|
// Password is a user password
|
|
Password []byte `json:"password"`
|
|
// Token is a user second factor token
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// SessionCreds is a web session credentials
|
|
type SessionCreds struct {
|
|
// ID is a web session id
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// AuthenticateUser authenticates user based on the request type.
|
|
// Returns the username of the authenticated user.
|
|
func (s *Server) AuthenticateUser(ctx context.Context, req AuthenticateUserRequest) (string, error) {
|
|
user := req.Username
|
|
|
|
mfaDev, actualUser, err := s.authenticateUser(ctx, req)
|
|
// err is handled below.
|
|
switch {
|
|
case user != "" && actualUser != "" && user != actualUser:
|
|
log.Warnf("Authenticate user mismatch (%q vs %q). Using request user (%q)", user, actualUser, user)
|
|
case user == "" && actualUser != "":
|
|
log.Debugf("User %q authenticated via passwordless", actualUser)
|
|
user = actualUser
|
|
}
|
|
|
|
event := &apievents.UserLogin{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.UserLoginEvent,
|
|
Code: events.UserLocalLoginFailureCode,
|
|
},
|
|
UserMetadata: apievents.UserMetadata{
|
|
User: user,
|
|
},
|
|
Method: events.LoginMethodLocal,
|
|
}
|
|
if mfaDev != nil {
|
|
m := mfaDeviceEventMetadata(mfaDev)
|
|
event.MFADevice = &m
|
|
}
|
|
if req.ClientMetadata != nil {
|
|
event.RemoteAddr = req.ClientMetadata.RemoteAddr
|
|
if len(req.ClientMetadata.UserAgent) > maxUserAgentLen {
|
|
event.UserAgent = req.ClientMetadata.UserAgent[:maxUserAgentLen-3] + "..."
|
|
} else {
|
|
event.UserAgent = req.ClientMetadata.UserAgent
|
|
}
|
|
}
|
|
if err != nil {
|
|
event.Code = events.UserLocalLoginFailureCode
|
|
event.Status.Success = false
|
|
event.Status.Error = err.Error()
|
|
} else {
|
|
event.Code = events.UserLocalLoginCode
|
|
event.Status.Success = true
|
|
}
|
|
if err := s.emitter.EmitAuditEvent(s.closeCtx, event); err != nil {
|
|
log.WithError(err).Warn("Failed to emit login event.")
|
|
}
|
|
return user, trace.Wrap(err)
|
|
}
|
|
|
|
var (
|
|
// authenticateWebauthnError is the generic error returned for failed WebAuthn
|
|
// authentication attempts.
|
|
authenticateWebauthnError = trace.AccessDenied("invalid Webauthn response")
|
|
// invalidUserPassError is the error for when either the provided username or
|
|
// password is incorrect.
|
|
invalidUserPassError = trace.AccessDenied("invalid username or password")
|
|
// invalidUserpass2FError is the error for when either the provided username,
|
|
// password, or second factor is incorrect.
|
|
invalidUserPass2FError = trace.AccessDenied("invalid username, password or second factor")
|
|
)
|
|
|
|
// IsInvalidLocalCredentialError checks if an error resulted from an incorrect username,
|
|
// password, or second factor.
|
|
func IsInvalidLocalCredentialError(err error) bool {
|
|
return errors.Is(err, invalidUserPassError) || errors.Is(err, invalidUserPass2FError)
|
|
}
|
|
|
|
// authenticateUser authenticates a user through various methods (password, MFA,
|
|
// passwordless)
|
|
// Returns the device used to authenticate (if applicable) and the username.
|
|
func (s *Server) authenticateUser(ctx context.Context, req AuthenticateUserRequest) (*types.MFADevice, string, error) {
|
|
if err := req.CheckAndSetDefaults(); err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
user := req.Username
|
|
passwordless := user == ""
|
|
|
|
// Only one path if passwordless, other variants shouldn't see an empty user.
|
|
if passwordless {
|
|
return s.authenticatePasswordless(ctx, req)
|
|
}
|
|
|
|
// 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) {
|
|
mfaResponse := &proto.MFAAuthenticateResponse{
|
|
Response: &proto.MFAAuthenticateResponse_Webauthn{
|
|
Webauthn: wanlib.CredentialAssertionResponseToProto(req.Webauthn),
|
|
},
|
|
}
|
|
dev, _, err := s.validateMFAAuthResponse(ctx, mfaResponse, user, passwordless)
|
|
return dev, trace.Wrap(err)
|
|
}
|
|
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 := s.checkPassword(user, req.OTP.Password, req.OTP.Token)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return res.mfaDev, nil
|
|
}
|
|
authErr = invalidUserPass2FError
|
|
}
|
|
if authenticateFn != nil {
|
|
var dev *types.MFADevice
|
|
err := s.WithUserLock(user, func() error {
|
|
var err error
|
|
dev, err = authenticateFn()
|
|
return err
|
|
})
|
|
switch {
|
|
case err != nil:
|
|
log.Debugf("User %v failed to authenticate: %v.", user, err)
|
|
if fieldErr := getErrorByTraceField(err); fieldErr != nil {
|
|
return nil, "", trace.Wrap(fieldErr)
|
|
}
|
|
|
|
return nil, "", trace.Wrap(authErr)
|
|
case dev == nil:
|
|
log.Debugf(
|
|
"MFA authentication returned nil device (Webauthn = %v, TOTP = %v): %v.",
|
|
req.Webauthn != nil, req.OTP != nil, err)
|
|
return nil, "", trace.Wrap(authErr)
|
|
default:
|
|
return dev, user, nil
|
|
}
|
|
}
|
|
|
|
// Try password-only authentication last.
|
|
if req.Pass == nil {
|
|
return nil, "", trace.AccessDenied("unsupported authentication method")
|
|
}
|
|
|
|
authPreference, err := s.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 authPreference.GetSecondFactor() {
|
|
case constants.SecondFactorOff:
|
|
// No 2FA required, check password only.
|
|
case constants.SecondFactorOptional:
|
|
// 2FA is optional. Make sure that a user does not have MFA devices
|
|
// registered.
|
|
devs, err := s.Services.GetMFADevices(ctx, user, false /* withSecrets */)
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
if len(devs) != 0 {
|
|
log.Warningf("MFA bypass attempt by user %q, access denied.", user)
|
|
return nil, "", trace.AccessDenied("missing second factor authentication")
|
|
}
|
|
default:
|
|
// 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.
|
|
log.Warningf("MFA bypass attempt by user %q, access denied.", user)
|
|
return nil, "", trace.AccessDenied("missing second factor")
|
|
}
|
|
if err = s.WithUserLock(user, func() error {
|
|
return s.checkPasswordWOToken(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
|
|
log.Debugf("User %v failed to authenticate: %v.", user, err)
|
|
return nil, "", trace.Wrap(invalidUserPassError)
|
|
}
|
|
return nil, user, nil
|
|
}
|
|
|
|
func (s *Server) authenticatePasswordless(ctx context.Context, req AuthenticateUserRequest) (*types.MFADevice, string, error) {
|
|
mfaResponse := &proto.MFAAuthenticateResponse{
|
|
Response: &proto.MFAAuthenticateResponse_Webauthn{
|
|
Webauthn: wanlib.CredentialAssertionResponseToProto(req.Webauthn),
|
|
},
|
|
}
|
|
dev, user, err := s.validateMFAAuthResponse(ctx, mfaResponse, "", true /* passwordless */)
|
|
if err != nil {
|
|
log.Debugf("Passwordless authentication failed: %v", 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 := s.WithUserLock(user, func() error { return nil }); err != nil {
|
|
log.Debugf("WithUserLock for user %q failed during passwordless authentication: %v", user, err)
|
|
return nil, user, trace.Wrap(authenticateWebauthnError)
|
|
}
|
|
|
|
return dev, user, 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 (s *Server) AuthenticateWebUser(ctx context.Context, req AuthenticateUserRequest) (types.WebSession, error) {
|
|
username := req.Username // Empty if passwordless.
|
|
|
|
authPref, err := s.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 {
|
|
s.emitNoLocalAuthEvent(username)
|
|
return nil, trace.AccessDenied(noLocalAuth)
|
|
}
|
|
|
|
if req.Session != nil {
|
|
session, err := s.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
|
|
}
|
|
|
|
actualUser, err := s.AuthenticateUser(ctx, req)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
username = actualUser
|
|
|
|
user, err := s.GetUser(username, false /* withSecrets */)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
sess, err := s.createUserWebSession(ctx, user)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return sess, nil
|
|
}
|
|
|
|
// AuthenticateSSHRequest is a request to authenticate SSH client user via CLI
|
|
type AuthenticateSSHRequest struct {
|
|
// AuthenticateUserRequest is a request with credentials
|
|
AuthenticateUserRequest
|
|
// TTL is a requested TTL for certificates to be issues
|
|
TTL time.Duration `json:"ttl"`
|
|
// CompatibilityMode sets certificate compatibility mode with old SSH clients
|
|
CompatibilityMode string `json:"compatibility_mode"`
|
|
RouteToCluster string `json:"route_to_cluster"`
|
|
// KubernetesCluster sets the target kubernetes cluster for the TLS
|
|
// certificate. This can be empty on older clients.
|
|
KubernetesCluster string `json:"kubernetes_cluster"`
|
|
// AttestationStatement is an attestation statement associated with the given public key.
|
|
AttestationStatement *keys.AttestationStatement `json:"attestation_statement,omitempty"`
|
|
}
|
|
|
|
// CheckAndSetDefaults checks and sets default certificate values
|
|
func (a *AuthenticateSSHRequest) CheckAndSetDefaults() error {
|
|
if err := a.AuthenticateUserRequest.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if len(a.PublicKey) == 0 {
|
|
return trace.BadParameter("missing parameter 'public_key'")
|
|
}
|
|
certificateFormat, err := utils.CheckCertificateFormatFlag(a.CompatibilityMode)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
a.CompatibilityMode = certificateFormat
|
|
return nil
|
|
}
|
|
|
|
// SSHLoginResponse is a response returned by web proxy, it preserves backwards compatibility
|
|
// on the wire, which is the primary reason for non-matching json tags
|
|
type SSHLoginResponse struct {
|
|
// User contains a logged-in user information
|
|
Username string `json:"username"`
|
|
// Cert is a PEM encoded signed certificate
|
|
Cert []byte `json:"cert"`
|
|
// TLSCertPEM is a PEM encoded TLS certificate signed by TLS certificate authority
|
|
TLSCert []byte `json:"tls_cert"`
|
|
// HostSigners is a list of signing host public keys trusted by proxy
|
|
HostSigners []TrustedCerts `json:"host_signers"`
|
|
}
|
|
|
|
// TrustedCerts contains host certificates, it preserves backwards compatibility
|
|
// on the wire, which is the primary reason for non-matching json tags
|
|
type TrustedCerts struct {
|
|
// ClusterName identifies teleport cluster name this authority serves,
|
|
// for host authorities that means base hostname of all servers,
|
|
// for user authorities that means organization name
|
|
ClusterName string `json:"domain_name"`
|
|
// AuthorizedKeys is a list of SSH public keys in authorized_keys format
|
|
// that can be used to check host key signatures.
|
|
AuthorizedKeys [][]byte `json:"checking_keys"`
|
|
// TLSCertificates is a list of TLS certificates of the certificate authority
|
|
// of the authentication server
|
|
TLSCertificates [][]byte `json:"tls_certs"`
|
|
}
|
|
|
|
// SSHCertPublicKeys returns a list of trusted host SSH certificate authority public keys
|
|
func (c *TrustedCerts) SSHCertPublicKeys() ([]ssh.PublicKey, error) {
|
|
out := make([]ssh.PublicKey, 0, len(c.AuthorizedKeys))
|
|
for _, keyBytes := range c.AuthorizedKeys {
|
|
publicKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
out = append(out, publicKey)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// AuthoritiesToTrustedCerts serializes authorities to TrustedCerts data structure
|
|
func AuthoritiesToTrustedCerts(authorities []types.CertAuthority) []TrustedCerts {
|
|
out := make([]TrustedCerts, len(authorities))
|
|
for i, ca := range authorities {
|
|
out[i] = TrustedCerts{
|
|
ClusterName: ca.GetClusterName(),
|
|
AuthorizedKeys: services.GetSSHCheckingKeys(ca),
|
|
TLSCertificates: services.GetTLSCerts(ca),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// AuthenticateSSHUser authenticates an SSH user and returns SSH and TLS
|
|
// certificates for the public key in req.
|
|
func (s *Server) AuthenticateSSHUser(ctx context.Context, req AuthenticateSSHRequest) (*SSHLoginResponse, error) {
|
|
username := req.Username // Empty if passwordless.
|
|
|
|
authPref, err := s.GetAuthPreference(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if !authPref.GetAllowLocalAuth() {
|
|
s.emitNoLocalAuthEvent(username)
|
|
return nil, trace.AccessDenied(noLocalAuth)
|
|
}
|
|
|
|
clusterName, err := s.GetClusterName()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
actualUser, err := s.AuthenticateUser(ctx, req.AuthenticateUserRequest)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
username = actualUser
|
|
|
|
// It's safe to extract the roles and traits directly from services.User as
|
|
// this endpoint is only used for local accounts.
|
|
user, err := s.GetUser(username, false /* withSecrets */)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
accessInfo := services.AccessInfoFromUser(user)
|
|
checker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), s)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Return the host CA for this cluster only.
|
|
authority, err := s.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.PinSourceIP() && clientIP == "" {
|
|
return nil, trace.BadParameter("source IP pinning is enabled but client IP is unknown")
|
|
}
|
|
|
|
certs, err := s.generateUserCert(certRequest{
|
|
user: user,
|
|
ttl: req.TTL,
|
|
publicKey: req.PublicKey,
|
|
compatibility: req.CompatibilityMode,
|
|
checker: checker,
|
|
traits: user.GetTraits(),
|
|
routeToCluster: req.RouteToCluster,
|
|
kubernetesCluster: req.KubernetesCluster,
|
|
loginIP: clientIP,
|
|
attestationStatement: req.AttestationStatement,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
UserLoginCount.Inc()
|
|
return &SSHLoginResponse{
|
|
Username: username,
|
|
Cert: certs.SSH,
|
|
TLSCert: certs.TLS,
|
|
HostSigners: AuthoritiesToTrustedCerts(hostCertAuthorities),
|
|
}, nil
|
|
}
|
|
|
|
// emitNoLocalAuthEvent creates and emits a local authentication is disabled message.
|
|
func (s *Server) emitNoLocalAuthEvent(username string) {
|
|
if err := s.emitter.EmitAuditEvent(s.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 {
|
|
log.WithError(err).Warn("Failed to emit no local auth event.")
|
|
}
|
|
}
|
|
|
|
func (s *Server) createUserWebSession(ctx context.Context, user types.User) (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 s.CreateWebSessionFromReq(ctx, types.NewWebSessionRequest{
|
|
User: user.GetName(),
|
|
Roles: user.GetRoles(),
|
|
Traits: user.GetTraits(),
|
|
LoginTime: s.clock.Now().UTC(),
|
|
})
|
|
}
|
|
|
|
func getErrorByTraceField(err error) error {
|
|
traceErr, ok := err.(trace.Error)
|
|
switch {
|
|
case !ok:
|
|
log.WithError(err).Warn("Unexpected error type, wanted TraceError")
|
|
return trace.AccessDenied("an error has occurred")
|
|
case traceErr.GetFields()[ErrFieldKeyUserMaxedAttempts] != nil:
|
|
return trace.AccessDenied(MaxFailedAttemptsErrMsg)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const noLocalAuth = "local auth disabled"
|