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

* Update e ref.

* Update proto.

* Handle SSO MFA challenge creation.

* Handle SSO MFA challenge response validation.

* Only create SSO MFA challenge if SSO MFA is an enabled second factor; Require SSO MFA for device registration when available.

* Update verification logic; Add test.

* Fix lint.
This commit is contained in:
Brian Joerger
2024-10-24 20:12:59 +00:00
committed by GitHub
parent 123cfc96ea
commit 4cd3f3bb9c
9 changed files with 2731 additions and 1133 deletions
File diff suppressed because it is too large Load Diff
@@ -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
@@ -1224,6 +1228,7 @@ message MFAAuthenticateResponse {
// Removed: U2FResponse U2F = 1;
TOTPResponse TOTP = 2;
webauthn.CredentialAssertionResponse Webauthn = 3;
SSOResponse SSO = 4;
}
}
@@ -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.
@@ -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.
+1 -1
Submodule e updated: b5db15651a...88c5aecb07
+30 -16
View File
@@ -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) {
@@ -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.
@@ -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.
@@ -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.
@@ -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,
@@ -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)
@@ -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:
@@ -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)
@@ -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")
}
@@ -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.
@@ -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)
}
+4 -1
View File
@@ -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
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)
}
+16
View File
@@ -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)
}
@@ -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
@@ -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
+12 -2
View File
@@ -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)
}
@@ -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) {
+108 -2
View File
@@ -18,18 +18,124 @@ package auth
import (
"context"
"crypto/subtle"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
// UpsertSSOMFASession upserts a new unverified SSO MFA session for the given username,
// beginSSOMFAChallenge creates a new SSO MFA auth request and session data for the given user and sso device.
func (a *Server) beginSSOMFAChallenge(ctx context.Context, user string, sso *types.SSOMFADevice, ssoClientRedirectURL string, ext *mfav1.ChallengeExtensions) (*proto.SSOChallenge, error) {
chal := new(proto.SSOChallenge)
switch sso.ConnectorType {
case constants.SAML:
resp, err := a.CreateSAMLAuthRequestForMFA(ctx, types.SAMLAuthRequest{
ConnectorID: sso.ConnectorId,
Type: sso.ConnectorType,
ClientRedirectURL: ssoClientRedirectURL,
CheckUser: true,
})
if err != nil {
return nil, trace.Wrap(err)
}
chal.RequestId = resp.ID
chal.RedirectUrl = resp.RedirectURL
case constants.OIDC:
resp, err := a.CreateOIDCAuthRequestForMFA(ctx, types.OIDCAuthRequest{
ConnectorID: sso.ConnectorId,
Type: sso.ConnectorType,
ClientRedirectURL: ssoClientRedirectURL,
CheckUser: true,
})
if err != nil {
return nil, trace.Wrap(err)
}
chal.RequestId = resp.StateToken
chal.RedirectUrl = resp.RedirectURL
default:
return nil, trace.BadParameter("unsupported sso connector type %v", sso.ConnectorType)
}
if err := a.upsertSSOMFASession(ctx, user, chal.RequestId, sso.ConnectorId, sso.ConnectorType, ext); err != nil {
return nil, trace.Wrap(err)
}
return chal, nil
}
// verifySSOMFASession verifies that the given sso mfa token matches an existing MFA session
// for the user and session ID. It also checks the required extensions, and finishes by deleting
// the MFA session if reuse is not allowed.
func (a *Server) verifySSOMFASession(ctx context.Context, username, sessionID, token string, requiredExtensions *mfav1.ChallengeExtensions) (*authz.MFAAuthData, error) {
if requiredExtensions == nil {
return nil, trace.BadParameter("requested challenge extensions must be supplied.")
}
const notFoundErrMsg = "mfa sso session data not found"
mfaSess, err := a.GetSSOMFASessionData(ctx, sessionID)
if trace.IsNotFound(err) {
return nil, trace.AccessDenied(notFoundErrMsg)
} else if err != nil {
return nil, trace.Wrap(err)
}
// Verify the user's name and sso device matches.
if mfaSess.Username != username {
return nil, trace.AccessDenied(notFoundErrMsg)
}
// Check if the MFA session matches the user's SSO MFA settings.
devs, err := a.Services.GetMFADevices(ctx, username, false /* withSecrets */)
if err != nil {
return nil, trace.Wrap(err)
}
groupedDevs := groupByDeviceType(devs)
if groupedDevs.SSO == nil {
return nil, trace.AccessDenied("invalid sso mfa session data; non-sso user")
} else if groupedDevs.SSO.GetSso().ConnectorId != mfaSess.ConnectorID || groupedDevs.SSO.GetSso().ConnectorType != mfaSess.ConnectorType {
return nil, trace.AccessDenied("invalid sso mfa session data; mismatched sso auth connector")
}
// Verify the token matches.
if subtle.ConstantTimeCompare([]byte(mfaSess.Token), []byte(token)) == 0 {
return nil, trace.AccessDenied("invalid SSO MFA challenge response")
}
// Check if the given scope is satisfied by the challenge scope.
if requiredExtensions.Scope != mfaSess.ChallengeExtensions.Scope {
return nil, trace.AccessDenied("required scope %q is not satisfied by the given sso mfa session with scope %q", requiredExtensions.Scope, mfaSess.ChallengeExtensions.Scope)
}
// If this session is reusable, but this context forbids reusable sessions, return an error.
if requiredExtensions.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO && mfaSess.ChallengeExtensions.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES {
return nil, trace.AccessDenied("the given sso mfa session allows reuse, but reuse is not permitted in this context")
}
if mfaSess.ChallengeExtensions.AllowReuse != mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES {
if err := a.DeleteSSOMFASessionData(ctx, sessionID); err != nil {
return nil, trace.Wrap(err)
}
}
return &authz.MFAAuthData{
Device: groupedDevs.SSO,
User: username,
AllowReuse: mfaSess.ChallengeExtensions.AllowReuse,
}, nil
}
// upsertSSOMFASession upserts a new unverified SSO MFA session for the given username,
// sessionID, connector details, and challenge extensions.
func (a *Server) UpsertSSOMFASession(ctx context.Context, user string, sessionID string, connectorID string, connectorType string, ext *mfav1.ChallengeExtensions) error {
func (a *Server) upsertSSOMFASession(ctx context.Context, user string, sessionID string, connectorID string, connectorType string, ext *mfav1.ChallengeExtensions) error {
err := a.UpsertSSOMFASessionData(ctx, &services.SSOMFASessionData{
Username: user,
RequestID: sessionID,
+752
View File
@@ -0,0 +1,752 @@
/*
* Teleport
* Copyright (C) 2024 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 (
"context"
"net/url"
"testing"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
)
func TestSSOMFAChallenge_Creation(t *testing.T) {
t.Parallel()
ctx := context.Background()
fakeClock := clockwork.NewFakeClock()
testAuthServer, err := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
Clock: fakeClock,
})
require.NoError(t, err)
testServer, err := testAuthServer.NewTestTLSServer()
require.NoError(t, err)
a := testServer.Auth()
a.SetSAMLService(&fakeSSOService{a})
a.SetOIDCService(&fakeSSOService{a})
// Enable SSO MFA support.
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactors: []types.SecondFactorType{
types.SecondFactorType_SECOND_FACTOR_TYPE_SSO,
},
AllowLocalAuth: types.NewBoolOption(false),
})
require.NoError(t, err)
_, err = a.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
// Create a standard user.
standardUser, _, err := CreateUserAndRole(a, "standard", []string{"role"}, nil)
require.NoError(t, err)
// Create a fake saml user with MFA disabled.
noMFASAMLUser, noMFASAMLRole, err := CreateUserAndRole(a, "saml-user-no-mfa", []string{"role"}, nil)
require.NoError(t, err)
noMFASAMLConnector, err := types.NewSAMLConnector("saml-no-mfa", types.SAMLConnectorSpecV2{
AssertionConsumerService: "http://localhost:65535/acs", // not called
Issuer: "test",
SSO: "https://localhost:65535/sso", // not called
AttributesToRoles: []types.AttributeMapping{
// not used. can be any name, value but role must exist
{Name: "groups", Value: "admin", Roles: []string{noMFASAMLRole.GetName()}},
},
})
require.NoError(t, err)
_, err = a.UpsertSAMLConnector(ctx, noMFASAMLConnector)
require.NoError(t, err)
noMFASAMLUser.SetCreatedBy(types.CreatedBy{
Time: a.clock.Now(),
Connector: &types.ConnectorRef{
ID: noMFASAMLConnector.GetName(),
Type: noMFASAMLConnector.GetKind(),
},
})
_, err = a.UpsertUser(ctx, noMFASAMLUser)
require.NoError(t, err)
// Create a fake saml user with MFA enabled.
samlUser, samlRole, err := CreateUserAndRole(a, "saml-user", []string{"role"}, nil)
require.NoError(t, err)
samlConnector, err := types.NewSAMLConnector("saml", types.SAMLConnectorSpecV2{
AssertionConsumerService: "http://localhost:65535/acs", // not called
Issuer: "test",
SSO: "https://localhost:65535/sso", // not called
AttributesToRoles: []types.AttributeMapping{
// not used. can be any name, value but role must exist
{Name: "groups", Value: "admin", Roles: []string{samlRole.GetName()}},
},
MFASettings: &types.SAMLConnectorMFASettings{
Enabled: true,
Issuer: "test",
Sso: "https://localhost:65535/sso", // not called
},
})
require.NoError(t, err)
_, err = a.UpsertSAMLConnector(ctx, samlConnector)
require.NoError(t, err)
samlUser.SetCreatedBy(types.CreatedBy{
Time: a.clock.Now(),
Connector: &types.ConnectorRef{
ID: samlConnector.GetName(),
Type: samlConnector.GetKind(),
},
})
_, err = a.UpsertUser(ctx, samlUser)
require.NoError(t, err)
// Create a fake oidc user with MFA enabled.
oidcUser, oidcRole, err := CreateUserAndRole(a, "oidc-user", []string{"role"}, nil)
require.NoError(t, err)
oidcConnector, err := types.NewOIDCConnector("oidc", types.OIDCConnectorSpecV3{
ClientID: "12345",
ClientSecret: "678910",
RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
ClaimsToRoles: []types.ClaimMapping{
{
Claim: "test",
Value: "test",
Roles: []string{oidcRole.GetName()},
},
},
MFASettings: &types.OIDCConnectorMFASettings{
Enabled: true,
ClientId: "12345",
ClientSecret: "678910",
},
})
require.NoError(t, err)
_, err = a.UpsertOIDCConnector(ctx, oidcConnector)
require.NoError(t, err)
oidcUser.SetCreatedBy(types.CreatedBy{
Time: a.clock.Now(),
Connector: &types.ConnectorRef{
ID: oidcConnector.GetName(),
Type: oidcConnector.GetKind(),
},
})
_, err = a.UpsertUser(ctx, oidcUser)
require.NoError(t, err)
for _, tt := range []struct {
name string
username string
setup func(t *testing.T)
challengeRequest *proto.CreateAuthenticateChallengeRequest
assertChallenge func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error)
}{
{
name: "NOK non sso user",
username: standardUser.GetName(),
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, // which scope doesn't matter here.
},
SSOClientRedirectURL: "/web/home", // value doesn't matter, as long as it isn't empty.
},
assertChallenge: func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error) {
require.NoError(t, err)
assert.Nil(t, chal.SSOChallenge)
},
},
{
name: "NOK sso mfa not enabled by auth connector",
username: noMFASAMLUser.GetName(),
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, // which scope doesn't matter here.
},
SSOClientRedirectURL: "/web/home", // value doesn't matter, as long as it isn't empty.
},
assertChallenge: func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error) {
require.NoError(t, err)
assert.Nil(t, chal.SSOChallenge)
},
},
{
name: "NOK sso mfa not enabled by auth preference",
username: samlUser.GetName(),
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, // which scope doesn't matter here.
},
SSOClientRedirectURL: "/web/home", // value doesn't matter, as long as it isn't empty.
},
setup: func(t *testing.T) {
// disable SSO MFA.
authPref.SetSecondFactors()
require.NoError(t, err)
_, err = a.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
t.Cleanup(func() {
authPref.SetSecondFactors(types.SecondFactorType_SECOND_FACTOR_TYPE_SSO)
require.NoError(t, err)
_, err = a.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
})
},
assertChallenge: func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error) {
require.NoError(t, err)
assert.Nil(t, chal.SSOChallenge)
},
},
{
name: "NOK SSOClientRedirectURL not provided",
username: samlUser.GetName(),
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, // which scope doesn't matter here.
},
SSOClientRedirectURL: "",
},
assertChallenge: func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error) {
require.NoError(t, err)
assert.Nil(t, chal.SSOChallenge)
},
},
{
name: "OK saml user",
username: samlUser.GetName(),
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, // which scope doesn't matter here.
},
SSOClientRedirectURL: "/web/home", // value doesn't matter, as long as it isn't empty.
},
assertChallenge: func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error) {
require.NoError(t, err)
require.NotNil(t, chal.SSOChallenge, "expected SSO challenge to be returned")
assert.NotEmpty(t, chal.SSOChallenge.RedirectUrl)
assert.NotEmpty(t, chal.SSOChallenge.RequestId)
// We should find an auth request with the resulting request ID.
req, err := a.GetSAMLAuthRequest(ctx, chal.SSOChallenge.RequestId)
assert.NoError(t, err)
assert.Equal(t, chal.SSOChallenge.RedirectUrl, req.RedirectURL)
assert.Equal(t, chal.SSOChallenge.RequestId, req.ID)
assert.Equal(t, "/web/home", req.ClientRedirectURL)
assert.Equal(t, samlConnector.GetName(), req.ConnectorID)
assert.Equal(t, samlConnector.GetKind(), req.Type)
assert.True(t, req.CheckUser)
// We should find non validated SSO MFA session data tied to the challenge by auth request ID.
sd, err := a.GetSSOMFASessionData(ctx, chal.SSOChallenge.RequestId)
require.NoError(t, err)
assert.Equal(t, &services.SSOMFASessionData{
RequestID: chal.SSOChallenge.RequestId,
Username: samlUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
}, sd)
},
},
{
name: "OK oidc user",
username: oidcUser.GetName(),
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, // which scope doesn't matter here.
},
SSOClientRedirectURL: "/web/home", // value doesn't matter, as long as it isn't empty.
},
assertChallenge: func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error) {
require.NoError(t, err)
require.NotNil(t, chal.SSOChallenge, "expected SSO challenge to be returned")
assert.NotEmpty(t, chal.SSOChallenge.RedirectUrl)
assert.NotEmpty(t, chal.SSOChallenge.RequestId)
// We should find an auth request with the resulting request ID.
req, err := a.GetOIDCAuthRequest(ctx, chal.SSOChallenge.RequestId)
assert.NoError(t, err)
assert.Equal(t, chal.SSOChallenge.RedirectUrl, req.RedirectURL)
assert.Equal(t, chal.SSOChallenge.RequestId, req.StateToken)
assert.Equal(t, "/web/home", req.ClientRedirectURL)
assert.Equal(t, oidcConnector.GetName(), req.ConnectorID)
assert.Equal(t, oidcConnector.GetKind(), req.Type)
assert.True(t, req.CheckUser)
// We should find non validated SSO MFA session data tied to the challenge by auth request ID.
sd, err := a.GetSSOMFASessionData(ctx, chal.SSOChallenge.RequestId)
require.NoError(t, err)
assert.Equal(t, &services.SSOMFASessionData{
RequestID: chal.SSOChallenge.RequestId,
Username: oidcUser.GetName(),
ConnectorID: oidcConnector.GetName(),
ConnectorType: oidcConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
}, sd)
},
},
{
name: "OK allow reuse",
username: samlUser.GetName(),
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN, // which scope doesn't matter here.
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES,
},
SSOClientRedirectURL: "/web/home", // value doesn't matter, as long as it isn't empty.
},
assertChallenge: func(t *testing.T, chal *proto.MFAAuthenticateChallenge, err error) {
require.NoError(t, err)
require.NotNil(t, chal.SSOChallenge, "expected SSO challenge to be returned")
// We should find non validated SSO MFA session data tied to the challenge by auth request ID.
sd, err := a.GetSSOMFASessionData(ctx, chal.SSOChallenge.RequestId)
require.NoError(t, err)
assert.Equal(t, mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES, sd.ChallengeExtensions.AllowReuse)
},
},
} {
t.Run(tt.name, func(t *testing.T) {
userClient, err := testServer.NewClient(TestUser(tt.username))
require.NoError(t, err)
if tt.setup != nil {
tt.setup(t)
}
chal, err := userClient.CreateAuthenticateChallenge(ctx, tt.challengeRequest)
tt.assertChallenge(t, chal, err)
})
}
}
func TestSSOMFAChallenge_Validation(t *testing.T) {
t.Parallel()
ctx := context.Background()
fakeClock := clockwork.NewFakeClock()
testAuthServer, err := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
Clock: fakeClock,
})
require.NoError(t, err)
testServer, err := testAuthServer.NewTestTLSServer()
require.NoError(t, err)
a := testServer.Auth()
// Create a standard user.
standardUser, _, err := CreateUserAndRole(a, "standard", []string{"role"}, nil)
require.NoError(t, err)
// Create a fake saml user with MFA enabled.
samlUser, samlRole, err := CreateUserAndRole(a, "saml-user", []string{"role"}, nil)
require.NoError(t, err)
samlConnector, err := types.NewSAMLConnector("saml", types.SAMLConnectorSpecV2{
AssertionConsumerService: "http://localhost:65535/acs", // not called
Issuer: "test",
SSO: "https://localhost:65535/sso", // not called
AttributesToRoles: []types.AttributeMapping{
// not used. can be any name, value but role must exist
{Name: "groups", Value: "admin", Roles: []string{samlRole.GetName()}},
},
MFASettings: &types.SAMLConnectorMFASettings{
Enabled: true,
Issuer: "test",
Sso: "https://localhost:65535/sso", // not called
},
})
require.NoError(t, err)
_, err = a.UpsertSAMLConnector(ctx, samlConnector)
require.NoError(t, err)
userCreatedAt := a.clock.Now().UTC()
samlUser.SetCreatedBy(types.CreatedBy{
Time: userCreatedAt,
Connector: &types.ConnectorRef{
ID: samlConnector.GetName(),
Type: samlConnector.GetKind(),
},
})
_, err = a.UpsertUser(ctx, samlUser)
require.NoError(t, err)
ssoDevice, err := types.NewMFADevice(samlConnector.GetDisplay(), samlConnector.GetName(), userCreatedAt, &types.MFADevice_Sso{
Sso: &types.SSOMFADevice{
ConnectorId: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
},
})
require.NoError(t, err)
// Create a fake saml user with MFA disabled.
noMFASAMLUser, noMFASAMLRole, err := CreateUserAndRole(a, "saml-user-no-mfa", []string{"role"}, nil)
require.NoError(t, err)
noMFASAMLConnector, err := types.NewSAMLConnector("saml-no-mfa", types.SAMLConnectorSpecV2{
AssertionConsumerService: "http://localhost:65535/acs", // not called
Issuer: "test",
SSO: "https://localhost:65535/sso", // not called
AttributesToRoles: []types.AttributeMapping{
// not used. can be any name, value but role must exist
{Name: "groups", Value: "admin", Roles: []string{noMFASAMLRole.GetName()}},
},
})
require.NoError(t, err)
_, err = a.UpsertSAMLConnector(ctx, noMFASAMLConnector)
require.NoError(t, err)
noMFASAMLUser.SetCreatedBy(types.CreatedBy{
Time: a.clock.Now(),
Connector: &types.ConnectorRef{
ID: noMFASAMLConnector.GetName(),
Type: noMFASAMLConnector.GetKind(),
},
})
_, err = a.UpsertUser(ctx, noMFASAMLUser)
require.NoError(t, err)
for _, tt := range []struct {
name string
username string
sd *services.SSOMFASessionData
ssoResponse *proto.SSOResponse
requiredExtensions *mfav1.ChallengeExtensions
assertValidation func(t *testing.T, mad *authz.MFAAuthData, err error)
}{
{
name: "NOK no required extensions data",
username: samlUser.GetName(),
sd: nil,
ssoResponse: nil,
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsBadParameter(err), "expected bad parameter error but got %v", err)
},
},
{
name: "NOK no session data",
username: samlUser.GetName(),
sd: nil,
ssoResponse: &proto.SSOResponse{
RequestId: "unknown",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "NOK mismatch user",
username: samlUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request1",
Username: "wrong-user",
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request1",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "NOK mismatch token",
username: samlUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request2",
Username: samlUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request2",
Token: "wrong-token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "NOK non validated session data",
username: samlUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request2",
Username: samlUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
},
ssoResponse: &proto.SSOResponse{
RequestId: "request2",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "NOK mismatch scope",
username: samlUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request3",
Username: samlUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request3",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_ADMIN_ACTION,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "NOK reuse not allowed",
username: samlUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request4",
Username: samlUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request4",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "NOK sso mfa not enabled by auth connector",
username: noMFASAMLUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request5",
Username: noMFASAMLUser.GetName(),
ConnectorID: noMFASAMLConnector.GetName(),
ConnectorType: noMFASAMLConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request5",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "NOK non sso user",
username: standardUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request6",
Username: standardUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request6",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
require.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
},
},
{
name: "OK sso user",
username: samlUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request7",
Username: samlUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request7",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
assert.NoError(t, err)
assert.Equal(t, &authz.MFAAuthData{
User: samlUser.GetName(),
Device: ssoDevice,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
}, mad)
},
},
{
name: "OK sso user allow reuse",
username: samlUser.GetName(),
sd: &services.SSOMFASessionData{
RequestID: "request8",
Username: samlUser.GetName(),
ConnectorID: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES,
},
Token: "token",
},
ssoResponse: &proto.SSOResponse{
RequestId: "request8",
Token: "token",
},
requiredExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES,
},
assertValidation: func(t *testing.T, mad *authz.MFAAuthData, err error) {
assert.NoError(t, err)
assert.Equal(t, &authz.MFAAuthData{
User: samlUser.GetName(),
Device: ssoDevice,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES,
}, mad)
},
},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.sd != nil {
err := a.UpsertSSOMFASessionData(ctx, tt.sd)
require.NoError(t, err)
}
data, err := a.ValidateMFAAuthResponse(ctx, &proto.MFAAuthenticateResponse{
Response: &proto.MFAAuthenticateResponse_SSO{
SSO: tt.ssoResponse,
},
}, tt.username, tt.requiredExtensions)
tt.assertValidation(t, data, err)
})
}
}
type fakeSSOService struct {
a *Server
}
func (s *fakeSSOService) CreateSAMLAuthRequest(ctx context.Context, req types.SAMLAuthRequest) (*types.SAMLAuthRequest, error) {
return nil, nil // unused in these tests.
}
func (s *fakeSSOService) CreateSAMLAuthRequestForMFA(ctx context.Context, req types.SAMLAuthRequest) (*types.SAMLAuthRequest, error) {
req.ID = uuid.NewString()
req.RedirectURL = uuid.NewString()
return &req, s.a.Services.CreateSAMLAuthRequest(ctx, req, defaults.SAMLAuthRequestTTL)
}
func (s *fakeSSOService) ValidateSAMLResponse(ctx context.Context, samlResponse, connectorID, clientIP string) (*authclient.SAMLAuthResponse, error) {
return nil, nil // unused in these tests.
}
func (s *fakeSSOService) CreateOIDCAuthRequest(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error) {
return nil, nil // unused in these tests.
}
func (s *fakeSSOService) CreateOIDCAuthRequestForMFA(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error) {
req.StateToken = uuid.NewString()
req.RedirectURL = uuid.NewString()
return &req, s.a.Services.CreateOIDCAuthRequest(ctx, req, defaults.OIDCAuthRequestTTL)
}
func (s *fakeSSOService) ValidateOIDCAuthCallback(ctx context.Context, q url.Values) (*authclient.OIDCAuthResponse, error) {
return nil, nil // unused in these tests.
}