Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SSO MFA - SSO MFA challenge creation and verification #47684

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,904 changes: 1,789 additions & 1,115 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,10 @@ message MFAAuthenticateChallenge {
// communications, in case of streaming RPCs. It may also return empty
// challenges for all other fields.
MFARequired MFARequired = 4;
// SSO Challenge is an SSO MFA challenge. If set, the client can go to the
// IdP redirect URL to perform an MFA check in the IdP and obtain an MFA token.
// This token paired with the request id can then be used as MFA verification.
SSOChallenge SSOChallenge = 5;
}

// MFAAuthenticateResponse is a response to MFAAuthenticateChallenge using one
Expand All @@ -1224,6 +1228,7 @@ message MFAAuthenticateResponse {
// Removed: U2FResponse U2F = 1;
TOTPResponse TOTP = 2;
webauthn.CredentialAssertionResponse Webauthn = 3;
SSOResponse SSO = 4;
}
}

Expand All @@ -1240,6 +1245,22 @@ message TOTPResponse {
string Code = 1;
}

// SSOChallenge contains SSO auth request details to perform an SSO MFA check.
message SSOChallenge {
// RequestId is the ID of an SSO auth request.
string request_id = 1;
// RedirectUrl is an IdP redirect URL to initate the SSO MFA flow.
string redirect_url = 2;
}

// SSOResponse is a response to SSOChallenge.
message SSOResponse {
// RequestId is the ID of an SSO auth request.
string request_id = 1;
// Token is a secret token used to verify the user's SSO MFA session.
string token = 2;
}

// MFARegisterChallenge is a challenge for registering a new MFA device.
message MFARegisterChallenge {
// Request depends on the type of the MFA device being registered.
Expand Down Expand Up @@ -1864,9 +1885,11 @@ message CreateAuthenticateChallengeRequest {
// call [AuthService.IsMFARequired] in the leaf instead of setting this field.
IsMFARequiredRequest MFARequiredCheck = 5 [(gogoproto.jsontag) = "mfa_required_check,omitempty"];
// ChallengeExtensions are extensions that will be apply to the issued MFA challenge.
// ChallengeExtensions only apply to webauthn challenges currently. Required, except
// for v15 clients and older.
// Required, except for v15 clients and older.
teleport.mfa.v1.ChallengeExtensions ChallengeExtensions = 6 [(gogoproto.jsontag) = "challenge_extensions,omitempty"];
// SSOClientRedirectURL should be supplied If the client supports SSO MFA checks.
// If unset, the server will only return non-SSO challenges.
string SSOClientRedirectURL = 7 [(gogoproto.jsontag) = "sso_client_redirect_url,omitempty"];
}

// CreatePrivilegeTokenRequest defines a request to obtain a privilege token.
Expand Down
2 changes: 1 addition & 1 deletion e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this one? It seems to be trying to update to a non-master commit.

Submodule e updated from e4f948 to dd5bb9
46 changes: 30 additions & 16 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3673,7 +3673,7 @@ func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.Cre
}
}

challenges, err := a.mfaAuthChallenge(ctx, username, challengeExtensions)
challenges, err := a.mfaAuthChallenge(ctx, username, req.SSOClientRedirectURL, challengeExtensions)
if err != nil {
// Do not obfuscate config-related errors.
if errors.Is(err, types.ErrPasswordlessRequiresWebauthn) || errors.Is(err, types.ErrPasswordlessDisabledBySettings) {
Expand Down Expand Up @@ -6754,7 +6754,7 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck

// mfaAuthChallenge constructs an MFAAuthenticateChallenge for all MFA devices
// registered by the user.
func (a *Server) mfaAuthChallenge(ctx context.Context, user string, challengeExtensions *mfav1.ChallengeExtensions) (*proto.MFAAuthenticateChallenge, error) {
func (a *Server) mfaAuthChallenge(ctx context.Context, user string, ssoClientRedirectURL string, challengeExtensions *mfav1.ChallengeExtensions) (*proto.MFAAuthenticateChallenge, error) {
isPasswordless := challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN

// Check what kind of MFA is enabled.
Expand All @@ -6764,6 +6764,7 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, challengeExt
}
enableTOTP := apref.IsSecondFactorTOTPAllowed()
enableWebauthn := apref.IsSecondFactorWebauthnAllowed()
enableSSO := apref.IsSecondFactorSSOAllowed()

// Fetch configurations. The IsSecondFactor*Allowed calls above already
// include the necessary checks of config empty, disabled, etc.
Expand Down Expand Up @@ -6834,7 +6835,7 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, challengeExt
if err != nil {
return nil, trace.Wrap(err)
}
groupedDevs := groupByDeviceType(devs, enableWebauthn)
groupedDevs := groupByDeviceType(devs)
challenge := &proto.MFAAuthenticateChallenge{}

// TOTP challenge.
Expand All @@ -6843,7 +6844,7 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, challengeExt
}

// WebAuthn challenge.
if len(groupedDevs.Webauthn) > 0 {
if enableWebauthn && len(groupedDevs.Webauthn) > 0 {
webLogin := &wanlib.LoginFlow{
U2F: u2fPref,
Webauthn: webConfig,
Expand All @@ -6856,6 +6857,14 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, challengeExt
challenge.WebauthnChallenge = wantypes.CredentialAssertionToProto(assertion)
}

// If the user has an SSO device and the client provided a redirect URL to handle
// the MFA SSO flow, create an SSO challenge.
if enableSSO && groupedDevs.SSO != nil && ssoClientRedirectURL != "" {
if challenge.SSOChallenge, err = a.beginSSOMFAChallenge(ctx, user, groupedDevs.SSO.GetSso(), ssoClientRedirectURL, challengeExtensions); err != nil {
return nil, trace.Wrap(err)
}
}

clusterName, err := a.GetClusterName()
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -6883,20 +6892,16 @@ type devicesByType struct {
SSO *types.MFADevice
}

func groupByDeviceType(devs []*types.MFADevice, groupWebauthn bool) devicesByType {
func groupByDeviceType(devs []*types.MFADevice) devicesByType {
res := devicesByType{}
for _, dev := range devs {
switch dev.Device.(type) {
case *types.MFADevice_Totp:
res.TOTP = true
case *types.MFADevice_U2F:
if groupWebauthn {
res.Webauthn = append(res.Webauthn, dev)
}
res.Webauthn = append(res.Webauthn, dev)
case *types.MFADevice_Webauthn:
if groupWebauthn {
res.Webauthn = append(res.Webauthn, dev)
}
res.Webauthn = append(res.Webauthn, dev)
case *types.MFADevice_Sso:
res.SSO = dev
default:
Expand All @@ -6914,7 +6919,7 @@ func groupByDeviceType(devs []*types.MFADevice, groupWebauthn bool) devicesByTyp
// Use only for registration purposes.
func (a *Server) validateMFAAuthResponseForRegister(ctx context.Context, resp *proto.MFAAuthenticateResponse, username string, requiredExtensions *mfav1.ChallengeExtensions) (hasDevices bool, err error) {
// Let users without a useable device go through registration.
if resp == nil || (resp.GetTOTP() == nil && resp.GetWebauthn() == nil) {
if resp == nil || (resp.GetTOTP() == nil && resp.GetWebauthn() == nil && resp.GetSSO() == nil) {
devices, err := a.Services.GetMFADevices(ctx, username, false /* withSecrets */)
if err != nil {
return false, trace.Wrap(err)
Expand All @@ -6923,16 +6928,18 @@ func (a *Server) validateMFAAuthResponseForRegister(ctx context.Context, resp *p
// Allowed, no devices registered.
return false, nil
}
devsByType := groupByDeviceType(devices)

authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return false, trace.Wrap(err)
}
totpEnabled := authPref.IsSecondFactorTOTPAllowed()
webauthnEnabled := authPref.IsSecondFactorWebauthnAllowed()

devsByType := groupByDeviceType(devices, webauthnEnabled)
if (totpEnabled && devsByType.TOTP) || (webauthnEnabled && len(devsByType.Webauthn) > 0) {
hasTOTP := authPref.IsSecondFactorTOTPAllowed() && devsByType.TOTP
hasWebAuthn := authPref.IsSecondFactorWebauthnAllowed() && len(devsByType.Webauthn) > 0
hasSSO := authPref.IsSecondFactorSSOAllowed() && devsByType.SSO != nil

if hasTOTP || hasWebAuthn || hasSSO {
return false, trace.BadParameter("second factor authentication required")
}

Expand Down Expand Up @@ -6960,6 +6967,10 @@ func (a *Server) ValidateMFAAuthResponse(
user string,
requiredExtensions *mfav1.ChallengeExtensions,
) (*authz.MFAAuthData, error) {
if requiredExtensions == nil {
return nil, trace.BadParameter("required challenge extensions parameter required")
}

authData, validateErr := a.validateMFAAuthResponseInternal(ctx, resp, user, requiredExtensions)
// validateErr handled after audit.

Expand Down Expand Up @@ -7094,6 +7105,9 @@ func (a *Server) validateMFAAuthResponseInternal(
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
}, nil

case *proto.MFAAuthenticateResponse_SSO:
mfaAuthData, err := a.verifySSOMFASession(ctx, user, res.SSO.RequestId, res.SSO.Token, requiredExtensions)
return mfaAuthData, trace.Wrap(err)
default:
return nil, trace.BadParameter("unknown or missing MFAAuthenticateResponse type %T", resp.Response)
}
Expand Down
5 changes: 4 additions & 1 deletion lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2165,8 +2165,11 @@ func (g *GRPCServer) DeleteRole(ctx context.Context, req *authpb.DeleteRoleReque
func doMFAPresenceChallenge(ctx context.Context, actx *grpcContext, stream authpb.AuthService_MaintainSessionPresenceServer, challengeReq *authpb.PresenceMFAChallengeRequest) error {
user := actx.User.GetName()

// TODO(Joerger): Extend SSO MFA support for moderated sessions.
var ssoClientRedirectURL string
Comment on lines +2168 to +2169
Copy link
Contributor Author

@Joerger Joerger Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be implemented in a follow up PR when I can get to it, but it's not a priority over per-session MFA and other MFA features.


chalExt := &mfav1pb.ChallengeExtensions{Scope: mfav1pb.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION}
authChallenge, err := actx.authServer.mfaAuthChallenge(ctx, user, chalExt)
authChallenge, err := actx.authServer.mfaAuthChallenge(ctx, user, ssoClientRedirectURL, chalExt)
if err != nil {
return trace.Wrap(err)
}
Expand Down
16 changes: 16 additions & 0 deletions lib/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (

type OIDCService interface {
CreateOIDCAuthRequest(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error)
CreateOIDCAuthRequestForMFA(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error)
ValidateOIDCAuthCallback(ctx context.Context, q url.Values) (*authclient.OIDCAuthResponse, error)
}

Expand Down Expand Up @@ -124,6 +125,8 @@ func (a *Server) DeleteOIDCConnector(ctx context.Context, connectorName string)
return nil
}

// CreateOIDCAuthRequest delegates the method call to the oidcAuthService if present,
// or returns a NotImplemented error if not present.
func (a *Server) CreateOIDCAuthRequest(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error) {
if a.oidcAuthService == nil {
return nil, errOIDCNotImplemented
Expand All @@ -133,6 +136,19 @@ func (a *Server) CreateOIDCAuthRequest(ctx context.Context, req types.OIDCAuthRe
return rq, trace.Wrap(err)
}

// CreateOIDCAuthRequestForMFA delegates the method call to the oidcAuthService if present,
// or returns a NotImplemented error if not present.
func (a *Server) CreateOIDCAuthRequestForMFA(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error) {
if a.oidcAuthService == nil {
return nil, errOIDCNotImplemented
}

rq, err := a.oidcAuthService.CreateOIDCAuthRequestForMFA(ctx, req)
return rq, trace.Wrap(err)
}

// ValidateOIDCAuthCallback delegates the method call to the oidcAuthService if present,
// or returns a NotImplemented error if not present.
func (a *Server) ValidateOIDCAuthCallback(ctx context.Context, q url.Values) (*authclient.OIDCAuthResponse, error) {
if a.oidcAuthService == nil {
return nil, errOIDCNotImplemented
Expand Down
14 changes: 12 additions & 2 deletions lib/auth/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ var ErrSAMLRequiresEnterprise = &trace.AccessDeniedError{Message: "SAML is only
// authentication - the connector CRUD operations and Get methods are
// implemented in auth.Server and provide no connector-specific logic.
type SAMLService interface {
// CreateSAMLAuthRequest creates SAML AuthnRequest
CreateSAMLAuthRequest(ctx context.Context, req types.SAMLAuthRequest) (*types.SAMLAuthRequest, error)
// ValidateSAMLResponse validates SAML auth response
CreateSAMLAuthRequestForMFA(ctx context.Context, req types.SAMLAuthRequest) (*types.SAMLAuthRequest, error)
ValidateSAMLResponse(ctx context.Context, samlResponse, connectorID, clientIP string) (*authclient.SAMLAuthResponse, error)
}

Expand Down Expand Up @@ -214,6 +213,17 @@ func (a *Server) CreateSAMLAuthRequest(ctx context.Context, req types.SAMLAuthRe
return rq, trace.Wrap(err)
}

// CreateSAMLAuthRequestForMFA delegates the method call to the samlAuthService if present,
// or returns a NotImplemented error if not present.
func (a *Server) CreateSAMLAuthRequestForMFA(ctx context.Context, req types.SAMLAuthRequest) (*types.SAMLAuthRequest, error) {
if a.samlAuthService == nil {
return nil, trace.Wrap(ErrSAMLRequiresEnterprise)
}

rq, err := a.samlAuthService.CreateSAMLAuthRequestForMFA(ctx, req)
return rq, trace.Wrap(err)
}

// ValidateSAMLResponse delegates the method call to the samlAuthService if present,
// or returns a NotImplemented error if not present.
func (a *Server) ValidateSAMLResponse(ctx context.Context, samlResponse, connectorID, clientIP string) (*authclient.SAMLAuthResponse, error) {
Expand Down
Loading
Loading