diff --git a/e b/e
index 6915e4210d56b..69b08e7d42731 160000
--- a/e
+++ b/e
@@ -1 +1 @@
-Subproject commit 6915e4210d56bd788a405b330da7016a9a752e0d
+Subproject commit 69b08e7d42731388572287d35a53aa895b0d78e1
diff --git a/fuzz/oss-fuzz-build.sh b/fuzz/oss-fuzz-build.sh
index 355fd81eb25fe..01124f85aa60d 100755
--- a/fuzz/oss-fuzz-build.sh
+++ b/fuzz/oss-fuzz-build.sh
@@ -26,9 +26,6 @@ build_teleport_fuzzers() {
compile_native_go_fuzzer $TELEPORT_PREFIX/lib/services \
FuzzParserEvalBoolPredicate fuzz_parser_eval_bool_predicate
- compile_native_go_fuzzer $TELEPORT_PREFIX/lib/auth \
- FuzzParseSAMLInResponseTo fuzz_parse_saml_in_response_to
-
compile_native_go_fuzzer $TELEPORT_PREFIX/lib/restrictedsession \
FuzzParseIPSpec fuzz_parse_ip_spec
diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go
index af75674e08fb1..0d00f16687d01 100644
--- a/lib/auth/apiserver.go
+++ b/lib/auth/apiserver.go
@@ -171,8 +171,6 @@ func NewAPIServer(config *APIConfig) (http.Handler, error) {
srv.POST("/:version/configuration/static_tokens", srv.WithAuth(srv.setStaticTokens))
// SSO validation handlers
- srv.POST("/:version/oidc/requests/validate", srv.WithAuth(srv.validateOIDCAuthCallback))
- srv.POST("/:version/saml/requests/validate", srv.WithAuth(srv.validateSAMLResponse))
srv.POST("/:version/github/requests/validate", srv.WithAuth(srv.validateGithubAuthCallback))
// Audit logs AKA events
@@ -868,74 +866,6 @@ func (s *APIServer) getSession(auth ClientI, w http.ResponseWriter, r *http.Requ
return se, nil
}
-func (s *APIServer) validateOIDCAuthCallback(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
- var req *ValidateOIDCAuthCallbackReq
- if err := httplib.ReadJSON(r, &req); err != nil {
- return nil, trace.Wrap(err)
- }
- response, err := auth.ValidateOIDCAuthCallback(r.Context(), req.Query)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- raw := OIDCAuthRawResponse{
- Username: response.Username,
- Identity: response.Identity,
- Cert: response.Cert,
- TLSCert: response.TLSCert,
- Req: response.Req,
- }
- if response.Session != nil {
- rawSession, err := services.MarshalWebSession(response.Session, services.WithVersion(version))
- if err != nil {
- return nil, trace.Wrap(err)
- }
- raw.Session = rawSession
- }
- raw.HostSigners = make([]json.RawMessage, len(response.HostSigners))
- for i, ca := range response.HostSigners {
- data, err := services.MarshalCertAuthority(ca, services.WithVersion(version))
- if err != nil {
- return nil, trace.Wrap(err)
- }
- raw.HostSigners[i] = data
- }
- return &raw, nil
-}
-
-func (s *APIServer) validateSAMLResponse(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
- var req *ValidateSAMLResponseReq
- if err := httplib.ReadJSON(r, &req); err != nil {
- return nil, trace.Wrap(err)
- }
- response, err := auth.ValidateSAMLResponse(r.Context(), req.Response, req.ConnectorID)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- raw := SAMLAuthRawResponse{
- Username: response.Username,
- Identity: response.Identity,
- Cert: response.Cert,
- Req: response.Req,
- TLSCert: response.TLSCert,
- }
- if response.Session != nil {
- rawSession, err := services.MarshalWebSession(response.Session, services.WithVersion(version))
- if err != nil {
- return nil, trace.Wrap(err)
- }
- raw.Session = rawSession
- }
- raw.HostSigners = make([]json.RawMessage, len(response.HostSigners))
- for i, ca := range response.HostSigners {
- data, err := services.MarshalCertAuthority(ca, services.WithVersion(version))
- if err != nil {
- return nil, trace.Wrap(err)
- }
- raw.HostSigners[i] = data
- }
- return &raw, nil
-}
-
// validateGithubAuthCallbackReq is a request to validate Github OAuth2 callback
type validateGithubAuthCallbackReq struct {
// Query is the callback query string
diff --git a/lib/auth/auth.go b/lib/auth/auth.go
index e0e3440bc08de..5333856f285b1 100644
--- a/lib/auth/auth.go
+++ b/lib/auth/auth.go
@@ -292,22 +292,6 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
)
}
}
- // Plug in SAML auth service
- sas := NewSAMLAuthService(&SAMLAuthServiceConfig{
- Auth: &as,
- Emitter: as.emitter,
- AssertionReplayService: as.Unstable.AssertionReplayService,
- })
- as.SetSAMLService(sas)
-
- oas, err := NewOIDCAuthService(&OIDCAuthServiceConfig{
- Auth: &as,
- Emitter: as.emitter,
- })
- if err != nil {
- return nil, trace.Wrap(err)
- }
- as.SetOIDCService(oas)
return &as, nil
}
diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go
index 8a52e18534d44..f9f5d4e882153 100644
--- a/lib/auth/auth_with_roles_test.go
+++ b/lib/auth/auth_with_roles_test.go
@@ -44,7 +44,6 @@ import (
"github.com/gravitational/teleport/lib/auth/testauthority"
libdefaults "github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
- "github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/tlsca"
@@ -156,181 +155,6 @@ func TestSSOUserCanReissueCert(t *testing.T) {
require.NoError(t, err)
}
-func TestSAMLAuthRequest(t *testing.T) {
- ctx := context.Background()
- srv := newTestTLSServer(t)
-
- emptyRole, err := CreateRole(ctx, srv.Auth(), "test-empty", types.RoleSpecV5{})
- require.NoError(t, err)
-
- _, err = CreateRole(ctx, srv.Auth(), "baz", types.RoleSpecV5{})
- require.NoError(t, err)
-
- access1Role, err := CreateRole(ctx, srv.Auth(), "test-access-1", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindSAMLRequest},
- Verbs: []string{types.VerbCreate},
- },
- },
- },
- })
- require.NoError(t, err)
-
- access2Role, err := CreateRole(ctx, srv.Auth(), "test-access-2", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindSAML},
- Verbs: []string{types.VerbCreate},
- },
- },
- },
- })
- require.NoError(t, err)
-
- access3Role, err := CreateRole(ctx, srv.Auth(), "test-access-3", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindSAML, types.KindSAMLRequest},
- Verbs: []string{types.VerbCreate},
- },
- },
- },
- })
- require.NoError(t, err)
-
- readerRole, err := CreateRole(ctx, srv.Auth(), "test-access-4", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindSAMLRequest},
- Verbs: []string{types.VerbRead},
- },
- },
- },
- })
- require.NoError(t, err)
-
- conn, err := types.NewSAMLConnector("foo", types.SAMLConnectorSpecV2{
- Issuer: "test",
- SSO: "test",
- Cert: fixtures.TLSCACertPEM,
- AssertionConsumerService: "test",
- AttributesToRoles: []types.AttributeMapping{{
- Name: "foo",
- Value: "bar",
- Roles: []string{"baz"},
- }},
- })
- require.NoError(t, err)
-
- err = srv.Auth().UpsertSAMLConnector(ctx, conn)
- require.NoError(t, err)
-
- reqNormal := types.SAMLAuthRequest{ConnectorID: conn.GetName(), Type: constants.SAML}
- reqTest := types.SAMLAuthRequest{ConnectorID: conn.GetName(), Type: constants.SAML, SSOTestFlow: true, ConnectorSpec: &types.SAMLConnectorSpecV2{
- Issuer: "test",
- Audience: "test",
- ServiceProviderIssuer: "test",
- SSO: "test",
- Cert: fixtures.TLSCACertPEM,
- AssertionConsumerService: "test",
- AttributesToRoles: []types.AttributeMapping{{
- Name: "foo",
- Value: "bar",
- Roles: []string{"baz"},
- }},
- }}
-
- tests := []struct {
- desc string
- roles []string
- request types.SAMLAuthRequest
- expectAccessDenied bool
- }{
- {
- desc: "empty role - no access",
- roles: []string{emptyRole.GetName()},
- request: reqNormal,
- expectAccessDenied: true,
- },
- {
- desc: "can create regular request with normal access",
- roles: []string{access1Role.GetName()},
- request: reqNormal,
- expectAccessDenied: false,
- },
- {
- desc: "cannot create sso test request with normal access",
- roles: []string{access1Role.GetName()},
- request: reqTest,
- expectAccessDenied: true,
- },
- {
- desc: "cannot create normal request with connector access",
- roles: []string{access2Role.GetName()},
- request: reqNormal,
- expectAccessDenied: true,
- },
- {
- desc: "cannot create sso test request with connector access",
- roles: []string{access2Role.GetName()},
- request: reqTest,
- expectAccessDenied: true,
- },
- {
- desc: "can create regular request with combined access",
- roles: []string{access3Role.GetName()},
- request: reqNormal,
- expectAccessDenied: false,
- },
- {
- desc: "can create sso test request with combined access",
- roles: []string{access3Role.GetName()},
- request: reqTest,
- expectAccessDenied: false,
- },
- }
-
- user, err := CreateUser(srv.Auth(), "dummy")
- require.NoError(t, err)
-
- userReader, err := CreateUser(srv.Auth(), "dummy-reader", readerRole)
- require.NoError(t, err)
-
- clientReader, err := srv.NewClient(TestUser(userReader.GetName()))
- require.NoError(t, err)
-
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- user.SetRoles(tt.roles)
- err = srv.Auth().UpsertUser(user)
- require.NoError(t, err)
-
- client, err := srv.NewClient(TestUser(user.GetName()))
- require.NoError(t, err)
-
- request, err := client.CreateSAMLAuthRequest(ctx, tt.request)
- if tt.expectAccessDenied {
- require.Error(t, err)
- require.True(t, trace.IsAccessDenied(err), "expected access denied, got: %v", err)
- return
- }
-
- require.NoError(t, err)
- require.NotEmpty(t, request.ID)
- require.Equal(t, tt.request.ConnectorID, request.ConnectorID)
-
- requestCopy, err := clientReader.GetSAMLAuthRequest(ctx, request.ID)
- require.NoError(t, err)
- require.Equal(t, request, requestCopy)
- })
- }
-}
-
func TestInstaller(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)
@@ -423,191 +247,6 @@ func TestInstaller(t *testing.T) {
}
}
-func TestOIDCAuthRequest(t *testing.T) {
- ctx := context.Background()
- srv := newTestTLSServer(t)
-
- idp := newFakeIDP(t, false /* tls */)
-
- emptyRole, err := CreateRole(ctx, srv.Auth(), "test-empty", types.RoleSpecV5{})
- require.NoError(t, err)
-
- access1Role, err := CreateRole(ctx, srv.Auth(), "test-access-1", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindOIDCRequest},
- Verbs: []string{types.VerbCreate},
- },
- },
- },
- })
- require.NoError(t, err)
-
- access2Role, err := CreateRole(ctx, srv.Auth(), "test-access-2", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindOIDC},
- Verbs: []string{types.VerbCreate},
- },
- },
- },
- })
- require.NoError(t, err)
-
- access3Role, err := CreateRole(ctx, srv.Auth(), "test-access-3", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindOIDC, types.KindOIDCRequest},
- Verbs: []string{types.VerbCreate},
- },
- },
- },
- })
- require.NoError(t, err)
-
- readerRole, err := CreateRole(ctx, srv.Auth(), "test-access-4", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Rules: []types.Rule{
- {
- Resources: []string{types.KindOIDCRequest},
- Verbs: []string{types.VerbRead},
- },
- },
- },
- })
- require.NoError(t, err)
-
- conn, err := types.NewOIDCConnector("example", types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "example-client-id",
- ClientSecret: "example-client-secret",
- RedirectURLs: []string{"https://localhost:3080/v1/webapi/oidc/callback"},
- Display: "sign in with example.com",
- Scope: []string{"foo", "bar"},
- ClaimsToRoles: []types.ClaimMapping{
- {
- Claim: "groups",
- Value: "idp-admin",
- Roles: []string{"access"},
- },
- },
- })
- require.NoError(t, err)
-
- err = srv.Auth().UpsertOIDCConnector(context.Background(), conn)
- require.NoError(t, err)
-
- reqNormal := types.OIDCAuthRequest{ConnectorID: conn.GetName(), Type: constants.OIDC}
- reqTest := types.OIDCAuthRequest{
- ConnectorID: conn.GetName(),
- Type: constants.OIDC,
- SSOTestFlow: true,
- ConnectorSpec: &types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "example-client-id",
- ClientSecret: "example-client-secret",
- RedirectURLs: []string{"https://localhost:3080/v1/webapi/oidc/callback"},
- Display: "sign in with example.com",
- Scope: []string{"foo", "bar"},
- ClaimsToRoles: []types.ClaimMapping{
- {
- Claim: "groups",
- Value: "idp-admin",
- Roles: []string{"access"},
- },
- },
- },
- }
-
- tests := []struct {
- desc string
- roles []string
- request types.OIDCAuthRequest
- expectAccessDenied bool
- }{
- {
- desc: "empty role - no access",
- roles: []string{emptyRole.GetName()},
- request: reqNormal,
- expectAccessDenied: true,
- },
- {
- desc: "can create regular request with normal access",
- roles: []string{access1Role.GetName()},
- request: reqNormal,
- expectAccessDenied: false,
- },
- {
- desc: "cannot create sso test request with normal access",
- roles: []string{access1Role.GetName()},
- request: reqTest,
- expectAccessDenied: true,
- },
- {
- desc: "cannot create normal request with connector access",
- roles: []string{access2Role.GetName()},
- request: reqNormal,
- expectAccessDenied: true,
- },
- {
- desc: "cannot create sso test request with connector access",
- roles: []string{access2Role.GetName()},
- request: reqTest,
- expectAccessDenied: true,
- },
- {
- desc: "can create regular request with combined access",
- roles: []string{access3Role.GetName()},
- request: reqNormal,
- expectAccessDenied: false,
- },
- {
- desc: "can create sso test request with combined access",
- roles: []string{access3Role.GetName()},
- request: reqTest,
- expectAccessDenied: false,
- },
- }
-
- user, err := CreateUser(srv.Auth(), "dummy")
- require.NoError(t, err)
-
- userReader, err := CreateUser(srv.Auth(), "dummy-reader", readerRole)
- require.NoError(t, err)
-
- clientReader, err := srv.NewClient(TestUser(userReader.GetName()))
- require.NoError(t, err)
-
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- user.SetRoles(tt.roles)
- err = srv.Auth().UpsertUser(user)
- require.NoError(t, err)
-
- client, err := srv.NewClient(TestUser(user.GetName()))
- require.NoError(t, err)
-
- request, err := client.CreateOIDCAuthRequest(ctx, tt.request)
- if tt.expectAccessDenied {
- require.Error(t, err)
- require.True(t, trace.IsAccessDenied(err), "expected access denied, got: %v", err)
- return
- }
-
- require.NoError(t, err)
- require.NotEmpty(t, request.StateToken)
- require.Equal(t, tt.request.ConnectorID, request.ConnectorID)
-
- requestCopy, err := clientReader.GetOIDCAuthRequest(ctx, request.StateToken)
- require.NoError(t, err)
- require.Equal(t, request, requestCopy)
- })
- }
-}
-
func TestGithubAuthRequest(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)
diff --git a/lib/auth/fuzz_test.go b/lib/auth/fuzz_test.go
index 5948092de34b7..3c3fd8d758db3 100644
--- a/lib/auth/fuzz_test.go
+++ b/lib/auth/fuzz_test.go
@@ -17,24 +17,11 @@ limitations under the License.
package auth
import (
- "encoding/base64"
"testing"
- "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
-func FuzzParseSAMLInResponseTo(f *testing.F) {
- // Disable Go App Engine logging
- logrus.SetLevel(logrus.PanicLevel)
-
- f.Fuzz(func(t *testing.T, response string) {
- require.NotPanics(t, func() {
- ParseSAMLInResponseTo(base64.StdEncoding.EncodeToString([]byte(response)))
- })
- })
-}
-
func FuzzParseAndVerifyIID(f *testing.F) {
f.Fuzz(func(t *testing.T, iidBytes []byte) {
require.NotPanics(t, func() {
diff --git a/lib/auth/oidc.go b/lib/auth/oidc.go
index 00fb100cecbc2..a79897844b4f6 100644
--- a/lib/auth/oidc.go
+++ b/lib/auth/oidc.go
@@ -19,30 +19,13 @@ package auth
import (
"context"
"encoding/json"
- "fmt"
- "io"
- "net/http"
"net/url"
- "sync"
- "time"
- "github.com/coreos/go-oidc/jose"
- "github.com/coreos/go-oidc/oauth2"
- "github.com/coreos/go-oidc/oidc"
- "github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
- "github.com/gravitational/teleport"
- "github.com/gravitational/teleport/api/constants"
- apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
- apiutils "github.com/gravitational/teleport/api/utils"
- "github.com/gravitational/teleport/api/utils/keys"
- "github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
- "github.com/gravitational/teleport/lib/services"
- "github.com/gravitational/teleport/lib/utils"
)
type OIDCService interface {
@@ -52,6 +35,47 @@ type OIDCService interface {
var errOIDCNotImplemented = trace.AccessDenied("OIDC is only available in enterprise subscriptions")
+// UpsertOIDCConnector creates or updates an OIDC connector.
+func (a *Server) UpsertOIDCConnector(ctx context.Context, connector types.OIDCConnector) error {
+ if err := a.Services.UpsertOIDCConnector(ctx, connector); err != nil {
+ return trace.Wrap(err)
+ }
+ if err := a.emitter.EmitAuditEvent(ctx, &apievents.OIDCConnectorCreate{
+ Metadata: apievents.Metadata{
+ Type: events.OIDCConnectorCreatedEvent,
+ Code: events.OIDCConnectorCreatedCode,
+ },
+ UserMetadata: ClientUserMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: connector.GetName(),
+ },
+ }); err != nil {
+ log.WithError(err).Warn("Failed to emit OIDC connector create event.")
+ }
+
+ return nil
+}
+
+// DeleteOIDCConnector deletes an OIDC connector by name.
+func (a *Server) DeleteOIDCConnector(ctx context.Context, connectorName string) error {
+ if err := a.Services.DeleteOIDCConnector(ctx, connectorName); err != nil {
+ return trace.Wrap(err)
+ }
+ if err := a.emitter.EmitAuditEvent(ctx, &apievents.OIDCConnectorDelete{
+ Metadata: apievents.Metadata{
+ Type: events.OIDCConnectorDeletedEvent,
+ Code: events.OIDCConnectorDeletedCode,
+ },
+ UserMetadata: ClientUserMetadata(ctx),
+ ResourceMetadata: apievents.ResourceMetadata{
+ Name: connectorName,
+ },
+ }); err != nil {
+ log.WithError(err).Warn("Failed to emit OIDC connector delete event.")
+ }
+ return nil
+}
+
func (a *Server) CreateOIDCAuthRequest(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error) {
if a.oidcAuthService == nil {
return nil, errOIDCNotImplemented
@@ -133,1012 +157,3 @@ type OIDCAuthRawResponse struct {
// trusted by proxy, used in console login
HostSigners []json.RawMessage `json:"host_signers"`
}
-
-type OIDCAuthService struct {
- auth *Server
- emitter apievents.Emitter
- clients map[string]*oidcClient
- lock sync.Mutex
- getClaimsFun func(ctx context.Context, oidcClient *oidc.Client, connector types.OIDCConnector, code string) (jose.Claims, error)
-}
-
-type OIDCAuthServiceConfig struct {
- Auth *Server
- Emitter apievents.Emitter
-}
-
-func (cfg *OIDCAuthServiceConfig) CheckAndSetDefaults() error {
- if cfg.Auth == nil {
- return trace.BadParameter("auth.Server not provided")
- }
- if cfg.Emitter == nil {
- cfg.Emitter = events.NewDiscardEmitter()
- }
- return nil
-}
-
-func NewOIDCAuthService(cfg *OIDCAuthServiceConfig) (*OIDCAuthService, error) {
- if err := cfg.CheckAndSetDefaults(); err != nil {
- return nil, err
- }
-
- return &OIDCAuthService{
- auth: cfg.Auth,
- emitter: cfg.Emitter,
- clients: make(map[string]*oidcClient),
- getClaimsFun: getClaims,
- }, nil
-}
-
-// oidcClient is internal structure that stores OIDC client and its config
-type oidcClient struct {
- client *oidc.Client
- connector types.OIDCConnector
- // syncCtx controls the provider sync goroutine.
- syncCtx context.Context
- syncCancel context.CancelFunc
- // firstSync will be closed once the first provider sync succeeds
- firstSync chan struct{}
-}
-
-// ErrOIDCNoRoles results from not mapping any roles from OIDC claims.
-var ErrOIDCNoRoles = trace.AccessDenied("No roles mapped from claims. The mappings may contain typos.")
-
-// getOIDCConnectorAndClient returns the associated oidc connector
-// and client for the given oidc auth request.
-func (oas *OIDCAuthService) getOIDCConnectorAndClient(ctx context.Context, request types.OIDCAuthRequest) (types.OIDCConnector, *oidc.Client, error) {
- // stateless test flow
- if request.SSOTestFlow {
- if request.ConnectorSpec == nil {
- return nil, nil, trace.BadParameter("ConnectorSpec cannot be nil when SSOTestFlow is true")
- }
-
- if request.ConnectorID == "" {
- return nil, nil, trace.BadParameter("ConnectorID cannot be empty")
- }
-
- connector, err := types.NewOIDCConnector(request.ConnectorID, *request.ConnectorSpec)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- // we don't want to cache the client. construct it directly.
- client, err := newOIDCClient(ctx, connector, request.ProxyAddress)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
- if err := client.waitFirstSync(defaults.WebHeadersTimeout); err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- // close this request-scoped oidc client after 10 minutes
- go func() {
- ticker := oas.auth.GetClock().NewTicker(defaults.OIDCAuthRequestTTL)
- defer ticker.Stop()
- select {
- case <-ticker.Chan():
- client.syncCancel()
- case <-client.syncCtx.Done():
- }
- }()
-
- return connector, client.client, nil
- }
-
- // regular execution flow
- connector, err := oas.auth.GetOIDCConnector(ctx, request.ConnectorID, true)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- client, err := oas.getCachedOIDCClient(ctx, connector, request.ProxyAddress)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- // Wait for the client to successfully sync after getting it from the cache.
- // We do this after caching the client to prevent locking the server during
- // the initial sync period.
- if err := client.waitFirstSync(defaults.WebHeadersTimeout); err != nil {
- return nil, nil, trace.Wrap(err)
- }
- return connector, client.client, nil
-}
-
-// getCachedOIDCClient gets a cached oidc client for
-// the given OIDC connector and redirectURL preference.
-func (oas *OIDCAuthService) getCachedOIDCClient(ctx context.Context, conn types.OIDCConnector, proxyAddr string) (*oidcClient, error) {
- oas.lock.Lock()
- defer oas.lock.Unlock()
-
- // Each connector and proxy combination has a distinct client,
- // so we use a composite key to capture all combinations.
- clientMapKey := conn.GetName() + "_" + proxyAddr
-
- cachedClient, ok := oas.clients[clientMapKey]
- if ok {
- if !cachedClient.needsRefresh(conn) && cachedClient.syncCtx.Err() == nil {
- return cachedClient, nil
- }
- // Cached client needs to be refreshed or is no longer syncing.
- cachedClient.syncCancel()
- delete(oas.clients, clientMapKey)
- }
-
- // Create a new oidc client and add it to the cache.
- client, err := newOIDCClient(ctx, conn, proxyAddr)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- oas.clients[clientMapKey] = client
- return client, nil
-}
-
-func newOIDCClient(ctx context.Context, conn types.OIDCConnector, proxyAddr string) (*oidcClient, error) {
- redirectURL, err := services.GetRedirectURL(conn, proxyAddr)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- config := oidcConfig(conn, redirectURL)
- client, err := oidc.NewClient(config)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- oidcClient := &oidcClient{client: client, connector: conn, firstSync: make(chan struct{})}
- oidcClient.startSync(ctx)
- return oidcClient, nil
-}
-
-func oidcConfig(conn types.OIDCConnector, redirectURL string) oidc.ClientConfig {
- return oidc.ClientConfig{
- RedirectURL: redirectURL,
- Credentials: oidc.ClientCredentials{
- ID: conn.GetClientID(),
- Secret: conn.GetClientSecret(),
- },
- // open id notifies provider that we are using OIDC scopes
- Scope: apiutils.Deduplicate(append([]string{"openid", "email"}, conn.GetScope()...)),
- }
-}
-
-// needsRefresh returns whether the client's connector and the
-// given connector have the same values for fields relevant to
-// generating and syncing an oidc.Client.
-func (c *oidcClient) needsRefresh(conn types.OIDCConnector) bool {
- return !cmp.Equal(conn.GetRedirectURLs(), c.connector.GetRedirectURLs()) ||
- conn.GetClientID() != c.connector.GetClientID() ||
- conn.GetClientSecret() != c.connector.GetClientSecret() ||
- !cmp.Equal(conn.GetScope(), c.connector.GetScope()) ||
- conn.GetIssuerURL() != c.connector.GetIssuerURL()
-}
-
-// startSync starts a goroutine to sync the client with its provider
-// config until the given ctx is closed or the sync is canceled.
-func (c *oidcClient) startSync(ctx context.Context) {
- // SyncProviderConfig doesn't take a context for cancellation, instead it
- // returns a channel that has to be closed to stop the sync. To ensure that the
- // sync is eventually stopped, we "wrap" the stop channel with a cancel context.
- c.syncCtx, c.syncCancel = context.WithCancel(ctx)
- go func() {
- stop := c.client.SyncProviderConfig(c.connector.GetIssuerURL())
- close(c.firstSync)
- <-c.syncCtx.Done()
- close(stop)
- }()
-}
-
-// waitFirstSync waits for the client to start syncing successfully, or
-// returns an error if syncing fails or fails to succeed within 10 seconds.
-// This prevents waiting on clients with faulty provider configurations.
-func (c *oidcClient) waitFirstSync(timeout time.Duration) error {
- timeoutTimer := time.NewTimer(timeout)
-
- select {
- case <-c.firstSync:
- case <-c.syncCtx.Done():
- case <-timeoutTimer.C:
- // cancel sync so that it gets removed from the cache
- c.syncCancel()
- return trace.ConnectionProblem(nil, "timed out syncing oidc connector %v, ensure URL %q is valid and accessible and check configuration",
- c.connector.GetName(), c.connector.GetIssuerURL())
- }
-
- // stop and flush timer
- if !timeoutTimer.Stop() {
- <-timeoutTimer.C
- }
-
- // return the syncing error if there is one
- return trace.Wrap(c.syncCtx.Err())
-}
-
-// UpsertOIDCConnector creates or updates an OIDC connector.
-func (a *Server) UpsertOIDCConnector(ctx context.Context, connector types.OIDCConnector) error {
- if err := a.Services.UpsertOIDCConnector(ctx, connector); err != nil {
- return trace.Wrap(err)
- }
- if err := a.emitter.EmitAuditEvent(ctx, &apievents.OIDCConnectorCreate{
- Metadata: apievents.Metadata{
- Type: events.OIDCConnectorCreatedEvent,
- Code: events.OIDCConnectorCreatedCode,
- },
- UserMetadata: ClientUserMetadata(ctx),
- ResourceMetadata: apievents.ResourceMetadata{
- Name: connector.GetName(),
- },
- }); err != nil {
- log.WithError(err).Warn("Failed to emit OIDC connector create event.")
- }
-
- return nil
-}
-
-// DeleteOIDCConnector deletes an OIDC connector by name.
-func (a *Server) DeleteOIDCConnector(ctx context.Context, connectorName string) error {
- if err := a.Services.DeleteOIDCConnector(ctx, connectorName); err != nil {
- return trace.Wrap(err)
- }
- if err := a.emitter.EmitAuditEvent(ctx, &apievents.OIDCConnectorDelete{
- Metadata: apievents.Metadata{
- Type: events.OIDCConnectorDeletedEvent,
- Code: events.OIDCConnectorDeletedCode,
- },
- UserMetadata: ClientUserMetadata(ctx),
- ResourceMetadata: apievents.ResourceMetadata{
- Name: connectorName,
- },
- }); err != nil {
- log.WithError(err).Warn("Failed to emit OIDC connector delete event.")
- }
- return nil
-}
-
-func (oas *OIDCAuthService) CreateOIDCAuthRequest(ctx context.Context, req types.OIDCAuthRequest) (*types.OIDCAuthRequest, error) {
- // ensure prompt removal of OIDC client in test flows. does nothing in regular flows.
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
-
- connector, client, err := oas.getOIDCConnectorAndClient(ctx, req)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- oauthClient, err := client.OAuthClient()
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- stateToken, err := utils.CryptoRandomHex(TokenLenBytes)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- req.StateToken = stateToken
-
- // online indicates that this login should only work online
- req.RedirectURL = oauthClient.AuthCodeURL(req.StateToken, teleport.OIDCAccessTypeOnline, connector.GetPrompt())
-
- // if the connector has an Authentication Context Class Reference (ACR) value set,
- // update redirect url and add it as a query value.
- acrValue := connector.GetACR()
- if acrValue != "" {
- u, err := url.Parse(req.RedirectURL)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- q := u.Query()
- q.Set("acr_values", acrValue)
- u.RawQuery = q.Encode()
- req.RedirectURL = u.String()
- }
-
- log.Debugf("OIDC redirect URL: %v.", req.RedirectURL)
-
- err = oas.auth.Services.CreateOIDCAuthRequest(ctx, req, defaults.OIDCAuthRequestTTL)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- return &req, nil
-}
-
-// ValidateOIDCAuthCallback is called by the proxy to check OIDC query parameters
-// returned by OIDC Provider, if everything checks out, auth server
-// will respond with OIDCAuthResponse, otherwise it will return error
-func (oas *OIDCAuthService) ValidateOIDCAuthCallback(ctx context.Context, q url.Values) (*OIDCAuthResponse, error) {
- event := &apievents.UserLogin{
- Metadata: apievents.Metadata{
- Type: events.UserLoginEvent,
- },
- Method: events.LoginMethodOIDC,
- }
-
- diagCtx := NewSSODiagContext(types.KindOIDC, oas.auth)
-
- auth, err := oas.validateOIDCAuthCallback(ctx, diagCtx, q)
- diagCtx.Info.Error = trace.UserMessage(err)
-
- diagCtx.WriteToBackend(ctx)
-
- claims := diagCtx.Info.OIDCClaims
- if claims != nil {
- attributes, err := apievents.EncodeMap(claims)
- if err != nil {
- event.Status.UserMessage = fmt.Sprintf("Failed to encode identity attributes: %v", err.Error())
- log.WithError(err).Debug("Failed to encode identity attributes.")
- } else {
- event.IdentityAttributes = attributes
- }
- }
-
- if err != nil {
- event.Code = events.UserSSOLoginFailureCode
- if diagCtx.Info.TestFlow {
- event.Code = events.UserSSOTestFlowLoginFailureCode
- }
- event.Status.Success = false
- event.Status.Error = trace.Unwrap(err).Error()
- event.Status.UserMessage = err.Error()
-
- if err := oas.emitter.EmitAuditEvent(ctx, event); err != nil {
- log.WithError(err).Warn("Failed to emit OIDC login failed event.")
- }
-
- return nil, trace.Wrap(err)
- }
-
- event.Code = events.UserSSOLoginCode
- if diagCtx.Info.TestFlow {
- event.Code = events.UserSSOTestFlowLoginCode
- }
- event.User = auth.Username
- event.Status.Success = true
-
- if err := oas.emitter.EmitAuditEvent(ctx, event); err != nil {
- log.WithError(err).Warn("Failed to emit OIDC login event.")
- }
-
- return auth, nil
-}
-
-func checkEmailVerifiedClaim(claims jose.Claims) error {
- claimName := "email_verified"
- unverifiedErr := trace.AccessDenied("email not verified by OIDC provider")
-
- emailVerified, hasEmailVerifiedClaim, _ := claims.StringClaim(claimName)
- if hasEmailVerifiedClaim {
- if emailVerified == "false" {
- return unverifiedErr
- }
- if emailVerified == "true" {
- return nil
- }
-
- return trace.BadParameter("unable to parse oidc claim: %q, must be either 'true' or 'false', got '%s'", claimName, emailVerified)
- }
-
- data, ok := claims[claimName]
- if !ok {
- return nil
- }
-
- emailVerifiedBool, ok := data.(bool)
- if !ok {
- return trace.BadParameter("unable to parse oidc claim: %q, must be a string or bool", claimName)
- }
-
- if !emailVerifiedBool {
- return unverifiedErr
- }
-
- return nil
-}
-
-func (oas *OIDCAuthService) validateOIDCAuthCallback(ctx context.Context, diagCtx *SSODiagContext, q url.Values) (*OIDCAuthResponse, error) {
- if errParam := q.Get("error"); errParam != "" {
- // try to find request so the error gets logged against it.
- state := q.Get("state")
- if state != "" {
- diagCtx.RequestID = state
- req, err := oas.auth.GetOIDCAuthRequest(ctx, state)
- if err == nil {
- diagCtx.Info.TestFlow = req.SSOTestFlow
- }
- }
-
- // optional parameter: error_description
- errDesc := q.Get("error_description")
- oidcErr := trace.OAuth2(oauth2.ErrorInvalidRequest, errParam, q)
- return nil, trace.WithUserMessage(oidcErr, "OIDC provider returned error: %v [%v]", errDesc, errParam)
- }
-
- code := q.Get("code")
- if code == "" {
- oidcErr := trace.OAuth2(oauth2.ErrorInvalidRequest, "code query param must be set", q)
- return nil, trace.WithUserMessage(oidcErr, "Invalid parameters received from OIDC provider.")
- }
-
- stateToken := q.Get("state")
- if stateToken == "" {
- oidcErr := trace.OAuth2(oauth2.ErrorInvalidRequest, "missing state query param", q)
- return nil, trace.WithUserMessage(oidcErr, "Invalid parameters received from OIDC provider.")
- }
- diagCtx.RequestID = stateToken
-
- req, err := oas.auth.GetOIDCAuthRequest(ctx, stateToken)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to get OIDC Auth Request.")
- }
- diagCtx.Info.TestFlow = req.SSOTestFlow
-
- // ensure prompt removal of OIDC client in test flows. does nothing in regular flows.
- ctxC, cancel := context.WithCancel(ctx)
- defer cancel()
-
- connector, client, err := oas.getOIDCConnectorAndClient(ctxC, *req)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to get OIDC connector and client.")
- }
-
- // extract claims from both the id token and the userinfo endpoint and merge them
- claims, err := oas.getClaims(ctx, client, connector, code)
- if err != nil {
- // different error message for Google Workspace as likely cause is different.
- if isGoogleWorkspaceConnector(connector) {
- return nil, trace.Wrap(err, "Failed to extract OIDC claims. Check your Google Workspace plan and enabled APIs. See: https://goteleport.com/docs/enterprise/sso/google-workspace/#ensure-your-google-workspace-plan-is-correct")
- }
-
- return nil, trace.Wrap(err, "Failed to extract OIDC claims. This may indicate need to set 'provider' flag in connector definition. See: https://goteleport.com/docs/enterprise/sso/#provider-specific-workarounds")
- }
- diagCtx.Info.OIDCClaims = types.OIDCClaims(claims)
-
- log.Debugf("OIDC claims: %v.", claims)
- if !connector.GetAllowUnverifiedEmail() {
- if err := checkEmailVerifiedClaim(claims); err != nil {
- return nil, trace.Wrap(err, "OIDC provider did not verify email.")
- }
- }
-
- // if we are sending acr values, make sure we also validate them
- acrValue := connector.GetACR()
- if acrValue != "" {
- err := validateACRValues(acrValue, connector.GetProvider(), claims)
- if err != nil {
- return nil, trace.Wrap(err, "OIDC ACR validation failure.")
- }
- log.Debugf("OIDC ACR values %q successfully validated.", acrValue)
- }
-
- ident, err := oidc.IdentityFromClaims(claims)
- if err != nil {
- return nil, trace.OAuth2(
- oauth2.ErrorUnsupportedResponseType, "unable to convert claims to identity", q)
- }
- diagCtx.Info.OIDCIdentity = &types.OIDCIdentity{
- ID: ident.ID,
- Name: ident.Name,
- Email: ident.Email,
- ExpiresAt: ident.ExpiresAt,
- }
- log.Debugf("OIDC user %q expires at: %v.", ident.Email, ident.ExpiresAt)
-
- if len(connector.GetClaimsToRoles()) == 0 {
- oidcErr := trace.BadParameter("no claims to roles mapping, check connector documentation")
- return nil, trace.WithUserMessage(oidcErr, "Claims-to-roles mapping is empty, SSO user will never have any roles.")
- }
- log.Debugf("Applying %v OIDC claims to roles mappings.", len(connector.GetClaimsToRoles()))
- diagCtx.Info.OIDCClaimsToRoles = connector.GetClaimsToRoles()
-
- // Calculate (figure out name, roles, traits, session TTL) of user and
- // create the user in the backend.
- params, err := oas.calculateOIDCUser(diagCtx, connector, claims, ident, req)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to calculate user attributes.")
- }
-
- diagCtx.Info.CreateUserParams = &types.CreateUserParams{
- ConnectorName: params.ConnectorName,
- Username: params.Username,
- KubeGroups: params.KubeGroups,
- KubeUsers: params.KubeUsers,
- Roles: params.Roles,
- Traits: params.Traits,
- SessionTTL: types.Duration(params.SessionTTL),
- }
-
- user, err := oas.createOIDCUser(params, req.SSOTestFlow)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to create user from provided parameters.")
- }
-
- // Auth was successful, return session, certificate, etc. to caller.
- resp := &OIDCAuthResponse{
- Req: OIDCAuthRequestFromProto(req),
- Identity: types.ExternalIdentity{
- ConnectorID: params.ConnectorName,
- Username: params.Username,
- },
- Username: user.GetName(),
- }
-
- // In test flow skip signing and creating web sessions.
- if req.SSOTestFlow {
- diagCtx.Info.Success = true
- return resp, nil
- }
-
- if !req.CheckUser {
- return resp, nil
- }
-
- // If the request is coming from a browser, create a web session.
- if req.CreateWebSession {
- session, err := oas.auth.CreateWebSessionFromReq(ctx, types.NewWebSessionRequest{
- User: user.GetName(),
- Roles: user.GetRoles(),
- Traits: user.GetTraits(),
- SessionTTL: params.SessionTTL,
- LoginTime: oas.auth.GetClock().Now().UTC(),
- })
- if err != nil {
- return nil, trace.Wrap(err, "Failed to create web session.")
- }
- resp.Session = session
- }
-
- // If a public key was provided, sign it and return a certificate.
- if len(req.PublicKey) != 0 {
- sshCert, tlsCert, err := oas.auth.CreateSessionCert(user, params.SessionTTL, req.PublicKey, req.Compatibility, req.RouteToCluster,
- req.KubernetesCluster, keys.AttestationStatementFromProto(req.AttestationStatement))
- if err != nil {
- return nil, trace.Wrap(err, "Failed to create session certificate.")
- }
-
- clusterName, err := oas.auth.GetClusterName()
- if err != nil {
- return nil, trace.Wrap(err, "Failed to obtain cluster name.")
- }
- resp.Cert = sshCert
- resp.TLSCert = tlsCert
-
- // Return the host CA for this cluster only.
- authority, err := oas.auth.GetCertAuthority(ctx, types.CertAuthID{
- Type: types.HostCA,
- DomainName: clusterName.GetClusterName(),
- }, false)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to obtain cluster's host CA.")
- }
- resp.HostSigners = append(resp.HostSigners, authority)
- }
-
- return resp, nil
-}
-
-// OIDCAuthRequestFromProto converts the types.OIDCAuthRequest to OIDCAuthRequest.
-func OIDCAuthRequestFromProto(req *types.OIDCAuthRequest) OIDCAuthRequest {
- return OIDCAuthRequest{
- ConnectorID: req.ConnectorID,
- PublicKey: req.PublicKey,
- CSRFToken: req.CSRFToken,
- CreateWebSession: req.CreateWebSession,
- ClientRedirectURL: req.ClientRedirectURL,
- }
-}
-
-func (oas *OIDCAuthService) calculateOIDCUser(diagCtx *SSODiagContext, connector types.OIDCConnector, claims jose.Claims, ident *oidc.Identity, request *types.OIDCAuthRequest) (*CreateUserParams, error) {
- var err error
-
- username, err := usernameFromClaims(connector, claims, ident)
- if err != nil {
- return nil, err
- }
-
- p := CreateUserParams{
- ConnectorName: connector.GetName(),
- Username: username,
- }
-
- p.Traits = services.OIDCClaimsToTraits(claims)
-
- diagCtx.Info.OIDCTraitsFromClaims = p.Traits
- diagCtx.Info.OIDCConnectorTraitMapping = connector.GetTraitMappings()
-
- var warnings []string
- warnings, p.Roles = services.TraitsToRoles(connector.GetTraitMappings(), p.Traits)
- if len(p.Roles) == 0 {
- if len(warnings) != 0 {
- log.WithField("connector", connector).Warnf("No roles mapped from claims. Warnings: %q", warnings)
- diagCtx.Info.OIDCClaimsToRolesWarnings = &types.SSOWarnings{
- Message: "No roles mapped for the user",
- Warnings: warnings,
- }
- } else {
- log.WithField("connector", connector).Warnf("No roles mapped from claims.")
- diagCtx.Info.OIDCClaimsToRolesWarnings = &types.SSOWarnings{
- Message: "No roles mapped for the user. The mappings may contain typos.",
- }
- }
- return nil, trace.Wrap(ErrOIDCNoRoles)
- }
-
- // Pick smaller for role: session TTL from role or requested TTL.
- roles, err := services.FetchRoles(p.Roles, oas.auth, p.Traits)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- roleTTL := roles.AdjustSessionTTL(apidefaults.MaxCertDuration)
- p.SessionTTL = utils.MinTTL(roleTTL, request.CertTTL)
-
- return &p, nil
-}
-
-func (oas *OIDCAuthService) createOIDCUser(p *CreateUserParams, dryRun bool) (types.User, error) {
- expires := oas.auth.GetClock().Now().UTC().Add(p.SessionTTL)
-
- log.Debugf("Generating dynamic OIDC identity %v/%v with roles: %v. Dry run: %v.", p.ConnectorName, p.Username, p.Roles, dryRun)
- user := &types.UserV2{
- Kind: types.KindUser,
- Version: types.V2,
- Metadata: types.Metadata{
- Name: p.Username,
- Namespace: apidefaults.Namespace,
- Expires: &expires,
- },
- Spec: types.UserSpecV2{
- Roles: p.Roles,
- Traits: p.Traits,
- OIDCIdentities: []types.ExternalIdentity{
- {
- ConnectorID: p.ConnectorName,
- Username: p.Username,
- },
- },
- CreatedBy: types.CreatedBy{
- User: types.UserRef{Name: teleport.UserSystem},
- Time: oas.auth.GetClock().Now().UTC(),
- Connector: &types.ConnectorRef{
- Type: constants.OIDC,
- ID: p.ConnectorName,
- Identity: p.Username,
- },
- },
- },
- }
-
- if dryRun {
- return user, nil
- }
-
- // Get the user to check if it already exists or not.
- existingUser, err := oas.auth.Services.GetUser(p.Username, false)
- if err != nil && !trace.IsNotFound(err) {
- return nil, trace.Wrap(err)
- }
-
- ctx := context.TODO()
-
- // Overwrite exisiting user if it was created from an external identity provider.
- if existingUser != nil {
- connectorRef := existingUser.GetCreatedBy().Connector
-
- // If the exisiting user is a local user, fail and advise how to fix the problem.
- if connectorRef == nil {
- return nil, trace.AlreadyExists("local user with name %q already exists. Either change "+
- "email in OIDC identity or remove local user and try again.", existingUser.GetName())
- }
-
- log.Debugf("Overwriting existing user %q created with %v connector %v.",
- existingUser.GetName(), connectorRef.Type, connectorRef.ID)
-
- if err := oas.auth.UpdateUser(ctx, user); err != nil {
- return nil, trace.Wrap(err)
- }
- } else {
- if err := oas.auth.CreateUser(ctx, user); err != nil {
- return nil, trace.Wrap(err)
- }
- }
-
- return user, nil
-}
-
-// usernameFromClaims gets the username of the OIDC user based on the claims received. The `username_claim` field in the OIDC
-// config specifies which claim from the OIDC provider to use as the user's username. If it isn't specified, the email will be used
-// as the username. If it is specified but specifies a claim that doesn't exist, an error is returned.
-func usernameFromClaims(connector types.OIDCConnector, claims jose.Claims, ident *oidc.Identity) (string, error) {
- usernameClaim := connector.GetUsernameClaim()
- if usernameClaim == "" {
- return ident.Email, nil
- }
-
- username, ok, err := claims.StringClaim(usernameClaim)
- if err != nil {
- return "", err
- } else if !ok {
- return "", trace.BadParameter("The configured username_claim of %q was not received from the IdP. Please update the username_claim in connector %q.", usernameClaim, connector.GetName())
- }
-
- return username, nil
-}
-
-// claimsFromIDToken extracts claims from the ID token.
-func claimsFromIDToken(oidcClient *oidc.Client, idToken string) (jose.Claims, error) {
- jwt, err := jose.ParseJWT(idToken)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- err = oidcClient.VerifyJWT(jwt)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- log.Debugf("Extracting OIDC claims from ID token.")
-
- claims, err := jwt.Claims()
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- return claims, nil
-}
-
-// claimsFromUserInfo finds the UserInfo endpoint from the provider config and then extracts claims from it.
-//
-// Note: We don't request signed JWT responses for UserInfo, instead we force the provider config and
-// the issuer to be HTTPS and leave integrity and confidentiality to TLS. Authenticity is taken care of
-// during the token exchange.
-func claimsFromUserInfo(oidcClient *oidc.Client, issuerURL string, accessToken string) (jose.Claims, error) {
- // If the issuer URL is not HTTPS, return the error as trace.NotFound to
- // allow the caller to treat this condition gracefully and extract claims
- // just from the token.
- err := isHTTPS(issuerURL)
- if err != nil {
- return nil, trace.NotFound(err.Error())
- }
-
- oac, err := oidcClient.OAuthClient()
- if err != nil {
- return nil, trace.Wrap(err)
- }
- hc := oac.HttpClient()
-
- // go get the provider config so we can find out where the UserInfo endpoint
- // is. if the provider doesn't offer a UserInfo endpoint return not found.
- pc, err := oidc.FetchProviderConfig(oac.HttpClient(), issuerURL)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- if pc.UserInfoEndpoint == nil {
- return nil, trace.NotFound("UserInfo endpoint not found")
- }
-
- endpoint := pc.UserInfoEndpoint.String()
-
- // If the userinfo endpoint is not HTTPS, return the error as trace.NotFound to
- // allow the caller to treat this condition gracefully and extract claims
- // just from the token.
- err = isHTTPS(endpoint)
- if err != nil {
- return nil, trace.NotFound(err.Error())
- }
- log.Debugf("Fetching OIDC claims from UserInfo endpoint: %q.", endpoint)
-
- req, err := http.NewRequest("GET", endpoint, nil)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
-
- resp, err := hc.Do(req)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- defer resp.Body.Close()
-
- code := resp.StatusCode
- if code < 200 || code > 299 {
- // These are expected userinfo failures.
- if code == http.StatusBadRequest || code == http.StatusUnauthorized ||
- code == http.StatusForbidden || code == http.StatusMethodNotAllowed {
- return nil, trace.AccessDenied("bad status code: %v", code)
- }
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- return nil, trace.ReadError(code, body)
- }
-
- var claims jose.Claims
- err = json.NewDecoder(resp.Body).Decode(&claims)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- return claims, nil
-}
-
-// mergeClaims merges b into a.
-func mergeClaims(a jose.Claims, b jose.Claims) (jose.Claims, error) {
- for k, v := range b {
- _, ok := a[k]
- if !ok {
- a[k] = v
- }
- }
-
- return a, nil
-}
-
-// getClaims gets claims from ID token and UserInfo and returns UserInfo claims merged into ID token claims.
-func (oas *OIDCAuthService) getClaims(ctx context.Context, oidcClient *oidc.Client, connector types.OIDCConnector, code string) (jose.Claims, error) {
- return oas.getClaimsFun(ctx, oidcClient, connector, code)
-}
-
-// getClaims implements OIDCAuthService.getClaims, but allows that code path to be overridden for testing.
-func getClaims(ctx context.Context, oidcClient *oidc.Client, connector types.OIDCConnector, code string) (jose.Claims, error) {
- oac, err := getOAuthClient(oidcClient, connector)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- t, err := oac.RequestToken(oauth2.GrantTypeAuthCode, code)
- if err != nil {
- if e, ok := err.(*oauth2.Error); ok {
- if e.Type == oauth2.ErrorAccessDenied {
- return nil, trace.Wrap(err, "the client_id and/or client_secret may be incorrect")
- }
- }
- return nil, trace.Wrap(err)
- }
-
- idTokenClaims, err := claimsFromIDToken(oidcClient, t.IDToken)
- if err != nil {
- log.Debugf("Unable to fetch OIDC ID token claims: %v.", err)
- return nil, trace.Wrap(err, "unable to fetch OIDC ID token claims")
- }
- log.Debugf("OIDC ID Token claims: %v.", idTokenClaims)
-
- userInfoClaims, err := claimsFromUserInfo(oidcClient, connector.GetIssuerURL(), t.AccessToken)
- if err != nil {
- if trace.IsNotFound(err) {
- log.Debugf("OIDC provider doesn't offer valid UserInfo endpoint. Returning token claims: %v.", idTokenClaims)
- return idTokenClaims, nil
- }
- // This captures 400, 401, 403, and 405.
- if trace.IsAccessDenied(err) {
- log.Debugf("UserInfo endpoint returned an error: %v. Returning token claims: %v.", err, idTokenClaims)
- return idTokenClaims, nil
- }
- log.Debugf("Unable to fetch UserInfo claims: %v.", err)
- return nil, trace.Wrap(err, "unable to fetch UserInfo claims")
- }
- log.Debugf("UserInfo claims: %v.", userInfoClaims)
-
- // make sure that the subject in the userinfo claim matches the subject in
- // the id token otherwise there is the possibility of a token substitution attack.
- // see section 16.11 of the oidc spec for more details.
- var idsub string
- var uisub string
- var exists bool
- if idsub, exists, err = idTokenClaims.StringClaim("sub"); err != nil || !exists {
- log.Debugf("Unable to extract OIDC sub claim from ID token.")
- return nil, trace.Wrap(err, "unable to extract OIDC sub claim from ID token")
- }
- if uisub, exists, err = userInfoClaims.StringClaim("sub"); err != nil || !exists {
- log.Debugf("Unable to extract OIDC sub claim from UserInfo.")
- return nil, trace.Wrap(err, "unable to extract OIDC sub claim from UserInfo")
- }
- if idsub != uisub {
- log.Debugf("OIDC claim subjects don't match '%v' != '%v'.", idsub, uisub)
- return nil, trace.BadParameter("OIDC claim subjects in UserInfo does not match")
- }
-
- claims, err := mergeClaims(idTokenClaims, userInfoClaims)
- if err != nil {
- log.Debugf("Unable to merge OIDC claims: %v.", err)
- return nil, trace.Wrap(err, "unable to merge OIDC claims")
- }
-
- if isGoogleWorkspaceConnector(connector) {
- claims, err = addGoogleWorkspaceClaims(ctx, connector, claims)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- }
-
- return claims, nil
-}
-
-// getOAuthClient returns a Oauth2 client from the oidc.Client. If the connector is set as a Ping provider sets the Client Secret Post auth method
-func getOAuthClient(oidcClient *oidc.Client, connector types.OIDCConnector) (*oauth2.Client, error) {
- oac, err := oidcClient.OAuthClient()
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- // For OIDC, Ping and Okta will throw an error when the
- // default client secret basic method is used.
- // See: https://github.com/gravitational/teleport/issues/8374
- switch connector.GetProvider() {
- case teleport.Ping, teleport.Okta:
- oac.SetAuthMethod(oauth2.AuthMethodClientSecretPost)
- }
-
- return oac, err
-}
-
-// validateACRValues validates that we get an appropriate response for acr values. By default
-// we expect the same value we send, but this function also handles Identity Provider specific
-// forms of validation.
-func validateACRValues(acrValue string, identityProvider string, claims jose.Claims) error {
- switch identityProvider {
- case teleport.NetIQ:
- log.Debugf("Validating OIDC ACR values with '%v' rules.", identityProvider)
-
- tokenAcr, ok := claims["acr"]
- if !ok {
- return trace.BadParameter("acr not found in claims")
- }
- tokenAcrMap, ok := tokenAcr.(map[string]interface{})
- if !ok {
- return trace.BadParameter("acr unexpected type: %T", tokenAcr)
- }
- tokenAcrValues, ok := tokenAcrMap["values"]
- if !ok {
- return trace.BadParameter("acr.values not found in claims")
- }
- tokenAcrValuesSlice, ok := tokenAcrValues.([]interface{})
- if !ok {
- return trace.BadParameter("acr.values unexpected type: %T", tokenAcr)
- }
-
- acrValueMatched := false
- for _, v := range tokenAcrValuesSlice {
- vv, ok := v.(string)
- if !ok {
- continue
- }
- if acrValue == vv {
- acrValueMatched = true
- break
- }
- }
- if !acrValueMatched {
- log.Debugf("No OIDC ACR match found for '%v' in '%v'.", acrValue, tokenAcrValues)
- return trace.BadParameter("acr claim does not match")
- }
- default:
- log.Debugf("Validating OIDC ACR values with default rules.")
-
- claimValue, exists, err := claims.StringClaim("acr")
- if !exists {
- return trace.BadParameter("acr claim does not exist")
- }
- if err != nil {
- return trace.Wrap(err)
- }
- if claimValue != acrValue {
- log.Debugf("No OIDC ACR match found '%v' != '%v'.", acrValue, claimValue)
- return trace.BadParameter("acr claim does not match")
- }
- }
-
- return nil
-}
-
-// isHTTPS checks if the scheme for a URL is https or not.
-func isHTTPS(u string) error {
- earl, err := url.Parse(u)
- if err != nil {
- return trace.Wrap(err)
- }
- if earl.Scheme != "https" {
- return trace.BadParameter("expected scheme https, got %q", earl.Scheme)
- }
-
- return nil
-}
diff --git a/lib/auth/oidc_google.go b/lib/auth/oidc_google.go
deleted file mode 100644
index fd2595890a23c..0000000000000
--- a/lib/auth/oidc_google.go
+++ /dev/null
@@ -1,256 +0,0 @@
-// Copyright 2022 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"
- "fmt"
- "os"
-
- "github.com/coreos/go-oidc/jose"
- "github.com/gravitational/trace"
- "golang.org/x/oauth2"
- "golang.org/x/oauth2/google"
- directory "google.golang.org/api/admin/directory/v1"
- "google.golang.org/api/cloudidentity/v1"
- "google.golang.org/api/option"
-
- "github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/api/utils"
-)
-
-const (
- // googleWorkspaceIssuerURL is the issuer URL for Google Workspace accounts.
- googleWorkspaceIssuerURL = "https://accounts.google.com"
-
- // googleGroupsClaim is the OIDC claim that we inject into the claims
- // returned for Google Workspace users, containing the email addresses of
- // the Google Groups that the user belongs to.
- googleGroupsClaim = "groups"
-)
-
-// isGoogleWorkspaceConnector returns true if the connector is a OIDC connector
-// for Google Workspace, configured to fetch extra claims.
-func isGoogleWorkspaceConnector(connector types.OIDCConnector) bool {
- // If google_service_account_uri and google_service_account are not set, we
- // assume that this is a non-Google Workspace OIDC provider using the same
- // issuer URL as Google Workspace (e.g.
- // https://developers.google.com/identity/protocols/oauth2/openid-connect).
- return connector.GetIssuerURL() == googleWorkspaceIssuerURL &&
- (connector.GetGoogleServiceAccountURI() != "" || connector.GetGoogleServiceAccount() != "")
-}
-
-// addGoogleWorkspaceClaims will fetch extra data from proprietary Google APIs
-// and it will add claims based on the fetched data. The current implementation
-// adds a "groups" claim containing the Google Groups that the user is a member
-// of.
-func addGoogleWorkspaceClaims(ctx context.Context, connector types.OIDCConnector, claims jose.Claims) (jose.Claims, error) {
- email, exists, err := claims.StringClaim("email")
- if err != nil || !exists {
- return nil, trace.BadParameter("no `email` in oauth claims for Google Workspace account")
- }
-
- var googleGroups []string
- switch connector.GetVersion() {
- // for the V3 connector we first check to see if the service account can use
- // the Cloud Identity API (fetching direct and indirect groups for all
- // domains) and if that's not the case we fall back to the Admin SDK
- // Directory API (fetching direct groups for all domains)
- case types.V3:
- credentials, err := getGoogleWorkspaceCredentials(ctx, connector, cloudidentity.CloudIdentityGroupsReadonlyScope)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- if credentials != nil {
- log.Debugf("fetching transitive Google groups for %v", email)
- googleGroups, err = groupsFromGoogleCloudIdentity(ctx, email, option.WithTokenSource(credentials))
- if err != nil {
- return nil, trace.Wrap(err)
- }
- } else {
- credentials, err := getGoogleWorkspaceCredentials(ctx, connector, directory.AdminDirectoryGroupReadonlyScope)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- if credentials == nil {
- return nil, trace.BadParameter("invalid Google Workspace credentials for scopes %v or %v",
- cloudidentity.CloudIdentityGroupsReadonlyScope, directory.AdminDirectoryGroupReadonlyScope)
- }
-
- log.Debugf("fetching direct Google groups with no domain filtering for %v", email)
- googleGroups, err = groupsFromGoogleDirectory(ctx, email, "", option.WithTokenSource(credentials))
- if err != nil {
- return nil, trace.Wrap(err)
- }
- }
-
- // for the V2 connector we always try to use the Admin SDK Directory API to
- // fetch direct groups filtered by domain, for backwards compatibility
- case types.V2:
- hostedDomain, exists, err := claims.StringClaim("hd")
- if err != nil || !exists {
- return nil, trace.BadParameter("no `hd` in oauth claims for Google Workspace account")
- }
-
- credentials, err := getGoogleWorkspaceCredentials(ctx, connector, directory.AdminDirectoryGroupReadonlyScope)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- if credentials == nil {
- return nil, trace.BadParameter("invalid Google Workspace credentials for scope %v", directory.AdminDirectoryGroupReadonlyScope)
- }
-
- log.Debugf("fetching direct Google groups for %v, filtering by domain %v", email, hostedDomain)
- googleGroups, err = groupsFromGoogleDirectory(ctx, email, hostedDomain, option.WithTokenSource(credentials))
- if err != nil {
- return nil, trace.Wrap(err)
- }
- default:
- return nil, trace.BadParameter("OIDC connector resource version %v is not supported", connector.GetVersion())
- }
-
- if len(googleGroups) > 0 {
- googleClaims := jose.Claims{googleGroupsClaim: googleGroups}
- log.Debugf("Claims from Google Workspace: %v.", googleClaims)
- claims, err = mergeClaims(claims, googleClaims)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- } else {
- log.Debugf("No Google Workspace claims.")
- }
-
- return claims, nil
-}
-
-func getGoogleWorkspaceCredentials(ctx context.Context, connector types.OIDCConnector, scopes ...string) (oauth2.TokenSource, error) {
- var jsonCredentials []byte
- var credentialLoadingMethod string
- if connector.GetGoogleServiceAccountURI() != "" {
- // load the google service account from URI
- credentialLoadingMethod = "google_service_account_uri"
-
- uri, err := utils.ParseSessionsURI(connector.GetGoogleServiceAccountURI())
- if err != nil {
- return nil, trace.BadParameter("failed to parse google_service_account_uri: %v", err)
- }
- jsonCredentials, err = os.ReadFile(uri.Path)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- } else if connector.GetGoogleServiceAccount() != "" {
- // load the google service account from string
- credentialLoadingMethod = "google_service_account"
- jsonCredentials = []byte(connector.GetGoogleServiceAccount())
- }
-
- // we only support service_account credentials (the only ones that allow
- // specifying an arbitrary Subject)
- jwtConfig, err := google.JWTConfigFromJSON(jsonCredentials, scopes...)
- if err != nil {
- return nil, trace.BadParameter("unable to parse google service account from %v: %v", credentialLoadingMethod, err)
- }
- // The "Admin SDK Directory API" needs admin delegation (see
- // https://developers.google.com/admin-sdk/directory/v1/guides/delegation
- // and
- // https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority )
- // and the "Cloud Identity API" needs an account with View permission on
- // all groups to work reliably.
- jwtConfig.Subject = connector.GetGoogleAdminEmail()
-
- tokenSource := jwtConfig.TokenSource(ctx)
- token, err := tokenSource.Token()
- if err != nil || !token.Valid() {
- log.WithError(err).Debugf("failed to obtain valid Google Workspace credentials for scopes %v", scopes)
- return nil, nil
- }
-
- return tokenSource, nil
-}
-
-func groupsFromGoogleDirectory(ctx context.Context, email, filterDomain string, clientOptions ...option.ClientOption) ([]string, error) {
- service, err := directory.NewService(ctx, clientOptions...)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- call := service.Groups.List().UserKey(email)
- if filterDomain != "" {
- call = call.Domain(filterDomain)
- }
-
- var groups []string
- err = call.Pages(ctx, func(resp *directory.Groups) error {
- if resp == nil {
- return nil
- }
- for _, g := range resp.Groups {
- if g != nil && g.Email != "" {
- groups = append(groups, g.Email)
- }
- }
- return nil
- })
- if err != nil {
- return nil, trace.Wrap(err)
- }
- return groups, nil
-}
-
-func groupsFromGoogleCloudIdentity(ctx context.Context, email string, clientOptions ...option.ClientOption) ([]string, error) {
- service, err := cloudidentity.NewService(ctx, clientOptions...)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- // SearchTransitiveGroups takes a fixed parameter as part of the URL
- // ("Format: `groups/{group}`, where `group` is always '-'") and a query
- // parameter that the google API docs claim to be a CEL expression
- // (https://opensource.google/projects/cel) that filters the results based
- // on `member_key_id`, optionally `member_key_namespace`, and `labels`. The
- // query parameter doesn't seem to actually be a CEL expression, and even
- // changing the single quotes into double quotes (which is fine according to
- // the CEL grammar) makes every API call fail with an "Unauthorized" error
- // message.
- //
- // The query string was lifted directly from
- // https://cloud.google.com/identity/docs/how-to/query-memberships#searching_for_all_group_memberships_of_a_member
- // and some more informations on group labels can be found at
- // https://cloud.google.com/identity/docs/groups#group_properties .
- // The actual docs for the API call are at
- // https://cloud.google.com/identity/docs/reference/rest/v1/groups.memberships/searchTransitiveGroups .
- call := service.Groups.Memberships.SearchTransitiveGroups("groups/-").
- Query(fmt.Sprintf("member_key_id == '%s' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels", email))
-
- var groups []string
- err = call.Pages(ctx, func(resp *cloudidentity.SearchTransitiveGroupsResponse) error {
- if resp == nil {
- return nil
- }
- for _, g := range resp.Memberships {
- if g != nil && g.GroupKey != nil && g.GroupKey.Id != "" {
- groups = append(groups, g.GroupKey.Id)
- }
- }
- return nil
- })
- if err != nil {
- return nil, trace.Wrap(err)
- }
- return groups, nil
-}
diff --git a/lib/auth/oidc_test.go b/lib/auth/oidc_test.go
deleted file mode 100644
index 78a96be4e4e70..0000000000000
--- a/lib/auth/oidc_test.go
+++ /dev/null
@@ -1,1001 +0,0 @@
-/*
-Copyright 2019 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"
- "crypto/tls"
- "encoding/json"
- "fmt"
- "net/http"
- "net/http/httptest"
- "net/url"
- "strings"
- "testing"
- "time"
-
- "github.com/coreos/go-oidc/jose"
- "github.com/coreos/go-oidc/oauth2"
- "github.com/coreos/go-oidc/oidc"
- "github.com/gravitational/trace"
- "github.com/jonboulle/clockwork"
- "github.com/stretchr/testify/require"
- directory "google.golang.org/api/admin/directory/v1"
- "google.golang.org/api/cloudidentity/v1"
- "google.golang.org/api/option"
-
- "github.com/gravitational/teleport"
- "github.com/gravitational/teleport/api/constants"
- "github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/lib/auth/keystore"
- authority "github.com/gravitational/teleport/lib/auth/testauthority"
- "github.com/gravitational/teleport/lib/backend"
- "github.com/gravitational/teleport/lib/backend/memory"
- "github.com/gravitational/teleport/lib/defaults"
- "github.com/gravitational/teleport/lib/fixtures"
- "github.com/gravitational/teleport/lib/services"
-)
-
-type OIDCSuite struct {
- a *Server
- b backend.Backend
- c clockwork.FakeClock
- oas *OIDCAuthService
-}
-
-func setUpSuite(t *testing.T) *OIDCSuite {
- s := OIDCSuite{}
-
- ctx := context.Background()
- s.c = clockwork.NewFakeClockAt(time.Now())
-
- var err error
- s.b, err = memory.New(memory.Config{
- Context: ctx,
- Clock: s.c,
- })
- require.NoError(t, err)
-
- clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
- ClusterName: "me.localhost",
- })
- require.NoError(t, err)
-
- authConfig := &InitConfig{
- ClusterName: clusterName,
- Backend: s.b,
- Authority: authority.New(),
- SkipPeriodicOperations: true,
- KeyStoreConfig: keystore.Config{
- Software: keystore.SoftwareConfig{
- RSAKeyPairSource: authority.New().GenerateKeyPair,
- },
- },
- }
- s.a, err = NewServer(authConfig)
- require.NoError(t, err)
-
- var ok bool
- s.oas, ok = s.a.oidcAuthService.(*OIDCAuthService)
- require.True(t, ok, "Server.oidcAuthService is not type *OIDCAuthService")
-
- return &s
-}
-
-// createInsecureOIDCClient creates an insecure client for testing.
-func createInsecureOIDCClient(t *testing.T, connector types.OIDCConnector) *oidc.Client {
- conf := oidcConfig(connector, "")
- conf.HTTPClient = &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: true,
- },
- },
- }
- client, err := oidc.NewClient(conf)
- require.NoError(t, err)
- client.SyncProviderConfig(connector.GetIssuerURL())
- return client
-}
-
-func TestCreateOIDCUser(t *testing.T) {
- t.Parallel()
-
- s := setUpSuite(t)
-
- // Dry-run creation of OIDC user.
- user, err := s.oas.createOIDCUser(&CreateUserParams{
- ConnectorName: "oidcService",
- Username: "foo@example.com",
- Roles: []string{"admin"},
- SessionTTL: 1 * time.Minute,
- }, true)
- require.NoError(t, err)
- require.Equal(t, "foo@example.com", user.GetName())
-
- // Dry-run must not create a user.
- _, err = s.a.GetUser("foo@example.com", false)
- require.Error(t, err)
-
- // Create OIDC user with 1 minute expiry.
- _, err = s.oas.createOIDCUser(&CreateUserParams{
- ConnectorName: "oidcService",
- Username: "foo@example.com",
- Roles: []string{"admin"},
- SessionTTL: 1 * time.Minute,
- }, false)
- require.NoError(t, err)
-
- // Within that 1 minute period the user should still exist.
- _, err = s.a.GetUser("foo@example.com", false)
- require.NoError(t, err)
-
- // Advance time 2 minutes, the user should be gone.
- s.c.Advance(2 * time.Minute)
- _, err = s.a.GetUser("foo@example.com", false)
- require.Error(t, err)
-}
-
-// TestUserInfoBlockHTTP ensures that an insecure userinfo endpoint returns
-// trace.NotFound similar to an invalid userinfo endpoint. For these users,
-// all claim information is already within the token and additional claim
-// information does not need to be fetched.
-func TestUserInfoBlockHTTP(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- s := setUpSuite(t)
-
- // Create configurable IdP to use in tests.
- idp := newFakeIDP(t, false /* tls */)
-
- // Create OIDC connector and client.
- connector, err := types.NewOIDCConnector("test-connector", types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- ClaimsToRoles: []types.ClaimMapping{{Claim: "roles", Value: "teleport-user", Roles: []string{"dictator"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- })
- require.NoError(t, err)
-
- oidcClient, err := s.oas.getCachedOIDCClient(ctx, connector, "")
- require.NoError(t, err)
-
- // Verify HTTP endpoints return trace.NotFound.
- _, err = claimsFromUserInfo(oidcClient.client, idp.s.URL, "")
- fixtures.AssertNotFound(t, err)
-}
-
-// TestUserInfoBadStatus asserts that a 4xx response from userinfo results
-// in AccessDenied.
-func TestUserInfoBadStatus(t *testing.T) {
- t.Parallel()
-
- // Create configurable IdP to use in tests.
- idp := newFakeIDP(t, true /* tls */)
-
- // Create OIDC connector and client.
- connector, err := types.NewOIDCConnector("test-connector", types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- ClaimsToRoles: []types.ClaimMapping{{Claim: "roles", Value: "teleport-user", Roles: []string{"dictator"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- })
- require.NoError(t, err)
- oidcClient := createInsecureOIDCClient(t, connector)
-
- // Verify HTTP endpoints return trace.AccessDenied.
- _, err = claimsFromUserInfo(oidcClient, idp.s.URL, "")
- fixtures.AssertAccessDenied(t, err)
-}
-
-func TestSSODiagnostic(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- claimsToRoles []types.ClaimMapping
- wantValidateErr error
- }{
- {
- name: "success",
- claimsToRoles: []types.ClaimMapping{
- {
- Claim: "groups",
- Value: "idp-admin",
- Roles: []string{"access"},
- },
- },
- },
- {
- name: "fail to map claims to roles",
- claimsToRoles: []types.ClaimMapping{
- {
- Claim: "groups",
- Value: "nonexistant",
- Roles: []string{"access"},
- },
- },
- wantValidateErr: ErrOIDCNoRoles,
- },
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- ctx := context.Background()
- s := setUpSuite(t)
-
- // Create configurable IdP to use in tests.
- idp := newFakeIDP(t, false /* tls */)
-
- // create role referenced in request.
- role, err := types.NewRole("access", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Logins: []string{"dummy"},
- },
- })
- require.NoError(t, err)
- err = s.a.CreateRole(ctx, role)
- require.NoError(t, err)
-
- // connector spec
- spec := types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- Display: "Test",
- Scope: []string{"groups"},
- ClaimsToRoles: tc.claimsToRoles,
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- }
-
- oidcRequest := types.OIDCAuthRequest{
- ConnectorID: "-sso-test-okta",
- Type: constants.OIDC,
- CertTTL: defaults.OIDCAuthRequestTTL,
- SSOTestFlow: true,
- ConnectorSpec: &spec,
- }
-
- request, err := s.a.CreateOIDCAuthRequest(ctx, oidcRequest)
- require.NoError(t, err)
- require.NotNil(t, request)
-
- values := url.Values{
- "code": []string{"XXX-code"},
- "state": []string{request.StateToken},
- }
-
- // override getClaimsFun.
- s.oas.getClaimsFun = func(closeCtx context.Context, oidcClient *oidc.Client, connector types.OIDCConnector, code string) (jose.Claims, error) {
- cc := map[string]interface{}{
- "email_verified": true,
- "groups": []string{"everyone", "idp-admin", "idp-dev"},
- "email": "superuser@example.com",
- "sub": "00001234abcd",
- "exp": 1652091713.0,
- }
- return cc, nil
- }
-
- resp, err := s.oas.ValidateOIDCAuthCallback(ctx, values)
- if tc.wantValidateErr != nil {
- require.ErrorIs(t, err, tc.wantValidateErr)
- return
- }
-
- require.NoError(t, err)
- require.NotNil(t, resp)
- require.Equal(t, &OIDCAuthResponse{
- Username: "superuser@example.com",
- Identity: types.ExternalIdentity{
- ConnectorID: "-sso-test-okta",
- Username: "superuser@example.com",
- },
- Req: OIDCAuthRequestFromProto(request),
- }, resp)
-
- diagCtx := SSODiagContext{}
-
- resp, err = s.oas.validateOIDCAuthCallback(ctx, &diagCtx, values)
- require.NoError(t, err)
- require.NotNil(t, resp)
- require.Equal(t, &OIDCAuthResponse{
- Username: "superuser@example.com",
- Identity: types.ExternalIdentity{
- ConnectorID: "-sso-test-okta",
- Username: "superuser@example.com",
- },
- Req: OIDCAuthRequestFromProto(request),
- }, resp)
- require.Equal(t, types.SSODiagnosticInfo{
- TestFlow: true,
- Success: true,
- CreateUserParams: &types.CreateUserParams{
- ConnectorName: "-sso-test-okta",
- Username: "superuser@example.com",
- Logins: nil,
- KubeGroups: nil,
- KubeUsers: nil,
- Roles: []string{"access"},
- Traits: map[string][]string{
- "email": {"superuser@example.com"},
- "groups": {"everyone", "idp-admin", "idp-dev"},
- "sub": {"00001234abcd"},
- },
- SessionTTL: 600000000000,
- },
- OIDCClaimsToRoles: []types.ClaimMapping{
- {
- Claim: "groups",
- Value: "idp-admin",
- Roles: []string{"access"},
- },
- },
- OIDCClaimsToRolesWarnings: nil,
- OIDCClaims: map[string]interface{}{
- "email_verified": true,
- "groups": []string{"everyone", "idp-admin", "idp-dev"},
- "email": "superuser@example.com",
- "sub": "00001234abcd",
- "exp": 1652091713.0,
- },
- OIDCIdentity: &types.OIDCIdentity{
- ID: "00001234abcd",
- Name: "",
- Email: "superuser@example.com",
- ExpiresAt: diagCtx.Info.OIDCIdentity.ExpiresAt,
- },
- OIDCTraitsFromClaims: map[string][]string{
- "email": {"superuser@example.com"},
- "groups": {"everyone", "idp-admin", "idp-dev"},
- "sub": {"00001234abcd"},
- },
- OIDCConnectorTraitMapping: []types.TraitMapping{
- {
- Trait: "groups",
- Value: "idp-admin",
- Roles: []string{"access"},
- },
- },
- }, diagCtx.Info)
- })
- }
-}
-
-// TestPingProvider confirms that the client_secret_post auth
-// method was set for a oauthclient.
-func TestPingProvider(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- s := setUpSuite(t)
-
- // Create configurable IdP to use in tests.
- idp := newFakeIDP(t, false /* tls */)
-
- // Create and upsert oidc connector into identity
- connector, err := types.NewOIDCConnector("test-connector", types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- Provider: teleport.Ping,
- ClaimsToRoles: []types.ClaimMapping{{Claim: "roles", Value: "teleport-user", Roles: []string{"dictator"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- })
- require.NoError(t, err)
- err = s.a.UpsertOIDCConnector(ctx, connector)
- require.NoError(t, err)
-
- for _, req := range []types.OIDCAuthRequest{
- {
- ConnectorID: "test-connector",
- }, {
- SSOTestFlow: true,
- ConnectorID: "test-connector",
- ConnectorSpec: &types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- Provider: teleport.Ping,
- ClaimsToRoles: []types.ClaimMapping{{Claim: "roles", Value: "teleport-user", Roles: []string{"dictator"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- },
- },
- } {
- t.Run(fmt.Sprintf("Test SSOFlow: %v", req.SSOTestFlow), func(t *testing.T) {
- oidcConnector, oidcClient, err := s.oas.getOIDCConnectorAndClient(ctx, req)
- require.NoError(t, err)
-
- oac, err := getOAuthClient(oidcClient, oidcConnector)
- require.NoError(t, err)
-
- // authMethod should be client secret post now
- require.Equal(t, oauth2.AuthMethodClientSecretPost, oac.GetAuthMethod())
- })
- }
-}
-
-func TestOIDCClientProviderSync(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- // Create configurable IdP to use in tests.
- idp := newFakeIDP(t, false /* tls */)
-
- // Create OIDC connector and client.
- connector, err := types.NewOIDCConnector("test-connector", types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- Provider: teleport.Ping,
- ClaimsToRoles: []types.ClaimMapping{{Claim: "roles", Value: "teleport-user", Roles: []string{"dictator"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- })
- require.NoError(t, err)
-
- client, err := newOIDCClient(ctx, connector, "proxy.example.com")
- require.NoError(t, err)
-
- // first sync should complete successfully
- require.NoError(t, client.waitFirstSync(100*time.Millisecond))
- require.NoError(t, client.syncCtx.Err())
-
- // Create OIDC client with a canceled ctx
- canceledCtx, cancel := context.WithCancel(ctx)
- cancel()
-
- client, err = newOIDCClient(canceledCtx, connector, "proxy.example.com")
- require.NoError(t, err)
-
- // provider sync goroutine should end and first sync should fail
- require.ErrorIs(t, client.syncCtx.Err(), context.Canceled)
- err = client.waitFirstSync(100 * time.Millisecond)
- require.Error(t, err)
- require.ErrorIs(t, err, context.Canceled)
-
- // Create OIDC connector and client without an issuer URL for provider syncing
- connectorNoIssuer, err := types.NewOIDCConnector("test-connector", types.OIDCConnectorSpecV3{
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- Provider: teleport.Ping,
- ClaimsToRoles: []types.ClaimMapping{{Claim: "roles", Value: "teleport-user", Roles: []string{"dictator"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- })
- require.NoError(t, err)
-
- timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
- defer cancel()
- client, err = newOIDCClient(timeoutCtx, connectorNoIssuer, "proxy.example.com")
- require.NoError(t, err)
-
- // first sync should fail after the given timeout and cancel the sync goroutine.
- err = client.waitFirstSync(100 * time.Millisecond)
- require.Error(t, err)
- require.True(t, trace.IsConnectionProblem(err))
- require.ErrorIs(t, client.syncCtx.Err(), context.Canceled)
-}
-
-func TestOIDCClientCache(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- s := setUpSuite(t)
-
- // Create configurable IdP to use in tests.
- idp := newFakeIDP(t, false /* tls */)
- connectorSpec := types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "00000000000000000000000000000000",
- ClientSecret: "0000000000000000000000000000000000000000000000000000000000000000",
- Provider: teleport.Ping,
- ClaimsToRoles: []types.ClaimMapping{{Claim: "roles", Value: "teleport-user", Roles: []string{"dictator"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- }
- connector, err := types.NewOIDCConnector("test-connector", connectorSpec)
- require.NoError(t, err)
-
- // Create and cache a new oidc client
- client, err := s.oas.getCachedOIDCClient(ctx, connector, "proxy.example.com")
- require.NoError(t, err)
-
- // The next call should return the same client (compare memory address)
- cachedClient, err := s.oas.getCachedOIDCClient(ctx, connector, "proxy.example.com")
- require.NoError(t, err)
- require.True(t, client == cachedClient)
-
- // Canceling provider sync on a cached client should cause it to be replaced
- client.syncCancel()
- cachedClient, err = s.oas.getCachedOIDCClient(ctx, connector, "proxy.example.com")
- require.NoError(t, err)
- require.False(t, client == cachedClient)
-
- // Certain changes to the connector should cause the cached client to be refreshed
- originalClient := cachedClient
- for _, tc := range []struct {
- desc string
- mutateConnector func(types.OIDCConnector)
- expectNoRefresh bool
- }{
- {
- desc: "IssuerURL",
- mutateConnector: func(conn types.OIDCConnector) {
- conn.SetIssuerURL(newFakeIDP(t, false /* tls */).s.URL)
- },
- }, {
- desc: "ClientID",
- mutateConnector: func(conn types.OIDCConnector) {
- conn.SetClientID("11111111111111111111111111111111")
- },
- }, {
- desc: "ClientSecret",
- mutateConnector: func(conn types.OIDCConnector) {
- conn.SetClientSecret("1111111111111111111111111111111111111111111111111111111111111111")
- },
- }, {
- desc: "RedirectURLs",
- mutateConnector: func(conn types.OIDCConnector) {
- conn.SetRedirectURLs([]string{"https://other.example.com/v1/webapi/oidc/callback"})
- },
- }, {
- desc: "Scope",
- mutateConnector: func(conn types.OIDCConnector) {
- conn.SetScope([]string{"groups"})
- },
- }, {
- desc: "Prompt - no refresh",
- mutateConnector: func(conn types.OIDCConnector) {
- conn.SetPrompt("none")
- },
- expectNoRefresh: true,
- },
- } {
- t.Run(tc.desc, func(t *testing.T) {
- newConnector, err := types.NewOIDCConnector("test-connector", connectorSpec)
- require.NoError(t, err)
- tc.mutateConnector(newConnector)
-
- client, err = s.oas.getCachedOIDCClient(ctx, newConnector, "proxy.example.com")
- require.NoError(t, err)
- require.True(t, (client == originalClient) == tc.expectNoRefresh)
-
- // reset cached client to the original client for remaining tests
- originalClient, err = s.oas.getCachedOIDCClient(ctx, connector, "proxy.example.com")
- require.NoError(t, err)
- })
- }
-}
-
-// fakeIDP is a configurable OIDC IdP that can be used to mock responses in
-// tests. At the moment it creates an HTTP server and only responds to the
-// "/.well-known/openid-configuration" endpoint.
-type fakeIDP struct {
- s *httptest.Server
-}
-
-// newFakeIDP creates a new instance of a configurable IdP.
-func newFakeIDP(t *testing.T, tls bool) *fakeIDP {
- var s fakeIDP
-
- mux := http.NewServeMux()
- mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) {
- http.Error(w, "unauthorized", http.StatusUnauthorized)
- })
- mux.HandleFunc("/", s.configurationHandler)
-
- if tls {
- s.s = httptest.NewTLSServer(mux)
- } else {
- s.s = httptest.NewServer(mux)
- }
-
- t.Cleanup(s.s.Close)
- return &s
-}
-
-// configurationHandler returns OpenID configuration.
-func (s *fakeIDP) configurationHandler(w http.ResponseWriter, r *http.Request) {
- resp := fmt.Sprintf(`
-{
- "issuer": "%v",
- "authorization_endpoint": "%v",
- "token_endpoint": "%v",
- "jwks_uri": "%v",
- "userinfo_endpoint": "%v/userinfo",
- "subject_types_supported": ["public"],
- "id_token_signing_alg_values_supported": ["HS256", "RS256"]
-}`, s.s.URL, s.s.URL, s.s.URL, s.s.URL, s.s.URL)
-
- w.Header().Set("Content-Type", "application/json")
- fmt.Fprintln(w, resp)
-}
-
-func TestOIDCGoogle(t *testing.T) {
- t.Parallel()
-
- directGroups := map[string][]string{
- "alice@foo.example": {"group1@foo.example", "group2@sub.foo.example", "group3@bar.example"},
- "bob@foo.example": {"group1@foo.example"},
- "carlos@bar.example": {"group1@foo.example", "group2@sub.foo.example", "group3@bar.example"},
- }
-
- // group2@sub.foo.example is in group3@bar.example and group3@bar.example is in group4@bar.example
- strictDirectGroups := map[string][]string{
- "alice@foo.example": {"group1@foo.example", "group2@sub.foo.example"},
- "bob@foo.example": {"group1@foo.example"},
- "carlos@bar.example": {"group1@foo.example", "group2@sub.foo.example"},
- }
- directIndirectGroups := map[string][]string{
- "alice@foo.example": {"group3@bar.example"},
- "bob@foo.example": {},
- "carlos@bar.example": {"group3@bar.example"},
- }
- indirectGroups := map[string][]string{
- "alice@foo.example": {"group4@bar.example"},
- "bob@foo.example": {},
- "carlos@bar.example": {"group4@bar.example"},
- }
-
- mux := http.NewServeMux()
- mux.HandleFunc("/admin/directory/v1/groups", func(rw http.ResponseWriter, r *http.Request) {
- require.Equal(t, "GET", r.Method)
-
- email := r.URL.Query().Get("userKey")
- require.NotEmpty(t, email)
- require.Contains(t, directGroups, email)
-
- domain := r.URL.Query().Get("domain")
-
- resp := &directory.Groups{}
- for _, groupEmail := range directGroups[email] {
- if domain == "" || strings.HasSuffix(groupEmail, "@"+domain) {
- resp.Groups = append(resp.Groups, &directory.Group{Email: groupEmail})
- }
- }
-
- require.NoError(t, json.NewEncoder(rw).Encode(resp))
- })
- mux.HandleFunc("/v1/groups/-/memberships:searchTransitiveGroups", func(rw http.ResponseWriter, r *http.Request) {
- require.Equal(t, "GET", r.Method)
- q := r.URL.Query().Get("query")
-
- // hacky solution but the query parameter of searchTransitiveGroups is also pretty hacky
- prefix := "member_key_id == '"
- suffix := "' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels"
- require.True(t, strings.HasPrefix(q, prefix))
- require.True(t, strings.HasSuffix(q, suffix))
- email := strings.TrimSuffix(strings.TrimPrefix(q, prefix), suffix)
- require.NotEmpty(t, email)
- require.Contains(t, directGroups, email)
-
- resp := &cloudidentity.SearchTransitiveGroupsResponse{}
-
- for relationType, groupEmails := range map[string][]string{
- "DIRECT": strictDirectGroups[email],
- "DIRECT_AND_INDIRECT": directIndirectGroups[email],
- "INDIRECT": indirectGroups[email],
- } {
- for _, groupEmail := range groupEmails {
- resp.Memberships = append(resp.Memberships, &cloudidentity.GroupRelation{
- GroupKey: &cloudidentity.EntityKey{
- Id: groupEmail,
- },
- Labels: map[string]string{
- "cloudidentity.googleapis.com/groups.discussion_forum": "",
- },
- RelationType: relationType,
- })
- }
- }
-
- require.NoError(t, json.NewEncoder(rw).Encode(resp))
- })
-
- ts := httptest.NewServer(mux)
- t.Cleanup(ts.Close)
- testOptions := []option.ClientOption{option.WithEndpoint(ts.URL), option.WithoutAuthentication()}
-
- ctx := context.Background()
-
- for _, testCase := range []struct {
- email, domain string
- transitive, direct, filtered []string
- }{
- {
- "alice@foo.example", "foo.example",
- []string{"group1@foo.example", "group2@sub.foo.example", "group3@bar.example", "group4@bar.example"},
- []string{"group1@foo.example", "group2@sub.foo.example", "group3@bar.example"},
- []string{"group1@foo.example"},
- },
- {
- "bob@foo.example", "foo.example",
- []string{"group1@foo.example"},
- []string{"group1@foo.example"},
- []string{"group1@foo.example"},
- },
- {
- "carlos@bar.example", "bar.example",
- []string{"group1@foo.example", "group2@sub.foo.example", "group3@bar.example", "group4@bar.example"},
- []string{"group1@foo.example", "group2@sub.foo.example", "group3@bar.example"},
- []string{"group3@bar.example"},
- },
- } {
- // transitive groups
- groups, err := groupsFromGoogleCloudIdentity(ctx, testCase.email, testOptions...)
- require.NoError(t, err)
- require.ElementsMatch(t, testCase.transitive, groups)
-
- // direct groups, unfiltered
- groups, err = groupsFromGoogleDirectory(ctx, testCase.email, "", testOptions...)
- require.NoError(t, err)
- require.ElementsMatch(t, testCase.direct, groups)
-
- // direct groups, filtered by domain
- groups, err = groupsFromGoogleDirectory(ctx, testCase.email, testCase.domain, testOptions...)
- require.NoError(t, err)
- require.ElementsMatch(t, testCase.filtered, groups)
- }
-}
-
-func TestEmailVerifiedClaim(t *testing.T) {
- tests := []struct {
- claims map[string]interface{}
- expectedError string
- }{
- {
- claims: map[string]interface{}{
- "email_verified": "true",
- },
- expectedError: "",
- },
- {
- claims: map[string]interface{}{
- "email_verified": "false",
- },
- expectedError: "email not verified by OIDC provider",
- },
- {
- claims: map[string]interface{}{
- "email_verified": false,
- },
- expectedError: "email not verified by OIDC provider",
- },
- {
- claims: map[string]interface{}{
- "email_verified": true,
- },
- expectedError: "",
- },
- {
- claims: map[string]interface{}{
- "email_verified": "random_value",
- },
- expectedError: "unable to parse oidc claim: \"email_verified\", must be either 'true' or 'false', got 'random_value'",
- },
- }
-
- for _, test := range tests {
- err := checkEmailVerifiedClaim(test.claims)
- if test.expectedError == "" {
- require.NoError(t, err)
- } else {
- require.ErrorContains(t, err, test.expectedError)
- }
- }
-}
-
-// TestUsernameClaim ensures that the `username_claim` field in an OIDC config is handled correctly.
-func TestUsernameClaim(t *testing.T) {
- ctx := context.Background()
- s := setUpSuite(t)
- idp := newFakeIDP(t, false)
-
- diagCtx := SSODiagContext{}
-
- // Create role that will be mapped to the user.
- role, err := types.NewRole("access", types.RoleSpecV5{
- Allow: types.RoleConditions{},
- })
- require.NoError(t, err)
- err = s.a.CreateRole(ctx, role)
- require.NoError(t, err)
-
- // Create claims with "preferred_username" field.
- claims := map[string]interface{}{
- "email_verified": true,
- "groups": []string{"everyone"},
- "email": "test-user@example.com",
- "sub": "00001234abcd",
- "exp": 1652091713.0,
- "preferred_username": "Teleport_TestUser",
- }
-
- // Create identity from the claims.
- ident, err := oidc.IdentityFromClaims(claims)
- require.NoError(t, err)
-
- tests := []struct {
- desc string
- spec types.OIDCConnectorSpecV3
- expectedUsername string
- expectedError string
- }{
- {
- desc: "username_claim specified with correct claim",
- spec: types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "000",
- ClientSecret: "0000",
- ClaimsToRoles: []types.ClaimMapping{{Claim: "groups", Value: "everyone", Roles: []string{"access"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- UsernameClaim: "preferred_username",
- },
- expectedUsername: "Teleport_TestUser",
- },
- {
- desc: "username_claim specified with incorrect claim",
- spec: types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "000",
- ClientSecret: "0000",
- ClaimsToRoles: []types.ClaimMapping{{Claim: "groups", Value: "everyone", Roles: []string{"access"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- UsernameClaim: "prefred_usrnam",
- },
- expectedError: "The configured username_claim of \"prefred_usrnam\" was not received from the IdP. Please update the username_claim in connector \"okta-oidc\".",
- },
- {
- desc: "no username_claim specified, default to using email",
- spec: types.OIDCConnectorSpecV3{
- IssuerURL: idp.s.URL,
- ClientID: "000",
- ClientSecret: "0000",
- ClaimsToRoles: []types.ClaimMapping{{Claim: "groups", Value: "everyone", Roles: []string{"access"}}},
- RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
- },
- expectedUsername: "test-user@example.com",
- },
- }
-
- for _, tc := range tests {
- t.Run(tc.desc, func(t *testing.T) {
- // Create OIDC connector with UsernameClaim specified.
- connector, err := types.NewOIDCConnector("okta-oidc", tc.spec)
- require.NoError(t, err)
-
- // Create OIDC request.
- oidcRequest := types.OIDCAuthRequest{
- ConnectorID: "okta-oidc",
- Type: constants.OIDC,
- CertTTL: defaults.OIDCAuthRequestTTL,
- SSOTestFlow: true,
- ConnectorSpec: &tc.spec,
- }
- request, err := s.a.CreateOIDCAuthRequest(ctx, oidcRequest)
- require.NoError(t, err)
-
- // Generate the userCreateParams for the OIDC user.
- createUserParams, err := s.oas.calculateOIDCUser(&diagCtx, connector, claims, ident, request)
- if tc.expectedError != "" {
- require.ErrorContains(t, err, tc.expectedError)
- } else {
- require.NoError(t, err)
- require.Equal(t, tc.expectedUsername, createUserParams.Username)
- }
- })
- }
-}
-
-func TestValidateACRValues(t *testing.T) {
- tests := []struct {
- comment string
- inIDToken string
- inACRValue string
- inACRProvider string
- outIsValid require.ErrorAssertionFunc
- }{
- {
- "0 - default, acr values match",
- `
-{
- "acr": "foo",
- "aud": "00000000-0000-0000-0000-000000000000",
- "exp": 1111111111
-}
- `,
- "foo",
- "",
- require.NoError,
- },
- {
- "1 - default, acr values do not match",
- `
-{
- "acr": "foo",
- "aud": "00000000-0000-0000-0000-000000000000",
- "exp": 1111111111
-}
- `,
- "bar",
- "",
- require.Error,
- },
- {
- "2 - netiq, acr values match",
- `
-{
- "acr": {
- "values": [
- "foo/bar/baz"
- ]
- },
- "aud": "00000000-0000-0000-0000-000000000000",
- "exp": 1111111111
-}
- `,
- "foo/bar/baz",
- "netiq",
- require.NoError,
- },
- {
- "3 - netiq, invalid format",
- `
-{
- "acr": {
- "values": "foo/bar/baz"
- },
- "aud": "00000000-0000-0000-0000-000000000000",
- "exp": 1111111111
-}
- `,
- "foo/bar/baz",
- "netiq",
- require.Error,
- },
- {
- "4 - netiq, invalid value",
- `
-{
- "acr": {
- "values": [
- "foo/bar/baz/qux"
- ]
- },
- "aud": "00000000-0000-0000-0000-000000000000",
- "exp": 1111111111
-}
- `,
- "foo/bar/baz",
- "netiq",
- require.Error,
- },
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.comment, func(t *testing.T) {
- t.Parallel()
- var claims jose.Claims
- err := json.Unmarshal([]byte(tt.inIDToken), &claims)
- require.NoError(t, err)
-
- err = validateACRValues(tt.inACRValue, tt.inACRProvider, claims)
- tt.outIsValid(t, err)
- })
- }
-}
diff --git a/lib/auth/saml.go b/lib/auth/saml.go
index a8bfbf2b3f4b7..3cf652a3e24a3 100644
--- a/lib/auth/saml.go
+++ b/lib/auth/saml.go
@@ -17,31 +17,16 @@ limitations under the License.
package auth
import (
- "bytes"
- "compress/flate"
"context"
- "encoding/base64"
"encoding/json"
"fmt"
- "io"
- "sync"
- "github.com/beevik/etree"
- "github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
- saml2 "github.com/russellhaering/gosaml2"
- "github.com/gravitational/teleport"
- "github.com/gravitational/teleport/api/constants"
- apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
- "github.com/gravitational/teleport/api/utils/keys"
- "github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
- "github.com/gravitational/teleport/lib/services/local"
- "github.com/gravitational/teleport/lib/utils"
)
// ErrSAMLRequiresEnterprise is the error returned by the SAML methods when not
@@ -129,307 +114,6 @@ func (a *Server) ValidateSAMLResponse(ctx context.Context, re string, connectorI
return resp, trace.Wrap(err)
}
-// SAMLAuthService implements the logic of the SAML connector, allowing SSO
-// logins using the SAML protocol.
-//
-// SAMLAuthService implements the SAMLService interface.
-type SAMLAuthService struct {
- auth *Server
- emitter apievents.Emitter
- assertionReplayService *local.AssertionReplayService
- samlProviders map[string]*samlProvider
- lock sync.Mutex
-}
-
-type SAMLAuthServiceConfig struct {
- Auth *Server
- Emitter apievents.Emitter
- AssertionReplayService *local.AssertionReplayService
-}
-
-// NewSAMLAuthService returns a SAMLAuthService configured to use the
-// services given in the config.
-func NewSAMLAuthService(cfg *SAMLAuthServiceConfig) *SAMLAuthService {
- return &SAMLAuthService{
- auth: cfg.Auth,
- emitter: cfg.Emitter,
- assertionReplayService: cfg.AssertionReplayService,
-
- samlProviders: make(map[string]*samlProvider),
- }
-}
-
-// samlProvider is internal structure that stores SAML client and its config
-type samlProvider struct {
- provider *saml2.SAMLServiceProvider
- connector types.SAMLConnector
-}
-
-// ErrSAMLNoRoles results from not mapping any roles from SAML claims.
-var ErrSAMLNoRoles = trace.AccessDenied("No roles mapped from claims. The mappings may contain typos.")
-
-func (sas *SAMLAuthService) CreateSAMLAuthRequest(ctx context.Context, req types.SAMLAuthRequest) (*types.SAMLAuthRequest, error) {
- connector, provider, err := sas.getSAMLConnectorAndProvider(ctx, req)
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- doc, err := provider.BuildAuthRequestDocument()
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- attr := doc.Root().SelectAttr("ID")
- if attr == nil || attr.Value == "" {
- return nil, trace.BadParameter("missing auth request ID")
- }
-
- req.ID = attr.Value
-
- // Workaround for Ping: Ping expects `SigAlg` and `Signature` query
- // parameters when "Enforce Signed Authn Request" is enabled, but gosaml2
- // only provides these parameters when binding == BindingHttpRedirect.
- // Luckily, BuildAuthURLRedirect sets this and is otherwise identical to
- // the standard BuildAuthURLFromDocument.
- if connector.GetProvider() == teleport.Ping {
- req.RedirectURL, err = provider.BuildAuthURLRedirect("", doc)
- } else {
- req.RedirectURL, err = provider.BuildAuthURLFromDocument("", doc)
- }
-
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- err = sas.auth.Services.CreateSAMLAuthRequest(ctx, req, defaults.SAMLAuthRequestTTL)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- return &req, nil
-}
-
-func (sas *SAMLAuthService) getSAMLConnectorAndProviderByID(ctx context.Context, connectorID string) (types.SAMLConnector, *saml2.SAMLServiceProvider, error) {
- connector, err := sas.auth.Identity.GetSAMLConnector(ctx, connectorID, true)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
- provider, err := sas.getSAMLProvider(connector)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- return connector, provider, nil
-}
-
-func (sas *SAMLAuthService) getSAMLConnectorAndProvider(ctx context.Context, req types.SAMLAuthRequest) (types.SAMLConnector, *saml2.SAMLServiceProvider, error) {
- if req.SSOTestFlow {
- if req.ConnectorSpec == nil {
- return nil, nil, trace.BadParameter("ConnectorSpec cannot be nil when SSOTestFlow is true")
- }
-
- if req.ConnectorID == "" {
- return nil, nil, trace.BadParameter("ConnectorID cannot be empty")
- }
-
- // stateless test flow
- connector, err := types.NewSAMLConnector(req.ConnectorID, *req.ConnectorSpec)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- // validate, set defaults for connector
- err = services.ValidateSAMLConnector(connector, sas.auth)
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- // we don't want to cache the provider. construct it directly instead of using sas.getSAMLProvider()
- provider, err := services.GetSAMLServiceProvider(connector, sas.auth.GetClock())
- if err != nil {
- return nil, nil, trace.Wrap(err)
- }
-
- return connector, provider, nil
- }
-
- // regular execution flow
- return sas.getSAMLConnectorAndProviderByID(ctx, req.ConnectorID)
-}
-
-func (sas *SAMLAuthService) getSAMLProvider(conn types.SAMLConnector) (*saml2.SAMLServiceProvider, error) {
- sas.lock.Lock()
- defer sas.lock.Unlock()
-
- providerPack, ok := sas.samlProviders[conn.GetName()]
- if ok && cmp.Equal(providerPack.connector, conn) {
- return providerPack.provider, nil
- }
- delete(sas.samlProviders, conn.GetName())
-
- serviceProvider, err := services.GetSAMLServiceProvider(conn, sas.auth.GetClock())
- if err != nil {
- return nil, trace.Wrap(err)
- }
-
- sas.samlProviders[conn.GetName()] = &samlProvider{connector: conn, provider: serviceProvider}
-
- return serviceProvider, nil
-}
-
-func (sas *SAMLAuthService) calculateSAMLUser(diagCtx *SSODiagContext, connector types.SAMLConnector, assertionInfo saml2.AssertionInfo, request *types.SAMLAuthRequest) (*CreateUserParams, error) {
- p := CreateUserParams{
- ConnectorName: connector.GetName(),
- Username: assertionInfo.NameID,
- }
-
- p.Traits = services.SAMLAssertionsToTraits(assertionInfo)
-
- diagCtx.Info.SAMLTraitsFromAssertions = p.Traits
- diagCtx.Info.SAMLConnectorTraitMapping = connector.GetTraitMappings()
-
- var warnings []string
- warnings, p.Roles = services.TraitsToRoles(connector.GetTraitMappings(), p.Traits)
- if len(p.Roles) == 0 {
- if len(warnings) != 0 {
- log.WithField("connector", connector).Warnf("No roles mapped from claims. Warnings: %q", warnings)
- diagCtx.Info.SAMLAttributesToRolesWarnings = &types.SSOWarnings{
- Message: "No roles mapped for the user",
- Warnings: warnings,
- }
- } else {
- log.WithField("connector", connector).Warnf("No roles mapped from claims.")
- diagCtx.Info.SAMLAttributesToRolesWarnings = &types.SSOWarnings{
- Message: "No roles mapped for the user. The mappings may contain typos.",
- }
- }
- return nil, trace.Wrap(ErrSAMLNoRoles)
- }
-
- // Pick smaller for role: session TTL from role or requested TTL.
- roles, err := services.FetchRoles(p.Roles, sas.auth, p.Traits)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- roleTTL := roles.AdjustSessionTTL(apidefaults.MaxCertDuration)
-
- if request != nil {
- p.SessionTTL = utils.MinTTL(roleTTL, request.CertTTL)
- } else {
- p.SessionTTL = roleTTL
- }
-
- return &p, nil
-}
-
-func (sas *SAMLAuthService) createSAMLUser(p *CreateUserParams, dryRun bool) (types.User, error) {
- expires := sas.auth.GetClock().Now().UTC().Add(p.SessionTTL)
-
- log.Debugf("Generating dynamic SAML identity %v/%v with roles: %v. Dry run: %v.", p.ConnectorName, p.Username, p.Roles, dryRun)
-
- user := &types.UserV2{
- Kind: types.KindUser,
- Version: types.V2,
- Metadata: types.Metadata{
- Name: p.Username,
- Namespace: apidefaults.Namespace,
- Expires: &expires,
- },
- Spec: types.UserSpecV2{
- Roles: p.Roles,
- Traits: p.Traits,
- SAMLIdentities: []types.ExternalIdentity{
- {
- ConnectorID: p.ConnectorName,
- Username: p.Username,
- },
- },
- CreatedBy: types.CreatedBy{
- User: types.UserRef{
- Name: teleport.UserSystem,
- },
- Time: sas.auth.GetClock().Now().UTC(),
- Connector: &types.ConnectorRef{
- Type: constants.SAML,
- ID: p.ConnectorName,
- Identity: p.Username,
- },
- },
- },
- }
-
- if dryRun {
- return user, nil
- }
-
- // Get the user to check if it already exists or not.
- existingUser, err := sas.auth.Services.GetUser(p.Username, false)
- if err != nil && !trace.IsNotFound(err) {
- return nil, trace.Wrap(err)
- }
-
- ctx := context.TODO()
-
- // Overwrite exisiting user if it was created from an external identity provider.
- if existingUser != nil {
- connectorRef := existingUser.GetCreatedBy().Connector
-
- // If the exisiting user is a local user, fail and advise how to fix the problem.
- if connectorRef == nil {
- return nil, trace.AlreadyExists("local user with name %q already exists. Either change "+
- "NameID in assertion or remove local user and try again.", existingUser.GetName())
- }
-
- log.Debugf("Overwriting existing user %q created with %v connector %v.",
- existingUser.GetName(), connectorRef.Type, connectorRef.ID)
-
- if err := sas.auth.UpdateUser(ctx, user); err != nil {
- return nil, trace.Wrap(err)
- }
- } else {
- if err := sas.auth.CreateUser(ctx, user); err != nil {
- return nil, trace.Wrap(err)
- }
- }
-
- return user, nil
-}
-
-func ParseSAMLInResponseTo(response string) (string, error) {
- raw, _ := base64.StdEncoding.DecodeString(response)
-
- doc := etree.NewDocument()
- err := doc.ReadFromBytes(raw)
- if err != nil {
- // Attempt to inflate the response in case it happens to be compressed (as with one case at saml.oktadev.com)
- buf, err := io.ReadAll(flate.NewReader(bytes.NewReader(raw)))
- if err != nil {
- return "", trace.Wrap(err)
- }
-
- doc = etree.NewDocument()
- err = doc.ReadFromBytes(buf)
- if err != nil {
- return "", trace.Wrap(err)
- }
- }
-
- if doc.Root() == nil {
- return "", trace.BadParameter("unable to parse response")
- }
-
- // Try to find the InResponseTo attribute in the SAML response. If we can't find this, return
- // a predictable error message so the caller may choose interpret it as an IdP-initiated payload.
- el := doc.Root()
- responseTo := el.SelectAttr("InResponseTo")
- if responseTo == nil {
- return "", trace.NotFound("missing InResponseTo attribute")
- }
- if responseTo.Value == "" {
- return "", trace.BadParameter("InResponseTo can not be empty")
- }
- return responseTo.Value, nil
-}
-
// SAMLAuthResponse is returned when auth server validated callback parameters
// returned from SAML identity provider
type SAMLAuthResponse struct {
@@ -494,260 +178,3 @@ type SAMLAuthRawResponse struct {
// TLSCert is TLS certificate authority certificate
TLSCert []byte `json:"tls_cert,omitempty"`
}
-
-// SAMLAuthRequestFromProto converts the types.SAMLAuthRequest to SAMLAuthRequestData.
-func SAMLAuthRequestFromProto(req *types.SAMLAuthRequest) SAMLAuthRequest {
- return SAMLAuthRequest{
- ID: req.ID,
- PublicKey: req.PublicKey,
- CSRFToken: req.CSRFToken,
- CreateWebSession: req.CreateWebSession,
- ClientRedirectURL: req.ClientRedirectURL,
- }
-}
-
-// ValidateSAMLResponse consumes attribute statements from SAML identity provider
-func (sas *SAMLAuthService) ValidateSAMLResponse(ctx context.Context, samlResponse string, connectorID string) (*SAMLAuthResponse, error) {
- event := &apievents.UserLogin{
- Metadata: apievents.Metadata{
- Type: events.UserLoginEvent,
- },
- Method: events.LoginMethodSAML,
- }
-
- diagCtx := NewSSODiagContext(types.KindSAML, sas.auth)
-
- auth, err := sas.validateSAMLResponse(ctx, diagCtx, samlResponse, connectorID)
- diagCtx.Info.Error = trace.UserMessage(err)
-
- diagCtx.WriteToBackend(ctx)
-
- attributeStatements := diagCtx.Info.SAMLAttributeStatements
- if attributeStatements != nil {
- attributes, err := apievents.EncodeMapStrings(attributeStatements)
- if err != nil {
- event.Status.UserMessage = fmt.Sprintf("Failed to encode identity attributes: %v", err.Error())
- log.WithError(err).Debug("Failed to encode identity attributes.")
- } else {
- event.IdentityAttributes = attributes
- }
- }
-
- if err != nil {
- event.Code = events.UserSSOLoginFailureCode
- if diagCtx.Info.TestFlow {
- event.Code = events.UserSSOTestFlowLoginFailureCode
- }
- event.Status.Success = false
- event.Status.Error = trace.Unwrap(err).Error()
- event.Status.UserMessage = err.Error()
- if err := sas.emitter.EmitAuditEvent(ctx, event); err != nil {
- log.WithError(err).Warn("Failed to emit SAML login failed event.")
- }
- return nil, trace.Wrap(err)
- }
-
- event.Status.Success = true
- event.User = auth.Username
- event.Code = events.UserSSOLoginCode
- if diagCtx.Info.TestFlow {
- event.Code = events.UserSSOTestFlowLoginCode
- }
-
- if err := sas.emitter.EmitAuditEvent(ctx, event); err != nil {
- log.WithError(err).Warn("Failed to emit SAML login event.")
- }
-
- return auth, nil
-}
-
-func (sas *SAMLAuthService) checkIDPInitiatedSAML(ctx context.Context, connector types.SAMLConnector, assertion *saml2.AssertionInfo) error {
- if !connector.GetAllowIDPInitiated() {
- return trace.AccessDenied("IdP initiated SAML is not allowed by the connector configuration")
- }
-
- // Not all IdP's provide these variables, replay mitigation is best effort.
- if assertion.SessionIndex != "" || assertion.SessionNotOnOrAfter == nil {
- return nil
- }
-
- err := sas.assertionReplayService.RecognizeSSOAssertion(ctx, connector.GetName(), assertion.SessionIndex, assertion.NameID, *assertion.SessionNotOnOrAfter)
- return trace.Wrap(err)
-}
-
-func (sas *SAMLAuthService) validateSAMLResponse(ctx context.Context, diagCtx *SSODiagContext, samlResponse string, connectorID string) (*SAMLAuthResponse, error) {
- idpInitiated := false
- var connector types.SAMLConnector
- var provider *saml2.SAMLServiceProvider
- var request *types.SAMLAuthRequest
- requestID, err := ParseSAMLInResponseTo(samlResponse)
- switch {
- case trace.IsNotFound(err):
- if connectorID == "" {
- return nil, trace.BadParameter("ACS URI did not include a valid SAML connector ID parameter")
- }
-
- idpInitiated = true
- connector, provider, err = sas.getSAMLConnectorAndProviderByID(ctx, connectorID)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to get SAML connector and provider")
- }
- case err != nil:
- return nil, trace.Wrap(err)
- default:
- diagCtx.RequestID = requestID
- request, err = sas.auth.Identity.GetSAMLAuthRequest(ctx, requestID)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to get SAML Auth Request")
- }
-
- diagCtx.Info.TestFlow = request.SSOTestFlow
- connector, provider, err = sas.getSAMLConnectorAndProvider(ctx, *request)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to get SAML connector and provider")
- }
- }
-
- assertionInfo, err := provider.RetrieveAssertionInfo(samlResponse)
- if err != nil {
- samlErr := trace.AccessDenied("received response with incorrect or missing attribute statements, please check the identity provider configuration to make sure that mappings for claims/attribute statements are set up correctly. , failed to retrieve SAML assertion info from response: %v.", err)
- return nil, trace.WithUserMessage(samlErr, "Failed to retrieve assertion info. This may indicate IdP configuration error.")
- }
-
- if assertionInfo != nil {
- diagCtx.Info.SAMLAssertionInfo = (*types.AssertionInfo)(assertionInfo)
- }
-
- if idpInitiated {
- if err := sas.checkIDPInitiatedSAML(ctx, connector, assertionInfo); err != nil {
- if trace.IsAccessDenied(err) {
- log.Warnf("Failed to process IdP-initiated login request. IdP-initiated login is disabled for this connector: %v.", err)
- }
-
- return nil, trace.Wrap(err)
- }
- }
-
- if assertionInfo.WarningInfo.InvalidTime {
- samlErr := trace.AccessDenied("invalid time in SAML assertion info")
- return nil, trace.WithUserMessage(samlErr, "SAML assertion info contained warning: invalid time.")
- }
-
- if assertionInfo.WarningInfo.NotInAudience {
- samlErr := trace.AccessDenied("no audience in SAML assertion info")
- return nil, trace.WithUserMessage(samlErr, "SAML: not in expected audience. Check auth connector audience field and IdP configuration for typos and other errors.")
- }
-
- log.Debugf("Obtained SAML assertions for %q.", assertionInfo.NameID)
- log.Debugf("SAML assertion warnings: %+v.", assertionInfo.WarningInfo)
-
- attributeStatements := map[string][]string{}
-
- for key, val := range assertionInfo.Values {
- var vals []string
- for _, vv := range val.Values {
- vals = append(vals, vv.Value)
- }
- log.Debugf("SAML assertion: %q: %q.", key, vals)
- attributeStatements[key] = vals
- }
-
- diagCtx.Info.SAMLAttributeStatements = attributeStatements
- diagCtx.Info.SAMLAttributesToRoles = connector.GetAttributesToRoles()
-
- if len(connector.GetAttributesToRoles()) == 0 {
- samlErr := trace.BadParameter("no attributes to roles mapping, check connector documentation")
- return nil, trace.WithUserMessage(samlErr, "Attributes-to-roles mapping is empty, SSO user will never have any roles.")
- }
-
- log.Debugf("Applying %v SAML attribute to roles mappings.", len(connector.GetAttributesToRoles()))
-
- // Calculate (figure out name, roles, traits, session TTL) of user and
- // create the user in the backend.
- params, err := sas.calculateSAMLUser(diagCtx, connector, *assertionInfo, request)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to calculate user attributes.")
- }
-
- diagCtx.Info.CreateUserParams = &types.CreateUserParams{
- ConnectorName: params.ConnectorName,
- Username: params.Username,
- KubeGroups: params.KubeGroups,
- KubeUsers: params.KubeUsers,
- Roles: params.Roles,
- Traits: params.Traits,
- SessionTTL: types.Duration(params.SessionTTL),
- }
-
- user, err := sas.createSAMLUser(params, request != nil && request.SSOTestFlow)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to create user from provided parameters.")
- }
-
- // Auth was successful, return session, certificate, etc. to caller.
- auth := &SAMLAuthResponse{
- Identity: types.ExternalIdentity{
- ConnectorID: params.ConnectorName,
- Username: params.Username,
- },
- Username: user.GetName(),
- }
-
- if request != nil {
- auth.Req = SAMLAuthRequestFromProto(request)
- } else {
- auth.Req = SAMLAuthRequest{
- CreateWebSession: true,
- }
- }
-
- // In test flow skip signing and creating web sessions.
- if request != nil && request.SSOTestFlow {
- diagCtx.Info.Success = true
- return auth, nil
- }
-
- // If the request is coming from a browser, create a web session.
- if request == nil || request.CreateWebSession {
- session, err := sas.auth.CreateWebSessionFromReq(ctx, types.NewWebSessionRequest{
- User: user.GetName(),
- Roles: user.GetRoles(),
- Traits: user.GetTraits(),
- SessionTTL: params.SessionTTL,
- LoginTime: sas.auth.GetClock().Now().UTC(),
- })
- if err != nil {
- return nil, trace.Wrap(err, "Failed to create web session.")
- }
-
- auth.Session = session
- }
-
- // If a public key was provided, sign it and return a certificate.
- if request != nil && len(request.PublicKey) != 0 {
- sshCert, tlsCert, err := sas.auth.CreateSessionCert(user, params.SessionTTL, request.PublicKey, request.Compatibility, request.RouteToCluster,
- request.KubernetesCluster, keys.AttestationStatementFromProto(request.AttestationStatement))
- if err != nil {
- return nil, trace.Wrap(err, "Failed to create session certificate.")
- }
- clusterName, err := sas.auth.GetClusterName()
- if err != nil {
- return nil, trace.Wrap(err, "Failed to obtain cluster name.")
- }
- auth.Cert = sshCert
- auth.TLSCert = tlsCert
-
- // Return the host CA for this cluster only.
- authority, err := sas.auth.GetCertAuthority(ctx, types.CertAuthID{
- Type: types.HostCA,
- DomainName: clusterName.GetClusterName(),
- }, false)
- if err != nil {
- return nil, trace.Wrap(err, "Failed to obtain cluster's host CA.")
- }
- auth.HostSigners = append(auth.HostSigners, authority)
- }
-
- diagCtx.Info.Success = true
- return auth, nil
-}
diff --git a/lib/auth/saml_test.go b/lib/auth/saml_test.go
deleted file mode 100644
index 6f384de385fb4..0000000000000
--- a/lib/auth/saml_test.go
+++ /dev/null
@@ -1,677 +0,0 @@
-/*
-Copyright 2019-2021 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"
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509/pkix"
- "encoding/base64"
- "encoding/xml"
- "net/url"
- "testing"
- "time"
-
- "github.com/jonboulle/clockwork"
- saml2 "github.com/russellhaering/gosaml2"
- samltypes "github.com/russellhaering/gosaml2/types"
- "github.com/stretchr/testify/require"
-
- apidefaults "github.com/gravitational/teleport/api/defaults"
- "github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/lib/auth/keystore"
- authority "github.com/gravitational/teleport/lib/auth/testauthority"
- "github.com/gravitational/teleport/lib/backend/memory"
- "github.com/gravitational/teleport/lib/defaults"
- "github.com/gravitational/teleport/lib/fixtures"
- "github.com/gravitational/teleport/lib/services"
- "github.com/gravitational/teleport/lib/tlsca"
- "github.com/gravitational/teleport/lib/utils"
-)
-
-func TestCreateSAMLUser(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- clock := clockwork.NewFakeClockAt(time.Now())
-
- b, err := memory.New(memory.Config{
- Context: ctx,
- Clock: clock,
- })
- require.NoError(t, err)
-
- clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
- ClusterName: "me.localhost",
- })
- require.NoError(t, err)
-
- authConfig := &InitConfig{
- ClusterName: clusterName,
- Backend: b,
- Authority: authority.New(),
- SkipPeriodicOperations: true,
- KeyStoreConfig: keystore.Config{
- Software: keystore.SoftwareConfig{
- RSAKeyPairSource: authority.New().GenerateKeyPair,
- },
- },
- }
-
- a, err := NewServer(authConfig)
- require.NoError(t, err)
-
- sas, ok := a.samlAuthService.(*SAMLAuthService)
- require.True(t, ok, "Server.samlAuthServer is not type *samlAuthServer")
-
- // Dry-run creation of SAML user.
- user, err := sas.createSAMLUser(&CreateUserParams{
- ConnectorName: "samlService",
- Username: "foo@example.com",
- Roles: []string{"admin"},
- SessionTTL: 1 * time.Minute,
- }, true)
- require.NoError(t, err)
- require.Equal(t, "foo@example.com", user.GetName())
-
- // Dry-run must not create a user.
- _, err = a.GetUser("foo@example.com", false)
- require.Error(t, err)
-
- // Create SAML user with 1 minute expiry.
- _, err = sas.createSAMLUser(&CreateUserParams{
- ConnectorName: "samlService",
- Username: "foo@example.com",
- Roles: []string{"admin"},
- SessionTTL: 1 * time.Minute,
- }, false)
- require.NoError(t, err)
-
- // Within that 1 minute period the user should still exist.
- _, err = a.GetUser("foo@example.com", false)
- require.NoError(t, err)
-
- // Advance time 2 minutes, the user should be gone.
- clock.Advance(2 * time.Minute)
- _, err = a.GetUser("foo@example.com", false)
- require.Error(t, err)
-}
-
-func TestEncryptedSAML(t *testing.T) {
- t.Parallel()
-
- // This Base64 encoded XML blob is a signed SAML response with an encrypted assertion for testing decryption and parsing.
- const EncryptedResponse = `PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDBmNTBiYTg0LWVmNjctNTQyZi1kZDgyLTI4NTU0MzVlMGM4MCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDctMTdUMDE6MDE6NDhaIiBEZXN0aW5hdGlvbj0iaHR0cDovL3NwLmV4YW1wbGUuY29tL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNGZlZTNiMDQ2Mzk1YzRlNzUxMDExZTk3Zjg5MDBiNTI3M2Q1NjY4NSI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDBmNTBiYTg0LWVmNjctNTQyZi1kZDgyLTI4NTU0MzVlMGM4MCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+TUxic3U4WFFOcW4xWE8walUzeHZIL0pPalZnPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5yVTVDUzhWQnZGVjl3RkUvOEY1NHROQTd3UVFWbG9UZkRsL0h1amJwRzJBWTNZcExtdWxzU2pOdngvc0F4a3luZ0lLTVE2dHphZkN3KzZjaGNldzh4bUNOcWdSNWNiQ09DbzB2UUJXaXhINm9jU2FKWDRTU21WeEhhU2p1clRNRkZnamFFYktiM2duV21haGpDb093TU9MZHJtWlprYkp2OWQrWTVUR0VYL2hhUmMvbXU2b05WT3dCL0xMdURDdzk3RTkxdVNUVUpvL1RPS0tVRjJYenZhVEEwMXZobzM5OTYvalpFWkRYR1ZyTGlkOTg5NDJXWWVjT3F6ZnZTWWtLemNaRGd2ZE1udlR1M20yVHpXQ1RqaEVzVDN1cjQ2OThIUmlyZUZTbnFINldhYUVMYjFFeGFiemdxNGFsRG9ma1J3ZU14YWJleVV6aUVBSWhKOGYrNVE9PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlES2pDQ0FoS2dBd0lCQWdJUUp0SkRKWlpCa2cvYWZNOGQyWkpDVGpBTkJna3Foa2lHOXcwQkFRc0ZBREJBTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGc2FHOXpkQzVzYjJOaGJHUnZiV0ZwYmpBZUZ3MHhOekExTURreE9UUXdNelphRncweU56QTFNRGN4T1RRd016WmFNRUF4RlRBVEJnTlZCQW9UREZSbGJHVndiM0owSUU5VFV6RW5NQ1VHQTFVRUF4TWVkR1ZzWlhCdmNuUXViRzlqWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF1S0ZMYWYyaUlJL3hEUittMllqNlBuVUVhK3F6cXd4c2RMVWpudW5GWmFBWEcraFptNE1sODBTQ2lCZ0lnVEhRbEp5TElrVHR1Um9INWFlTXl6MUVSVUN0aWk0WnNUcURyampVeWJ4UDRyKzRIVlg2bTM0czZod0VyOEZpZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNFBVeDBHMHdSWUpwUnl3b2o4RzBJa3BmUVRpWCtDQUM3ZHQ1d3M3WnJuR3FDTkJMR2k1YkdzYU1tcHRWYnNTRXAxVGVubnRGNTRWMWlSNDlJVjVKcURobTFTMEhta2xlb0p6S2RjKzZzUC94TmVwejlQSnp1RjlkOU51YlRMV2dCc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRFZSMFRBUUgvQkFJd0FEQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFBVlU2c05CZGo3NnNhSHdPeEdTZG5FcVFvMnRNdVIzbXNTTTRGNndGSzJVa0tlcHNEN0NZSWYvUHpOU05VcUE1SklFVVZlTXFHeWlIdUFiVTRDNjU1blQxSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbU54V3NKS2NaNWsrcTRlTXhjaTltS1JISHFzcXVXS1h6UWxVUk1ORkkrbUdhRndyS000ZG16YVIwQkVjK2lsU3hRcVV2UTc0c21zTEsremhOaWttZ2psR0M1b2I5ZzhYa2hWQWtKTUFoMnJiOW9uRE5pUmw2OGlBZ2N6UDg4bVh1dk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPg0KICA8c2FtbHA6U3RhdHVzPg0KICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4NCiAgPC9zYW1scDpTdGF0dXM+DQogIA0KPHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNyc2Etb2FlcC1tZ2YxcCIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+TjlLaHFKeWtJdGk1eVZETzdzT0VpT2lMb1VWL2p5aEdITU0wZmFTUzZnWVJ5RUZqaFR2RzNCUEpsd1RTTXpzTTFuY1pwTGVBd0FKaFVzci9mT0pCVGtQbjA3UzZqZGsxYTBMaU9EbjkrcDlCVXlidjRXYWsyWGduMnhXNytDNVQ5bGhvQ0dHRThrdHh6Q0tXL1FhWUNWV3RsMEp1TGdNYWIyWHUzL1dJdVlSWDhKbVZ2ckdPWTlOd0hpeFhFT09PbDFSUUNnbXpNaCtxUno5eFhwWGU0Sk1XRGNqQ0g2blk5V2Fxem4yQTNJVnJzS1V3bUZuTWFaM1lOM04ybmNROWo2QXRYSThoWEErMjBvRlhBQWx2c3JVK0xOek9hTFRzb1QySFZVUER0YldhQm9tS1cxdW9lc1hPZG1KNnVDUVc4Zk9BL3p0QStjL1JERTdyaTZiSFNERCs3YW5uQlNaRzVzL1lrdm5PT2wxRjRFYndLdVpMd3RWdjQza1B1dnRVeUVxTE1HRFlhNmtXczlLRjRsR0dqa0YxMXpqTmVnSTRFd09EVXhZSm14QldRNXhvVm1sOGVvK0VNdGt5NzFGeUttMFhpSGo0cWw5Qnl4dUtaVHJrTXdjQjlPSkNFKzcwM1dpdW5uZy9OSGkrV1IrTmhORlZqUm40SWQ3UTVFTEZZNFRNMklNQld6Y3R2cEd6TGVHdjF2L2RXanVodU1aVjJpeG5nMzRxNXg3VVVnODAwNVlRTnNwOVcwYVZKdDRnY0tUeFAyRnJTVGVjM0MxRktVT1JXSCtVc2ZpZ21GY09YdHFvQXgyK3dDZGs3MkVTS3Z2WFYvWEVFSmd0aTRiVldUK0dzSmc2eGRhTjBPNnlpWTg4eE5BcWZMUEcwajJTb0k1bUcxYzM0YVE9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+DQogICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+a1dWbkpzTkZZZ1JTNlMvTlFERVlsN3RTTjVsTzR6YWtqdXdxTDROMlFHbW9rdnBWRlJreFp3bjVhOXkrVDhLUGZpOWt2bEhlNktzL1I1UTJ2NkdVd0Z4V1dPYVZrOFJDMUh1blN1ZnZ5Tks1Y21hRmJSM0t3OFZZZnNPRVV2T0d6Y2pqTFFjSFFFUEZUMXJsMW0vazc3dExzbFlxV1B6WkRhUFpkU1lkd0kvdlo0OThIeXF3b0Y4U2tCcFVQcW02bnYwVGFVdU5pOWMzbkdDUTBWejE3UGFnLzN3SERWTnA3RlNsSFJlSStySkZsS0RXalF4MStDZjF6U3pjTG5Ecll1aEt3MWY3WVVTYUh0enlBL2l5MkhRMDdSZjRyNUpRaUorR2FOSUkwYk44RTZ0QXJqbjFZSGZWMDRQdWNwa0xDTEJTRndiOERXZE9wMnFEN3pvcEJvc2g2YVF6Vjh3QjhsUEFqUUd2aFhlQVdwRmsxWkVaUG5SRkkydkdkaGFHL2o5TkQvM0EvbzlvZFZpd1ZBdFd4SG9WVzZWcWtLOG5GQlVyL25IVDZTYmdsOFlUd2s4N0hRaldHUEdSaksyNEp3YXc0VjV6djN4bG9zSlFpdnc2dGwyVXk5YWROdUlyOUFCRGJuc3RmdXlQNEZaenR2Q0RTUHFPZlVVQlhya0thczNJRE1iNm9KREQ3a003QVdHcEU5K0lJT0NoVzdiRk5hb3RndHN1OTVqd1ZNQ3BVL0NkNkhPbURrMWpDYVc5RzhlQWJVdjhaUEVmN1Q5c0Z1VnZOaVBVbkFyMWV1VEl6WFNPekFtZk5jdXZpZ1BnOUlCYzAvT0pYWjBBVTgvQWxyRTZRSldwL2pDYThUNTF0YTFjbVN6SGQ4SG0zTEt5aDhsYjljUG5RR3RCeW9LcFUzZEMwQWk3OU1Lc1NhekRTalByY2l6ZUdhS0Vzd1NCTWtWRGtDWFMybGJicXBrckxvN2tMdy9TNG1OMWZCVjU5a2txd1ZlL3pKQXJNNllIckNJUURwRkRCSWtHeXE1WVl1VkQyeXk4YnJmeFNBemMyK2ZpeWQ5OVJndEtTeGZhVkROV3VJZzJnVTRQME40TnVTOCtrb0MzclF6eWovOGdWSlRpSGg5UTFEOVRJbjhjZGllTE12ZlZLT25oMHZBTnR0MDN2YnRHVkJxTTQwa3VLeUxKRWE5TWY0N2xvWk5qamUxd2VvWW1wblZScGVxVGxzUDRzVmwzd3QwYWlDZG5mdHVaVlVWb0M3Um51VDRidVRWZVgybUdNMElxR242czJwZ0xsSU5xVEFTY0F0K3FlVEhycmVTcUhFSmovZnhyM3NySEpyMHZjQ0w2enZZMWtOUi9taGcyL25Bc0hwTkc3c21ITS9oQ3gzOS9HZ01FeFpXbU5lVXV3amhhOHpWc3FRdnZ6cDM3Nk1OWC9xeERuU0JxTEorTERlRXg0SnYweGJxd0tvVE01L3BSQThDOVA4NWRqZXYwT1RKWVBEOGZzNHhabmV6NHRQTlZhcEc1RzEwWG1Wem9TYm1MNXdYamV0bXJJY0NMZ0laeEZnZ1NFSmsxUGFtdThzU2VraHBNOE1EbVZXejZrRDZxdUxyRXNqc0h2K0Rxc1REMkJIRjJMOVVuMjlaL0NvbFBNaDRuT0NRZHpjcjNUT0dEalJaMk1lanJHb2VnblVORW5MaGhwK2luWTB5L2lsODZYV2FweERoUEFLclIxS3pTRUJ5Yk5UUE81S0o4M0pBMGx0dE5JVk9YZ3FkZkQxSy9KYnM4ZHdYY3pRenZCRXBMOXlEZVN4d0wzVXR2VG80NEJLVkYxUVdwUDVvdHU5ZndpZXNkZlJLZm1zM3pteFZiQTZ4dkprK0M2MkE2YXBDcjhqL3FaUldNTUczNytnMUI4bU13eGdabkxyOE9ic2JMQjBVV3JOYWdMbldpc29ZQTVmclcxN21wbW9tc3V1cmhQQ2IrbGczbmFkK1BCRXpXaUZISnJvVkdQMFRwZ1NWZlZvSU9wYkZCS2JpWjVoUnd5YURuaFMrL1UreXNUZ1hXdE1qUk9QYjRROVZiUW8vSXd0NEpVZjZNUlVlN0FoaTNUbTUyOUR5Q1ppOFNydTBtRDF4ZHArdjlkSXF2ZEoxZXROYXZFMTlCSmRDSzN0VHhIZHk2ODh6bEo4NFJOUTlxYzI1R3pudXVFNFg3ZjBkUkQrdEoyNkJNVmh5L3RLeDRzdUpIVURzS05yUmZ4aGM4czNwU2FhUTFZS0E4eVpnQnBIN2tDY0FJZ2g5NlpEU3NuUngzdU5Zc0txakFmd0x3TkNQam02bHY1YVV1azBtYklMelBwMDhFRWFzRWNhWE1CcEZraERKTGhIS1l2WUhPdjlQYTZvWEFOVzZSOC9rSUxoT2ppbnl3RmZUZ2FDci96R20zcitjc2VoM0RxTGNjZjRteXY3Tmp5eERUSWtVeUVDdDJLYnI3S2ZPckh3T3I0L3M0ODVhVVVJVDRxYml5Lzd4U0E1NXlDaFUzV0RkaGpDQ3l2WXNSa3cvUjhVQUMyRExmZWxDVlducWdoTnE3blhNdU9zM3h4L3hpLzE5eEVYK3RxTWNqUHVsc3FQejk4WDF1UE5iZDluYXpSUnFEaXQ2R0Y2NHNlY1ZKVmVjZlpHNTJpTHpRbzZLNzVtdnU0M00rNlI5MkhDdkVWd09sRDhCRWoxYncrUWFaL3FDNVF4UjNtMkR5VHlmd20wVVVUM25yOG0yTDdaR3ozTUU2RXViTzZFWFBVR0JpRnlUeTNNUzRxVGUrbTJoWHdwd0xhTXM3SXFjRVBvMW5KeUJmN1plWm1kZVpsLzJHZ1pVNk94YmxiTmtUZUJORlRWVjhzK0Y2K2h3ZVArMzdBbEVsVWNGNTZaUk9NU0Nmemw0OFRtR1BZNHdpRmlMWGErMXZqTFN6NGdlYzhlVGZJcXczdEdaNjAvUTJGRGV6TUV2MEd0cSsxbTU3VXpqWGtLSVoyNzJBZ3VUNzZsbWUyd1B3dUVtOER2ZHE0Z21ZZlE5MVZ5M3J3aElsVVpvakRxU2R0UHJiTU4zK0JvR1Y0SVFTdUExNlN0UUtJWkg3VVNkckZBZ1dhLzVjQzUwVjIvMUVMMWZRWjJFa01LbktFczRnc2hZNU5YRTYzTmJTSjZiMnpKQll6MDdRcXVTemhycGxUeTR3dVpqcytRd082bUhDZkdEaFY4R2I1RG1HcnExMVVJNVllNVp4RXNLS0FvSlRYdWFOZlRpamFKb2l5L2lvcmFZZk00eUNHUVIyU04wdnc4Zk9LdXA3SlIrejk5TXdmSEJwV21LaHhFM3dITWpZOWpJa3Nyc3JLWnk3MnFRaGROMDZCdEo2SFdpN25PNWJUdHVQZlpmUFdPOFVEZUpKZFdLdUVUNUdpeWFxWGhOM0VwQUQvcDA5RjZkOEgrb1gyRVAzQjRZWXE2cTlSOHpJZG9kZ3cxbncxZjM3MERpL0pSSG1PbnQ5VEord05FS0x5eW42dFVodXRpOGZMY2VKYkk2dHJFb1I2S3NZWGJUeXprVXg5TGtiVkNFelVicVV1YmtrdG5WVm5rcHFJSmNFTGdpZWl0TFRTYW80ZkdOaFROVDVCWDEySWtoQWsrSEhSZlZtWjdhY0lBNDlLYkhlRElpUGxOR3A1WGxMQ1luZGxlOFZlWDF4bEw4Y0duZlB5QWtiSTRLN3RQR2h4S0YzSWFoazFtTVFGWFRhQ3FvdS9pYm9ZQ0F0R3pod3pOaUtLcXhRT2FSWFpQWkY3TlVKbE13NlR1VFhhaU9pM0dLMDZ4eXY2ZEcxTk9ya2daRmhNSS9CZHdjWVcxaHdCaElXWHBVTnpNUyt6cDVTS1BOQW5BSHE4aW16OU1JcGNQajFkYXo0cUNQd3N4dXMwYXBnMkdZQ0xYTW8zdnlTRXd3WTAwQjZzUUlrRHhLTUc4TWtvQjFpMU5vVGMwNnJxTkFOL2szZ3h4UnpBNUFpNHU4NGhpd3dxOXlMWC9oNjFGRnpySklEY3gydHl4MHdUTFd2SzNBWCtOZStQMDZvWWEyVHN4R0RkV0RQR1BFc2U0aFZlTUl6PC94ZW5jOkNpcGhlclZhbHVlPg0KICAgPC94ZW5jOkNpcGhlckRhdGE+DQo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+`
-
- // This XML blob is a sample EntityDescriptor made to satisfy the connector validator for testing.
- const EntityDescriptor = `
-
-
-
-
-
- MIIFazCCA1OgAwIBAgIUDpXWZ8npv3sWeCQbB1WCwMoDe9QwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTAyMTgyMTUyNTVaFw0yMjAyMTgyMTUyNTVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDiEvFfAwgR8rfFPXVkJiWQGisFQNpQ5oq4ng5sD/3phPBBzwx0TTn+V+XG5pBTlyVe0h9kLqZ3Dnavdk9VDC1DIrc0CSKUhP01JdV9TlC/tCek9a2IQEjEZ0pZPbU/gtXxEGyrs9JVFf0K8saMH6xB8jJwB4Eq9jB8rsWZJh4HeyX1VEdruPdwRkFjuNhBnIax//DQSZepAhtM+mtxP+cHtRzXPlXHTpYvxcP2LoXjSdCh/XEu8Ai33O4Ek14HIFmNQ63pmzmxhpcPm8ejDFchOEU67zeOz2RQNAefeHRgG1gvFIcgmVXcLM+VmC0JlzNuyMFY1XUygm1PYcFz93p4OGJBkYgKifNHPcMzTLQtPoY397WREd/kkMtvgxSDs6GQr2VwByHoo5IoQJ/OpridaDduL9NSc6YHEEXxSceMSdI+txuZvOAJJuLR1DQ5S5xjdHBj8uDsAnmX7oORVadEJ38Aj1UlM+Lk6qnmoBEGAXEfa3Fxyz0qgN9MrtutJO0S4BLqqmXgM9Kulp0B7e7gkRaAyNt/Y0+dAuzYva+uTd7Qm96EEYCTwd9LM4OghTLpDCXFm5EQI+D0zEyOGhDqwQDdx3MHJoPd6xg72ZkoiADY235D/av/ZisF7acPucLvQ41gbWphQgsRTN81lRll/Wgd4EknznXq060RQBkNbwIDAQABo1MwUTAdBgNVHQ4EFgQUzpwOh72T7DyvsvkVV9Cu4YRKBTYwHwYDVR0jBBgwFoAUzpwOh72T7DyvsvkVV9Cu4YRKBTYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEADSc0AEFgMcwArn9zvppOdMlF4GqyJa7mzeVAKHRyXiLm4TSUk8oBk8GgO9f32B5sEUVBnL5FnzEUm7hMAG5DUcMXANkHguIwoISpAZdFh1VhH+13HIOmxre/UN9a1l829g1dANvYWcoGJc4uUtj3HF5UKcfEmrUwISimW0Mpuin+jDlRiLvpvImqxWUyFazucpE8Kj4jqmFNnoOLAQbEerR61W1wC3fpifM9cW5mKLsSpk9uG5PUTWKA1W7u+8AgLxvfdbFA9HnDc93JKWeWyBLX6GSeVL6y9pOY9MRBHqnpPVEPcjbZ3ZpX1EPWbniF+WRCIpjcye0obTTjipWJli5HqwGGauyXPGmevCkG96jiy8nf18HrQ3459SuRSZ1lQD5EoF+1QBL/O1Y6P7PVuOSQev376RD56tOLu1EWxZAmfDNNmlZSmZSn+h5JRcjSh1NFfktIVkHtNPKw8FXDp8098oqrJ3MoNTQgE0vpXiho1QIxWhfaEU5y/WynZFk1PssjBULWNxbeIpOFYk3paNyEpb9cOkOE8ZHOdi7WWJSwHaDmx6qizOQXO75QMLIMxkCdENFx6wWbNMvKCxOlPfgkNcBaAsybM+K0AHwwvyzlcpVfEdaCexGtecBoGkjFRCG+f9InppaaSzmgbIJvkSOMUWEDO/JlFizzWAG8koM=
-
-
-
-
-
-
- MIIFazCCA1OgAwIBAgIUDpXWZ8npv3sWeCQbB1WCwMoDe9QwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTAyMTgyMTUyNTVaFw0yMjAyMTgyMTUyNTVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDiEvFfAwgR8rfFPXVkJiWQGisFQNpQ5oq4ng5sD/3phPBBzwx0TTn+V+XG5pBTlyVe0h9kLqZ3Dnavdk9VDC1DIrc0CSKUhP01JdV9TlC/tCek9a2IQEjEZ0pZPbU/gtXxEGyrs9JVFf0K8saMH6xB8jJwB4Eq9jB8rsWZJh4HeyX1VEdruPdwRkFjuNhBnIax//DQSZepAhtM+mtxP+cHtRzXPlXHTpYvxcP2LoXjSdCh/XEu8Ai33O4Ek14HIFmNQ63pmzmxhpcPm8ejDFchOEU67zeOz2RQNAefeHRgG1gvFIcgmVXcLM+VmC0JlzNuyMFY1XUygm1PYcFz93p4OGJBkYgKifNHPcMzTLQtPoY397WREd/kkMtvgxSDs6GQr2VwByHoo5IoQJ/OpridaDduL9NSc6YHEEXxSceMSdI+txuZvOAJJuLR1DQ5S5xjdHBj8uDsAnmX7oORVadEJ38Aj1UlM+Lk6qnmoBEGAXEfa3Fxyz0qgN9MrtutJO0S4BLqqmXgM9Kulp0B7e7gkRaAyNt/Y0+dAuzYva+uTd7Qm96EEYCTwd9LM4OghTLpDCXFm5EQI+D0zEyOGhDqwQDdx3MHJoPd6xg72ZkoiADY235D/av/ZisF7acPucLvQ41gbWphQgsRTN81lRll/Wgd4EknznXq060RQBkNbwIDAQABo1MwUTAdBgNVHQ4EFgQUzpwOh72T7DyvsvkVV9Cu4YRKBTYwHwYDVR0jBBgwFoAUzpwOh72T7DyvsvkVV9Cu4YRKBTYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEADSc0AEFgMcwArn9zvppOdMlF4GqyJa7mzeVAKHRyXiLm4TSUk8oBk8GgO9f32B5sEUVBnL5FnzEUm7hMAG5DUcMXANkHguIwoISpAZdFh1VhH+13HIOmxre/UN9a1l829g1dANvYWcoGJc4uUtj3HF5UKcfEmrUwISimW0Mpuin+jDlRiLvpvImqxWUyFazucpE8Kj4jqmFNnoOLAQbEerR61W1wC3fpifM9cW5mKLsSpk9uG5PUTWKA1W7u+8AgLxvfdbFA9HnDc93JKWeWyBLX6GSeVL6y9pOY9MRBHqnpPVEPcjbZ3ZpX1EPWbniF+WRCIpjcye0obTTjipWJli5HqwGGauyXPGmevCkG96jiy8nf18HrQ3459SuRSZ1lQD5EoF+1QBL/O1Y6P7PVuOSQev376RD56tOLu1EWxZAmfDNNmlZSmZSn+h5JRcjSh1NFfktIVkHtNPKw8FXDp8098oqrJ3MoNTQgE0vpXiho1QIxWhfaEU5y/WynZFk1PssjBULWNxbeIpOFYk3paNyEpb9cOkOE8ZHOdi7WWJSwHaDmx6qizOQXO75QMLIMxkCdENFx6wWbNMvKCxOlPfgkNcBaAsybM+K0AHwwvyzlcpVfEdaCexGtecBoGkjFRCG+f9InppaaSzmgbIJvkSOMUWEDO/JlFizzWAG8koM=
-
-
-
- urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
-
-
- `
-
- signingKeypair := &types.AsymmetricKeyPair{
- Cert: fixtures.TLSCACertPEM,
- PrivateKey: fixtures.TLSCAKeyPEM,
- }
-
- encryptionKeypair := &types.AsymmetricKeyPair{
- Cert: fixtures.EncryptionCertPEM,
- PrivateKey: fixtures.EncryptionKeyPEM,
- }
-
- connector, err := types.NewSAMLConnector("spongebob", types.SAMLConnectorSpecV2{
- Cert: signingKeypair.Cert,
- Issuer: "http://idp.example.com/metadata.php",
- SSO: "nil",
- AssertionConsumerService: "http://sp.example.com/demo1/index.php?acs",
- EntityDescriptor: EntityDescriptor,
- })
- require.NoError(t, err)
-
- connector.SetSigningKeyPair(signingKeypair)
- connector.SetEncryptionKeyPair(encryptionKeypair)
-
- clock := clockwork.NewFakeClockAt(time.Date(2021, time.April, 4, 0, 0, 0, 0, time.UTC))
- provider, err := services.GetSAMLServiceProvider(connector, clock)
- require.NoError(t, err)
- assertionInfo, err := provider.RetrieveAssertionInfo(EncryptedResponse)
- require.NoError(t, err)
- require.NotEmpty(t, assertionInfo.Assertions)
-}
-
-// TestPingSAMLWorkaround ensures we provide required additional authn query
-// parameters for Ping backends (PingOne, PingFederate, etc) when
-// `provider: ping` is set.
-func TestPingSAMLWorkaround(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- clock := clockwork.NewFakeClockAt(time.Now())
-
- // Create a Server instance for testing.
- b, err := memory.New(memory.Config{
- Context: ctx,
- Clock: clock,
- })
- require.NoError(t, err)
-
- clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
- ClusterName: "me.localhost",
- })
- require.NoError(t, err)
-
- authConfig := &InitConfig{
- ClusterName: clusterName,
- Backend: b,
- Authority: authority.New(),
- SkipPeriodicOperations: true,
- KeyStoreConfig: keystore.Config{
- Software: keystore.SoftwareConfig{
- RSAKeyPairSource: authority.New().GenerateKeyPair,
- },
- },
- }
-
- a, err := NewServer(authConfig)
- require.NoError(t, err)
-
- // Create a new SAML connector for Ping.
- const entityDescriptor = `
-
-
-
-
- MIIDejCCAmKgAwIBAgIGAXnsYbiQMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKDA1QaW5nIElkZW50aXR5MRYwFAYDVQQLDA1QaW5nIElkZW50aXR5MT8wPQYDVQQDDDZQaW5nT25lIFNTTyBDZXJ0aWZpY2F0ZSBmb3IgQWRtaW5pc3RyYXRvcnMgZW52aXJvbm1lbnQwHhcNMjEwNjA4MTYwODE3WhcNMjIwNjA4MTYwODE3WjB+MQswCQYDVQQGEwJVUzEWMBQGA1UECgwNUGluZyBJZGVudGl0eTEWMBQGA1UECwwNUGluZyBJZGVudGl0eTE/MD0GA1UEAww2UGluZ09uZSBTU08gQ2VydGlmaWNhdGUgZm9yIEFkbWluaXN0cmF0b3JzIGVudmlyb25tZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArqJP+9QA8rzt9lLrKQigkT1HxCP5qIQH9vKgIhCDx5q7eSHOlxQ7MMa+1v1WQq1y5mgNG1zxe+cEaJ646JHQLoa0yj+rXsfCsUsKG7qceHzMR8p4y74x77PHTBJEviS9g/+fMGq7eaSK/F8ksPBfBjHnWv+lvnzrAGhxEuBXfFPf5Gb2Vr5LYurZEu9lIdFtSnFCVjzUIC1SMyovl92K4WdJpZ60N8FUSR6Jb7b8gWjnNHNc1iwr5C2b8+HUuWhqCIc0TQygEilZAdJhpYkeCQMiSqySsV+cmJ1vdjsV0HXX0YREDq6koklnw1hyTe1AckcH6qfWyBcoG2VYORjZPQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA0eVvkB+/RSIEs7CXje7KKFGO99X7nIBNcpztp6kevxTDFHKsVlGFfl/mkksw9SjzdWSMDgGxxy6riYnScQD0FdyxaKzM0CRFfqdHf2+qVnK4GbiodqLOVp1dDE6CSQuPp7inQr+JDO/xD1WUAyMSC+ouFRdHq2O7MCYolEcyWiZoTTcch8RhLo5nqueKQfP0vaJwzAPgpXxAuabVuyrtN0BZHixO/sjjg9yup8/esCMBB/RR90PxzbI+8ZX5g1MxZZwSaXauQFyOjm5/t+JEisZf8rzrrhDd2GzWrYngB8DJLxCUK1JTM5SO/k3TqeDHLHi202P7AN2S/1CqzCaGb
-
-
-
-
-
-
-
-
- `
-
- signingKeypair := &types.AsymmetricKeyPair{
- Cert: fixtures.TLSCACertPEM,
- PrivateKey: fixtures.TLSCAKeyPEM,
- }
-
- encryptionKeypair := &types.AsymmetricKeyPair{
- Cert: fixtures.EncryptionCertPEM,
- PrivateKey: fixtures.EncryptionKeyPEM,
- }
-
- // SAML connector validation requires the roles in mappings exist.
- role, err := types.NewRole("admin", types.RoleSpecV5{})
- require.NoError(t, err)
- err = a.CreateRole(ctx, role)
- require.NoError(t, err)
-
- connector, err := types.NewSAMLConnector("ping", types.SAMLConnectorSpecV2{
- AssertionConsumerService: "https://proxy.example.com:3080/v1/webapi/saml/acs",
- Provider: "ping",
- Display: "Ping",
- AttributesToRoles: []types.AttributeMapping{
- {Name: "groups", Value: "ping-admin", Roles: []string{role.GetName()}},
- },
- EntityDescriptor: entityDescriptor,
- SigningKeyPair: signingKeypair,
- EncryptionKeyPair: encryptionKeypair,
- })
- require.NoError(t, err)
-
- err = a.UpsertSAMLConnector(ctx, connector)
- require.NoError(t, err)
-
- // Create an auth request that we can inspect.
- req, err := a.CreateSAMLAuthRequest(ctx, types.SAMLAuthRequest{
- ConnectorID: "ping",
- })
- require.NoError(t, err)
-
- // Parse the generated redirection URL.
- parsed, err := url.Parse(req.RedirectURL)
- require.NoError(t, err)
-
- require.Equal(t, "auth.pingone.com", parsed.Host)
- require.Equal(t, "/8be7412d-7d2f-4392-90a4-07458d3dee78/saml20/idp/sso", parsed.Path)
-
- // SigAlg and Signature must be added when `provider: ping`.
- require.NotEmpty(t, parsed.Query().Get("SigAlg"), "SigAlg is required for provider: ping")
- require.NotEmpty(t, parsed.Query().Get("Signature"), "Signature is required for provider: ping")
-}
-
-func TestServer_getConnectorAndProvider(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- clock := clockwork.NewFakeClockAt(time.Now())
-
- // Create a Server instance for testing.
- b, err := memory.New(memory.Config{
- Context: ctx,
- Clock: clock,
- })
- require.NoError(t, err)
-
- clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
- ClusterName: "me.localhost",
- })
- require.NoError(t, err)
-
- authConfig := &InitConfig{
- ClusterName: clusterName,
- Backend: b,
- Authority: authority.New(),
- SkipPeriodicOperations: true,
- KeyStoreConfig: keystore.Config{
- Software: keystore.SoftwareConfig{
- RSAKeyPairSource: authority.New().GenerateKeyPair,
- },
- },
- }
-
- a, err := NewServer(authConfig)
- require.NoError(t, err)
-
- sas, ok := a.samlAuthService.(*SAMLAuthService)
- require.True(t, ok, "Server.samlAuthServer is not type *samlAuthServer")
-
- _, err = CreateRole(ctx, a, "baz", types.RoleSpecV5{})
- require.NoError(t, err)
-
- caKey, err := rsa.GenerateKey(rand.Reader, 2048)
- require.NoError(t, err)
-
- tlsCert, err := tlsca.GenerateSelfSignedCAWithSigner(
- caKey,
- pkix.Name{
- CommonName: "server1",
- Organization: []string{"server1"},
- }, nil, defaults.CATTL)
- require.NoError(t, err)
- require.NotNil(t, tlsCert)
-
- keyPEM, certPEM, err := utils.GenerateSelfSignedSigningCert(pkix.Name{
- Organization: []string{"Teleport OSS"},
- CommonName: "teleport.localhost.localdomain",
- }, nil, 10*365*24*time.Hour)
- require.NoError(t, err)
-
- request := types.SAMLAuthRequest{
- ID: "ABC",
- ConnectorID: "zzz",
- CheckUser: false,
- PublicKey: nil,
- CertTTL: 0,
- CreateWebSession: false,
- SSOTestFlow: true,
- ConnectorSpec: &types.SAMLConnectorSpecV2{
- Issuer: "test",
- Audience: "test",
- ServiceProviderIssuer: "test",
- SSO: "test",
- Cert: string(tlsCert),
- AssertionConsumerService: "test",
- AttributesToRoles: []types.AttributeMapping{{
- Name: "foo",
- Value: "bar",
- Roles: []string{"baz"},
- }},
- SigningKeyPair: &types.AsymmetricKeyPair{
- PrivateKey: string(keyPEM),
- Cert: string(certPEM),
- },
- },
- }
-
- connector, provider, err := sas.getSAMLConnectorAndProvider(context.Background(), request)
- require.NoError(t, err)
- require.NotNil(t, connector)
- require.NotNil(t, provider)
-
- expectedConnector := &types.SAMLConnectorV2{Kind: "saml", Version: "v2", Metadata: types.Metadata{Name: "zzz", Namespace: apidefaults.Namespace}, Spec: *request.ConnectorSpec}
- require.Equal(t, expectedConnector, connector)
-
- require.Equal(t, "test", provider.IdentityProviderSSOURL)
- require.Equal(t, "test", provider.IdentityProviderIssuer)
- require.Equal(t, "test", provider.AssertionConsumerServiceURL)
- require.Equal(t, "test", provider.ServiceProviderIssuer)
-
- conn, err := types.NewSAMLConnector("foo", types.SAMLConnectorSpecV2{
- Issuer: "test",
- SSO: "test",
- Cert: string(tlsCert),
- AssertionConsumerService: "test",
- AttributesToRoles: []types.AttributeMapping{{
- Name: "foo",
- Value: "bar",
- Roles: []string{"baz"},
- }},
- })
- require.NoError(t, err)
-
- err = a.UpsertSAMLConnector(ctx, conn)
- require.NoError(t, err)
-
- request2 := types.SAMLAuthRequest{
- ID: "ABC",
- ConnectorID: "foo",
- SSOTestFlow: false,
- }
-
- connector, provider, err = sas.getSAMLConnectorAndProvider(context.Background(), request2)
- require.NoError(t, err)
- require.NotNil(t, connector)
- require.NotNil(t, provider)
-}
-
-func TestServer_ValidateSAMLResponse(t *testing.T) {
- t.Parallel()
-
- ctx := context.Background()
- clock := clockwork.NewFakeClockAt(time.Date(2022, 4, 25, 9, 0, 0, 0, time.UTC))
-
- // Create a Server instance for testing.
- b, err := memory.New(memory.Config{
- Context: ctx,
- Clock: clock,
- })
- require.NoError(t, err)
-
- clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
- ClusterName: "me.localhost",
- })
- require.NoError(t, err)
-
- authConfig := &InitConfig{
- ClusterName: clusterName,
- Backend: b,
- Authority: authority.New(),
- SkipPeriodicOperations: true,
- KeyStoreConfig: keystore.Config{
- Software: keystore.SoftwareConfig{
- RSAKeyPairSource: authority.New().GenerateKeyPair,
- },
- },
- }
-
- a, err := NewServer(authConfig, WithClock(clock))
- require.NoError(t, err)
-
- sas, ok := a.samlAuthService.(*SAMLAuthService)
- require.True(t, ok, "Server.samlAuthServer is not type *samlAuthServer")
-
- // empty response gives error.
- response, err := a.ValidateSAMLResponse(context.Background(), "", "")
- require.Nil(t, response)
- require.Error(t, err)
-
- // create role referenced in request.
- role, err := types.NewRole("access", types.RoleSpecV5{
- Allow: types.RoleConditions{
- Logins: []string{"dummy"},
- },
- })
- require.NoError(t, err)
- err = a.CreateRole(ctx, role)
- require.NoError(t, err)
-
- // real response from Okta
- respOkta := `http://www.okta.com/exk14fxcpjuKMcor30h8uBRfvYvl5C/LPCh36uAmRLHW76+aDP3ngChtIwP3/Fc=M1VfkOOBH6r7niHhfGvf4OJ1HH5QJl83aD/b+mTDUUnXzHXgXlkb0BGQkSFn6ixojwCoXchpxCNzVLPN/tvfyY1dxP4MO8b+/07bGuVD2yTNlhN43/FFcDpmZ1ZDW8w2nPF1E5gy1lR8Wx2NgT3kQ2Ui1vRNX/KeX/P9NnABj4AjcshyHK2e49WLM/D4U84XOl7ODtzS7PTvtB0SGIwRE25G//8AsAv81eBfHL54Nz1HAqinMhxQtz32ZDXpKaAV6GypyBTvk6vo7Pkk4OiL6G9VIGC8Bd/gnavsc+Ickfuo7KTq8NDKTLB5WG34XKJqq6dGopSMrxr67oYjCEDZfw==MIIDpDCCAoygAwIBAgIGAX4zyofpMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG
-A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
-MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi04MTMzNTQxHDAaBgkqhkiG9w0BCQEW
-DWluZm9Ab2t0YS5jb20wHhcNMjIwMTA3MDkwNTU4WhcNMzIwMTA3MDkwNjU4WjCBkjELMAkGA1UE
-BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
-BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtODEzMzU0MRwwGgYJ
-KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
-xQz+tLD5cNlOBfdohHvqNWIfC13OCSnUAe20qA0K8y+jtZrpwjtjjLX8iRuCx8dYc/nd6zYOhhSq
-2sLmrRa09wUXXTgnLGcj50gePTaroYLyF4FNgQWLvPHJk0FGcx6JvD6L+V5RzYwH87Fhg8niP4LZ
-EBw3iZnsIJN9KOuLuQeXTW0PIlMFzpCwT9aUCHCoLepe5Ou8oi8XcOCmsOESHPchV2RC/xQDIqRP
-Lp1Sf7NNJ6mTmP2gOoLwsz95beOLrEI+PI/GgZBqM3OutWA0L9mAbJK9T5dPAvhnwCV+SK2HvicJ
-T8c6uJxuKmoWv1t3SyaN0cIbmw6vj9CIf4DTwQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCWGgLL
-f3tgUZRGjmR5iiKeOeaEWG/eaF1nfenVfSaWT9ckimcXyLCY/P7CXEBiioVrxjky07iceJpi4rVE
-RcVZ8SGXCa0NroESmIFlIHez6vRTrqUsfDmidxsSCwY02eaBq+9gK5iXV5WeXMKbn0yeGwF+3PkU
-RAH1HuypwMH0FJRLIdW36pw7FCrGrXpk3UC6mEumXC9FptjSK1FlW+ZckgDprePOoUpypEygr2UC
-XXOsqT0dwBUUttdOQMZHqIiXS5VPJ8zhYPHBGYI8WGk5FWVuXIXhgRm7LN/EyXIvCOFmDH0tVnQL
-V115UGOwvjOOxmOFbYBn865SHgMndFtrhttp://www.okta.com/exk14fxcpjuKMcor30h8XwJSotSzU2qLdzu/WDk8dpQ/Cy1Id88932S/95+N+Ds=qyIvGi1+w93AdGUj0+T5RYAq+CAjLSScMTMc7dLTEze6qr3mP51W/bCoZz8E47lpsbLeh0EiATa6h2Uaj6/34rILfCt3aQRNjNicu0gBKhePyNraapdnoyeqJEV8UrAOOKFiH30e5AvQ1nRZqfgY7KMt6cZH5/eXjUS63lPJJn4yr9vLw9loCdHCoHlaseh2IHi7CickyyxSMTX+Y58zpBy2g/KwN3K4oZM4a10ZYWkZpzkZJXDRSUkEc/wTTO7IPPY7Zv7R7UC+zjf5Px1sYeKTkkIxlZViZmtqjYuhibnTmhroJx7wX/LtOPxCkwLHlQRDACBNbP/UtrudU1ZMxA==MIIDpDCCAoygAwIBAgIGAX4zyofpMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG
-A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
-MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi04MTMzNTQxHDAaBgkqhkiG9w0BCQEW
-DWluZm9Ab2t0YS5jb20wHhcNMjIwMTA3MDkwNTU4WhcNMzIwMTA3MDkwNjU4WjCBkjELMAkGA1UE
-BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
-BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtODEzMzU0MRwwGgYJ
-KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
-xQz+tLD5cNlOBfdohHvqNWIfC13OCSnUAe20qA0K8y+jtZrpwjtjjLX8iRuCx8dYc/nd6zYOhhSq
-2sLmrRa09wUXXTgnLGcj50gePTaroYLyF4FNgQWLvPHJk0FGcx6JvD6L+V5RzYwH87Fhg8niP4LZ
-EBw3iZnsIJN9KOuLuQeXTW0PIlMFzpCwT9aUCHCoLepe5Ou8oi8XcOCmsOESHPchV2RC/xQDIqRP
-Lp1Sf7NNJ6mTmP2gOoLwsz95beOLrEI+PI/GgZBqM3OutWA0L9mAbJK9T5dPAvhnwCV+SK2HvicJ
-T8c6uJxuKmoWv1t3SyaN0cIbmw6vj9CIf4DTwQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCWGgLL
-f3tgUZRGjmR5iiKeOeaEWG/eaF1nfenVfSaWT9ckimcXyLCY/P7CXEBiioVrxjky07iceJpi4rVE
-RcVZ8SGXCa0NroESmIFlIHez6vRTrqUsfDmidxsSCwY02eaBq+9gK5iXV5WeXMKbn0yeGwF+3PkU
-RAH1HuypwMH0FJRLIdW36pw7FCrGrXpk3UC6mEumXC9FptjSK1FlW+ZckgDprePOoUpypEygr2UC
-XXOsqT0dwBUUttdOQMZHqIiXS5VPJ8zhYPHBGYI8WGk5FWVuXIXhgRm7LN/EyXIvCOFmDH0tVnQL
-V115UGOwvjOOxmOFbYBn865SHgMndFtrops@gravitational.iohttps://boson.tener.io:3080/v1/webapi/saml/acsurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportops@gravitational.ioEveryoneokta-adminokta-dev`
-
- caKey, err := rsa.GenerateKey(rand.Reader, 2048)
- require.NoError(t, err)
-
- tlsCert, err := tlsca.GenerateSelfSignedCAWithSigner(
- caKey,
- pkix.Name{
- CommonName: "server1",
- Organization: []string{"server1"},
- }, nil, defaults.CATTL)
- require.NoError(t, err)
- require.NotNil(t, tlsCert)
-
- keyPEM, certPEM, err := utils.GenerateSelfSignedSigningCert(pkix.Name{
- Organization: []string{"Teleport OSS"},
- CommonName: "teleport.localhost.localdomain",
- }, nil, 10*365*24*time.Hour)
- require.NoError(t, err)
-
- // SAML connector validation requires the roles in mappings exist.
- connectorRole, err := types.NewRole("baz", types.RoleSpecV5{})
- require.NoError(t, err)
- err = a.CreateRole(ctx, connectorRole)
- require.NoError(t, err)
-
- conn, err := types.NewSAMLConnector("saml-test-conn", types.SAMLConnectorSpecV2{
- Issuer: "test",
- SSO: "test",
- Cert: string(tlsCert),
- AssertionConsumerService: "test",
- AttributesToRoles: []types.AttributeMapping{{
- Name: "foo",
- Value: "bar",
- Roles: []string{connectorRole.GetName()},
- }},
- SigningKeyPair: &types.AsymmetricKeyPair{
- PrivateKey: string(keyPEM),
- Cert: string(certPEM),
- },
- })
- require.NoError(t, err)
-
- err = a.UpsertSAMLConnector(ctx, conn)
- require.NoError(t, err)
-
- err = a.Services.CreateSAMLAuthRequest(ctx, types.SAMLAuthRequest{
- ID: "_4f256462-6c2d-466d-afc0-6ee36602b6f2",
- ConnectorID: "saml-test-conn",
- SSOTestFlow: true,
- RedirectURL: "https://dev-813354.oktapreview.com/app/dev-813354_krzysztofssodev_1/exk14fxcpjuKMcor30h8/sso/saml?SAMLRequest=lFZZk6JKE%2F0rHc6jYbOIiMbtiSgWEREEQVBfbrAUUMiiFFLgr%2F9i7Jm%2BPXf75j5mUifz5MmMJH%2FDQVlcl%2BDeZtUe3u4Qty99WVR4%2BfzwNro31bIOMMLLKighXrbR0gHGdsm%2B0strU7d1VBejT5B%2FRwQYw6ZFdTV60eS30e9cws54jmcnfMTGE47n40mQRPSEh3DK8zQb8gk7evFgg1FdvY3YV3r0Yn3PKqIqRlX67wnD90d4uXZda2LtHHf0An6QkOoK30vYOLDpUAQP%2B%2B3bKGvbK15SVFjjunptYQWbV1Qvp7RAUx1DERgGV0R9q5QKIjx60TC%2BQ63CbVC1byOWZtkJzU3YmUsLy9lsyQjn0YsMcYuqoH3W8CNBDLuJwEynM%2B61vrTBtYEdguQ1qksquF4%2Fff790jwG%2FGjrBOM6ht3vDAX7C8MlfXTN77oR1c2UzgQK4%2FrJa%2FT12dTlk1nz9b8V9Bv1GftbjJcOSqugvTfwe5Nj%2FF7DkqIIIa9k%2Blo3KcXSNE3RC6ovixij9MvoAwtjrUrqpykFVV2hKCjQ4ymGAdusjl9AkdYNarPyHwLzFMN%2BCzyJGK5imBH1M69fjMJQNPeD3qSsG%2FilwcEEZwE747%2BH3MMENrCK4Mthr72NvvzaeD6hbhNUOKmbEv9s%2Fl9aP6kGqw4W9RXGE%2Fyjuu%2FUfj3g36hF%2FZWgjFKI2%2F8oHayiLz8J9h7FC4o7%2FGogv2z54Iyj8bXj7FAoDc5Dwf3O0atwvsq3Ju%2FKBm0Bcnh7MvoMfjo%2B5H83%2FzQ8H%2F1%2BR2wJqAs5PUmLBA%2B5pPtrwRn4rhbER6QzrNHr9h1nztqm1S0kJT1Oo0NfxHRdnvx%2B27Lurbdl8FgdEZvM3FBqnV1DeHisSX7ezTHT24DMvdJBpb%2FlrVj0UmQk%2BVHhB1MyU65CSl2t1cHyBANokpgwPbrm86ne%2BBssK0UEVoAqlKFREkvel%2BWM7Qkv7MVOsUg0X%2FM3Yc1x2prVjXw4CUVxccn6UO53BsObdG4MpcBNGcYaWyp9NBQXcRTKtqSdVpyo7zM7PA6OQ53nJ0%2BMTGtqHpFmVLfxdnaKUpMa32Wt2N%2Bsy0NYr8aC7gxb5KPAnR%2Flta%2B0VaWa8I5XdyHSZ3XZiMM4Ho9tKaHA29uH9J%2B0%2Fia%2FDoePVhxn9EIO2uDDkL7t0wRFQQu%2FGpom6w9JAtkuBUQTQartwVbT871EMqWEfmR2ZGASUToQ2T5t9PqsZV1kAlvZijYgtqx4hmiogDkoUmYYnurh81HsospOXcZ0DSciG%2Fske7YtK%2F2MPvt9EamLIZZmOGTNLFSzLpra91Bd5Ce%2Fv4QskwU%2BR9ZZZBq5RkxZYww5ZYyHwvnffI%2Bnb%2Fjw5SIw9geikGcOXSH94Y8conVgjH7zAIWYmp4IDHdd7YtQ9Ug43dDbsu9O7AoH6uLxB599F%2Fqra5hLEnC0P9csijaQ01SxgCxJwK6lNFVEYAZRd7v7M%2BFkb1nIzriDojbZoOe3vb2z8xUppklq%2BWaQ9tNq84j53vOPQqEG7KbjFFo93MIT2GoBVXgNOCVuy9oqdDirWoiHq6pTV9NKdySK52MZjvO5hjMLye6miZrHoqR7nN96vqwyTKsb%2FZTz7W63urT12CWsdlmV05vNRw6rcpcdjw26i3w%2FgNhShGyfn%2BVNfM%2Bgf7xk1IWiu16cx7QVxoXZFJej1yyctR%2FHM51Hjqe4xgy4sraoSXSSbuJj8O%2F0GMw4xO38u5s6F3Z6m7GPqddGEb3SRKII6%2FMBGHGHUkd30Km51Xi%2FSFhm3%2FN%2BRBToqKs5ZeJzCQKj7u1wk%2FYL9mblYsBeBAkQBYDASA2RI3J6kr09bQF7TYnAlkEKgQHIt7mLFaKIFLElA4C%2Fm1H52SP56MXrtT1di%2FxxbqOHV9mUlKYPZacgQ2KkAveXIzxXIrPm3etKKRd5787nrSeqIt0vYjAopNyRE0iT8WrmbrSCL2%2FJoIVHfXxc9bcgquX1tWmt9lbe1ky7Z2l1EVlC2SdhOQ5v3LzZ%2BSbTpqDUir4P9vyZVV2Ffehk1g15fMYOiqX%2BEixOsS%2BAVImuldfCW583mHN2UYjijYbHC7PqW3DdWIa%2B2l3CS5dv%2BtkCXxIJm1VzEeSpBJGM3Dm1NuStwUXCVHFVmpMo52E6nVpcE54uXS2%2FDI8hBnqnIR2hBWZLRkfuDqu6d5GVZmyFRl1zA3%2Fci3Tr1WUs2f1DOm9lzCPSIVU9wGZOtPcF9Oel8uF8XzvU54X008L6%2Bv2gNYMSarJVFygaXkBR1ERqYNDCt1Hb3OHoZVU3ZdD%2B8%2B3IvDJPD4onyfPp8l7hK4xQgmD8%2FKf%2B9XD%2B%2Br8AAAD%2F%2Fw%3D%3D",
- ClientRedirectURL: "http://127.0.0.1:57293/callback?secret_key=70e98f8871530e66e6a136ae71fe002454dc1c76d754f090f895508a2226c36b",
- ConnectorSpec: &types.SAMLConnectorSpecV2{
- Issuer: "http://www.okta.com/exk14fxcpjuKMcor30h8",
- SSO: "https://dev-813354.oktapreview.com/app/dev-813354_krzysztofssodev_1/exk14fxcpjuKMcor30h8/sso/saml",
- Cert: "",
- Display: "Okta",
- AssertionConsumerService: "https://boson.tener.io:3080/v1/webapi/saml/acs",
- Audience: "https://boson.tener.io:3080/v1/webapi/saml/acs",
- ServiceProviderIssuer: "https://boson.tener.io:3080/v1/webapi/saml/acs",
- EntityDescriptor: "\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\u003cmd:EntityDescriptor entityID=\"http://www.okta.com/exk14fxcpjuKMcor30h8\" xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\u003e\u003cmd:IDPSSODescriptor WantAuthnRequestsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\"\u003e\u003cmd:KeyDescriptor use=\"signing\"\u003e\u003cds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"\u003e\u003cds:X509Data\u003e\u003cds:X509Certificate\u003eMIIDpDCCAoygAwIBAgIGAX4zyofpMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG\nA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU\nMBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi04MTMzNTQxHDAaBgkqhkiG9w0BCQEW\nDWluZm9Ab2t0YS5jb20wHhcNMjIwMTA3MDkwNTU4WhcNMzIwMTA3MDkwNjU4WjCBkjELMAkGA1UE\nBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV\nBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtODEzMzU0MRwwGgYJ\nKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\nxQz+tLD5cNlOBfdohHvqNWIfC13OCSnUAe20qA0K8y+jtZrpwjtjjLX8iRuCx8dYc/nd6zYOhhSq\n2sLmrRa09wUXXTgnLGcj50gePTaroYLyF4FNgQWLvPHJk0FGcx6JvD6L+V5RzYwH87Fhg8niP4LZ\nEBw3iZnsIJN9KOuLuQeXTW0PIlMFzpCwT9aUCHCoLepe5Ou8oi8XcOCmsOESHPchV2RC/xQDIqRP\nLp1Sf7NNJ6mTmP2gOoLwsz95beOLrEI+PI/GgZBqM3OutWA0L9mAbJK9T5dPAvhnwCV+SK2HvicJ\nT8c6uJxuKmoWv1t3SyaN0cIbmw6vj9CIf4DTwQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCWGgLL\nf3tgUZRGjmR5iiKeOeaEWG/eaF1nfenVfSaWT9ckimcXyLCY/P7CXEBiioVrxjky07iceJpi4rVE\nRcVZ8SGXCa0NroESmIFlIHez6vRTrqUsfDmidxsSCwY02eaBq+9gK5iXV5WeXMKbn0yeGwF+3PkU\nRAH1HuypwMH0FJRLIdW36pw7FCrGrXpk3UC6mEumXC9FptjSK1FlW+ZckgDprePOoUpypEygr2UC\nXXOsqT0dwBUUttdOQMZHqIiXS5VPJ8zhYPHBGYI8WGk5FWVuXIXhgRm7LN/EyXIvCOFmDH0tVnQL\nV115UGOwvjOOxmOFbYBn865SHgMndFtr\u003c/ds:X509Certificate\u003e\u003c/ds:X509Data\u003e\u003c/ds:KeyInfo\u003e\u003c/md:KeyDescriptor\u003e\u003cmd:NameIDFormat\u003eurn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\u003c/md:NameIDFormat\u003e\u003cmd:SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://dev-813354.oktapreview.com/app/dev-813354_krzysztofssodev_1/exk14fxcpjuKMcor30h8/sso/saml\"/\u003e\u003cmd:SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"https://dev-813354.oktapreview.com/app/dev-813354_krzysztofssodev_1/exk14fxcpjuKMcor30h8/sso/saml\"/\u003e\u003c/md:IDPSSODescriptor\u003e\u003c/md:EntityDescriptor\u003e",
- EntityDescriptorURL: "",
- AttributesToRoles: []types.AttributeMapping{
- {
- Name: "groups",
- Value: "okta-admin",
- Roles: []string{"access"},
- },
- },
- SigningKeyPair: &types.AsymmetricKeyPair{
- PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1py+q5bnxhAvZ7bnhQQauHIqOpFA5CMXCXd+A9Y1qDHecnN3\nrFVZfyUZrYm/gTQZSptgAshr+VWsBh9O3ZAZ5Lg+f0FSkYr+k0+A7Bx3v4N76Psi\nyE+INMmtyvP2bTGyOrHqaeGzQYkpiPq044WS2j5PDYiQWbepDpxLYbiQ7qwzS9xZ\nZp6w8TyFGNkMl26F5ZeSH+T/S/EHt3Q9t2U2uWRdWv1IdZ13krqJJURMzkBMMj2j\nBxgKoHPJa7T4DniLg5a5OBKTbernbPdW1xzQUgHATwdlQAx2+KBIpKJiuqixH1/b\nVHHpZzAR5IYXv82xmYBoyjFBsmDH3ao+MFraTwIDAQABAoIBAQCEXZbINEHtiiwC\n1u/Cvb5RRrC/ALm6O95Ii3egnCzp+SAPDSKhmt6hKdvFifEgmmaC+oPkE4Ns/Ccm\ne4bj5q3hwLVjPYHUnJrZdq64cfJ1n338O3C/hTYoAL/9Li0uOfmIdBV1iqxJ3nRM\ntPx+W/MwQj/1w+XsP/e4ODPSKMjTOyZkLVhArLA2qBM3l1NBWQw4EV96m1Dvjq2o\nxFYhSODZOYDYXq82NZya3cBzj30kEB/6fNk6qPAsMa3Ck7F/9mx3MA2XM/S0aB/U\nq+5I1g+mTAMPKa2c2Tv2hwWuRU9ddKGiXuw/gHPoBwEU7AVNk+nNSVDhj5/pqcFB\nM/lPNg6xAoGBAPu/oICyXwlXYfsvkTx+HHpY7Imq8RtBUjVpD3z+LC6+QydlF2Z/\nNqLDxBPZAAdA7VGzw5QdNR8DKY9EgeRQAcci8FIqjueTPQDVKl9kwJzOxqg8DM8t\nR8YpIOtP22JvjHAkFafBq9cWYZNLQwVabIkCdc0jUruN53WKRkj+b0npAoGBANo8\nkX7ypsS+riLu6Ez89tXHV78CW8eMb/Wxsa2hgrzd7KmgjYdj2wlfRaEY6pSvBA1Y\nMvy/Emvm2KhwuGzWWPJvoaQq/Hr6Puns9DVP8U3TTJDC3R5dDZUQ0DiVUGyez9Qu\nXV5aNMnXFjGPdRQYjGM1zG6677dM0CVYu/6MVSd3AoGBAN5X3tALufgsHzOUTXfa\nAhjk1PS573yc8piNk8pXSnp2PCVdGY/DJ2QV9uV4sJe3dmLEnCYCrdoYFuqcHQSi\nzQ8uAobvY4uP9T75BhV+jMdxsO8BKmcInO2dgZ+SxjZoQucAV8f0O2saL0/CFw1x\nUY6oh5aIbheMOzMKzwzE+1GRAoGAYm5FFWPuUfjK49irj+XckulZKz6uFJ/D86YU\nxIJ/TB4wWwWeL/2a0mxVJGbvjuYtRrOMM7EeZup0t+w3UmePMLGmzzvQKstpyupj\n7xPCe16dPwGU59gCg0RVFeBKqOMsS8Apvp+jBZJsYSgaH1k/IJQoQ50u95a+nsmZ\n6SJ0WdsCgYBfTcIJ456LNPQ7sjDtcqIQcbBgXYIt/u4dIhz0zuLSfvTrXAtcy7nU\nEDU+N2Ay715GmS+/iwc592Itam93t3sl1ql/Y+SrrxGQ7zRd6MALKIxM0+4qptB5\nDFXT1B5qhKHdZFr71AIGPrUjfsRQV8tPKFjRkuQ0zqEPvi4g06RMRw==\n-----END RSA PRIVATE KEY-----\n",
- Cert: "-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIRALIKjRCwhEmeWcNvwy1fBCUwDQYJKoZIhvcNAQELBQAw\nQDEVMBMGA1UEChMMVGVsZXBvcnQgT1NTMScwJQYDVQQDEx50ZWxlcG9ydC5sb2Nh\nbGhvc3QubG9jYWxkb21haW4wHhcNMjIwNDI1MDg1MzE4WhcNMzIwNDIyMDg1MzE4\nWjBAMRUwEwYDVQQKEwxUZWxlcG9ydCBPU1MxJzAlBgNVBAMTHnRlbGVwb3J0Lmxv\nY2FsaG9zdC5sb2NhbGRvbWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBANacvquW58YQL2e254UEGrhyKjqRQOQjFwl3fgPWNagx3nJzd6xVWX8lGa2J\nv4E0GUqbYALIa/lVrAYfTt2QGeS4Pn9BUpGK/pNPgOwcd7+De+j7IshPiDTJrcrz\n9m0xsjqx6mnhs0GJKYj6tOOFkto+Tw2IkFm3qQ6cS2G4kO6sM0vcWWaesPE8hRjZ\nDJduheWXkh/k/0vxB7d0PbdlNrlkXVr9SHWdd5K6iSVETM5ATDI9owcYCqBzyWu0\n+A54i4OWuTgSk23q52z3Vtcc0FIBwE8HZUAMdvigSKSiYrqosR9f21Rx6WcwEeSG\nF7/NsZmAaMoxQbJgx92qPjBa2k8CAwEAAaMgMB4wDgYDVR0PAQH/BAQDAgeAMAwG\nA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBADXVdHHQ3HB6X7QizVnQ/Cgg\nzEOEiMC1ClsxkXeZnB1H6TpFEm9jxT77tVBGB0x9dAyEwmOwYAgf+F5TJIl6mqfy\nIbXK+XFxqacoDHprtPtqmqH1tR20G9cP8mxfbm+bq47rOWN1tgAmIlxxaR6Z2GTE\n2zKw5vyjdZsSidCxka9YdW8AgEcpnVteqxjrs4SOcbidJIs+9NnxtApJPMKFOkbk\nvjJx59skfCsNnrk8D3CeiDiT7/HMDLM4c83ETG04C/SzNSvGlpf60mTIjkyzydAK\nvIiKii9s2m1KiTOsGKVkDEr+PbMoo4y6XRB0tVomdCQxzCZLDs6iwviGGUer7wI=\n-----END CERTIFICATE-----\n",
- },
- Provider: "",
- EncryptionKeyPair: nil,
- },
- }, defaults.SAMLAuthRequestTTL)
- require.NoError(t, err)
-
- // check ValidateSAMLResponse
- response, err = sas.ValidateSAMLResponse(context.Background(), base64.StdEncoding.EncodeToString([]byte(respOkta)), "")
- require.NoError(t, err)
- require.NotNil(t, response)
-
- // check internal method, validate diagnostic outputs.
- diagCtx := NewSSODiagContext(types.KindSAML, a)
- auth, err := sas.validateSAMLResponse(context.Background(), diagCtx, base64.StdEncoding.EncodeToString([]byte(respOkta)), "")
- require.NoError(t, err)
-
- // ensure diag info got stored and is identical.
- infoFromBackend, err := a.GetSSODiagnosticInfo(context.Background(), types.KindSAML, auth.Req.ID)
- require.NoError(t, err)
- require.Equal(t, &diagCtx.Info, infoFromBackend)
-
- // verify values
- require.Equal(t, "ops@gravitational.io", auth.Username)
- require.Equal(t, "ops@gravitational.io", auth.Identity.Username)
- require.Equal(t, "saml-test-conn", auth.Identity.ConnectorID)
- require.Equal(t, "_4f256462-6c2d-466d-afc0-6ee36602b6f2", auth.Req.ID)
- require.Equal(t, 0, len(auth.HostSigners))
-
- authnInstant := time.Date(2022, 4, 25, 8, 3, 11, 779000000, time.UTC)
-
- // ignore, this is boring and very complex.
- require.NotNil(t, diagCtx.Info.SAMLAssertionInfo.Assertions)
- diagCtx.Info.SAMLAssertionInfo.Assertions = nil
-
- require.Equal(t, types.SSODiagnosticInfo{
- TestFlow: true,
- Error: "",
- Success: true,
- CreateUserParams: &types.CreateUserParams{
- ConnectorName: "saml-test-conn",
- Username: "ops@gravitational.io",
- Roles: []string{"access"},
- Traits: map[string][]string{
- "groups": {"Everyone", "okta-admin", "okta-dev"},
- "username": {"ops@gravitational.io"},
- },
- SessionTTL: 108000000000000,
- },
- SAMLAttributesToRoles: []types.AttributeMapping{
- {
- Name: "groups",
- Value: "okta-admin",
- Roles: []string{"access"},
- },
- },
- SAMLAttributesToRolesWarnings: nil,
- SAMLAttributeStatements: map[string][]string{
- "groups": {"Everyone", "okta-admin", "okta-dev"},
- "username": {"ops@gravitational.io"},
- },
- SAMLAssertionInfo: &types.AssertionInfo{
- NameID: "ops@gravitational.io",
- Values: map[string]samltypes.Attribute{
- "groups": {
- XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:assertion", Local: "Attribute"},
- Name: "groups",
- NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified",
- Values: []samltypes.AttributeValue{
- {
- XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:assertion", Local: "AttributeValue"},
- Value: "Everyone",
- },
- {
- XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:assertion", Local: "AttributeValue"},
- Value: "okta-admin",
- },
- {
- XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:assertion", Local: "AttributeValue"},
- Value: "okta-dev",
- },
- },
- },
- "username": {
- XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:assertion", Local: "Attribute"},
- Name: "username",
- NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified",
- Values: []samltypes.AttributeValue{
- {
- XMLName: xml.Name{Space: "urn:oasis:names:tc:SAML:2.0:assertion", Local: "AttributeValue"},
- Value: "ops@gravitational.io",
- },
- },
- },
- },
- WarningInfo: &saml2.WarningInfo{},
- SessionIndex: "_4f256462-6c2d-466d-afc0-6ee36602b6f2",
- AuthnInstant: &authnInstant,
- SessionNotOnOrAfter: nil,
- Assertions: nil,
- ResponseSignatureValidated: true,
- },
- SAMLTraitsFromAssertions: map[string][]string{
- "groups": {"Everyone", "okta-admin", "okta-dev"},
- "username": {"ops@gravitational.io"},
- },
- SAMLConnectorTraitMapping: []types.TraitMapping{
- {
- Trait: "groups",
- Value: "okta-admin",
- Roles: []string{"access"},
- },
- },
- }, diagCtx.Info)
-
- // make sure no users have been created.
- users, err := a.GetUsers(false)
- require.NoError(t, err)
- require.Equal(t, 0, len(users))
-}
diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go
index d2aafea862fa8..a355bf4760db2 100644
--- a/lib/web/apiserver.go
+++ b/lib/web/apiserver.go
@@ -609,17 +609,6 @@ func (h *Handler) bindDefaultEndpoints() {
// Kube access handlers.
h.GET("/webapi/sites/:site/kubernetes", h.WithClusterAuth(h.clusterKubesGet))
- // OIDC related callback handlers
- h.GET("/webapi/oidc/login/web", h.WithRedirect(h.oidcLoginWeb))
- h.GET("/webapi/oidc/callback", h.WithMetaRedirect(h.oidcCallback))
- h.POST("/webapi/oidc/login/console", h.WithLimiter(h.oidcLoginConsole))
-
- // SAML 2.0 handlers
- h.POST("/webapi/saml/acs", h.WithMetaRedirect(h.samlACS))
- h.POST("/webapi/saml/acs/:connector", h.WithMetaRedirect(h.samlACS))
- h.GET("/webapi/saml/sso", h.WithMetaRedirect(h.samlSSO))
- h.POST("/webapi/saml/login/console", h.WithLimiter(h.samlSSOConsole))
-
// Github connector handlers
h.GET("/webapi/github/login/web", h.WithRedirect(h.githubLoginWeb))
h.GET("/webapi/github/callback", h.WithMetaRedirect(h.githubCallback))
@@ -1329,32 +1318,6 @@ func (h *Handler) motd(w http.ResponseWriter, r *http.Request, p httprouter.Para
return webclient.MotD{Text: authPrefs.GetMessageOfTheDay()}, nil
}
-func (h *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
- logger := h.log.WithField("auth", "oidc")
- logger.Debug("Web login start.")
-
- req, err := ParseSSORequestParams(r)
- if err != nil {
- logger.WithError(err).Error("Failed to extract SSO parameters from request.")
- return client.LoginFailedRedirectURL
- }
-
- response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(r.Context(), types.OIDCAuthRequest{
- CSRFToken: req.CSRFToken,
- ConnectorID: req.ConnectorID,
- CreateWebSession: true,
- ClientRedirectURL: req.ClientRedirectURL,
- CheckUser: true,
- ProxyAddress: r.Host,
- })
- if err != nil {
- logger.WithError(err).Error("Error creating auth request.")
- return client.LoginFailedRedirectURL
- }
-
- return response.RedirectURL
-}
-
func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
logger := h.log.WithField("auth", "github")
logger.Debug("Web login start.")
@@ -1484,112 +1447,6 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr
return redirectURL.String()
}
-func (h *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
- logger := h.log.WithField("auth", "oidc")
- logger.Debug("Console login start.")
-
- req := new(client.SSOLoginConsoleReq)
- if err := httplib.ReadJSON(r, req); err != nil {
- logger.WithError(err).Error("Error reading json.")
- return nil, trace.AccessDenied(SSOLoginFailureMessage)
- }
-
- if err := req.CheckAndSetDefaults(); err != nil {
- logger.WithError(err).Error("Missing request parameters.")
- return nil, trace.AccessDenied(SSOLoginFailureMessage)
- }
-
- response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(r.Context(), types.OIDCAuthRequest{
- ConnectorID: req.ConnectorID,
- ClientRedirectURL: req.RedirectURL,
- PublicKey: req.PublicKey,
- CertTTL: req.CertTTL,
- CheckUser: true,
- Compatibility: req.Compatibility,
- RouteToCluster: req.RouteToCluster,
- KubernetesCluster: req.KubernetesCluster,
- ProxyAddress: r.Host,
- AttestationStatement: req.AttestationStatement.ToProto(),
- })
- if err != nil {
- logger.WithError(err).Error("Failed to create OIDC auth request.")
- return nil, trace.AccessDenied(SSOLoginFailureMessage)
- }
-
- return &client.SSOLoginConsoleResponse{
- RedirectURL: response.RedirectURL,
- }, nil
-}
-
-func (h *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
- logger := h.log.WithField("auth", "oidc")
- logger.Debug("Callback start.")
-
- response, err := h.cfg.ProxyClient.ValidateOIDCAuthCallback(r.Context(), r.URL.Query())
- if err != nil {
- logger.WithError(err).Error("Error while processing callback.")
-
- // try to find the auth request, which bears the original client redirect URL.
- // if found, use it to terminate the flow.
- //
- // this improves the UX by terminating the failed SSO flow immediately, rather than hoping for a timeout.
- if requestID := r.URL.Query().Get("state"); requestID != "" {
- if request, errGet := h.cfg.ProxyClient.GetOIDCAuthRequest(r.Context(), requestID); errGet == nil && !request.CreateWebSession {
- if redURL, errEnc := RedirectURLWithError(request.ClientRedirectURL, err); errEnc == nil {
- return redURL.String()
- }
- }
- }
-
- if errors.Is(err, auth.ErrOIDCNoRoles) {
- return client.LoginFailedUnauthorizedRedirectURL
- }
-
- return client.LoginFailedBadCallbackRedirectURL
- }
-
- // if we created web session, set session cookie and redirect to original url
- if response.Req.CreateWebSession {
- logger.Info("Redirecting to web browser.")
-
- res := &SSOCallbackResponse{
- CSRFToken: response.Req.CSRFToken,
- Username: response.Username,
- SessionName: response.Session.GetName(),
- ClientRedirectURL: response.Req.ClientRedirectURL,
- }
-
- if err := SSOSetWebSessionAndRedirectURL(w, r, res, true); err != nil {
- logger.WithError(err).Error("Error setting web session.")
- return client.LoginFailedRedirectURL
- }
-
- return res.ClientRedirectURL
- }
-
- logger.Info("Callback redirecting to console login.")
- if len(response.Req.PublicKey) == 0 {
- logger.Error("Not a web or console login request.")
- return client.LoginFailedRedirectURL
- }
-
- redirectURL, err := ConstructSSHResponse(AuthParams{
- ClientRedirectURL: response.Req.ClientRedirectURL,
- Username: response.Username,
- Identity: response.Identity,
- Session: response.Session,
- Cert: response.Cert,
- TLSCert: response.TLSCert,
- HostSigners: response.HostSigners,
- })
- if err != nil {
- logger.WithError(err).Error("Error constructing ssh response")
- return client.LoginFailedRedirectURL
- }
-
- return redirectURL.String()
-}
-
func (h *Handler) installer(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
httplib.SetScriptHeaders(w.Header())
diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go
index 49ce5fd4c7f22..43e3e5b2b2f73 100644
--- a/lib/web/apiserver_test.go
+++ b/lib/web/apiserver_test.go
@@ -20,7 +20,6 @@ import (
"archive/tar"
"bufio"
"bytes"
- "compress/flate"
"compress/gzip"
"context"
"crypto"
@@ -41,14 +40,12 @@ import (
"os"
"os/user"
"path/filepath"
- "regexp"
"sort"
"strings"
"sync"
"testing"
"time"
- "github.com/beevik/etree"
"github.com/gogo/protobuf/proto"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
@@ -75,7 +72,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
- kyaml "k8s.io/apimachinery/pkg/util/yaml"
authztypes "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
@@ -96,13 +92,11 @@ import (
"github.com/gravitational/teleport/lib/auth/native"
"github.com/gravitational/teleport/lib/auth/testauthority"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
- "github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/bpf"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/conntest"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
- "github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/httplib/csrf"
kubeproxy "github.com/gravitational/teleport/lib/kube/proxy"
@@ -734,137 +728,6 @@ func Test_clientMetaFromReq(t *testing.T) {
}, got)
}
-func TestSAML(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- rawConnector string
- validSession bool
- expectedRedirectURL string
- }{
- {
- name: "success",
- rawConnector: fixtures.SAMLOktaConnectorV2,
- validSession: true,
- expectedRedirectURL: "/after",
- },
- {
- name: "fail to map claims to roles",
- rawConnector: strings.ReplaceAll(fixtures.SAMLOktaConnectorV2, "Everyone", "No-one"),
- validSession: false,
- expectedRedirectURL: client.LoginFailedUnauthorizedRedirectURL,
- },
- }
-
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- ctx := context.Background()
- s := newWebSuite(t)
- input := tc.rawConnector
-
- decoder := kyaml.NewYAMLOrJSONDecoder(strings.NewReader(input), defaults.LookaheadBufSize)
- var raw services.UnknownResource
- err := decoder.Decode(&raw)
- require.NoError(t, err)
-
- connector, err := services.UnmarshalSAMLConnector(raw.Raw)
- require.NoError(t, err)
-
- role, err := types.NewRoleV3(connector.GetAttributesToRoles()[0].Roles[0], types.RoleSpecV5{
- Options: types.RoleOptions{
- MaxSessionTTL: types.NewDuration(apidefaults.MaxCertDuration),
- },
- Allow: types.RoleConditions{
- NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
- Namespaces: []string{apidefaults.Namespace},
- Rules: []types.Rule{
- types.NewRule(types.Wildcard, services.RW()),
- },
- },
- })
- require.NoError(t, err)
- role.SetLogins(types.Allow, []string{s.user})
- err = s.server.Auth().UpsertRole(s.ctx, role)
- require.NoError(t, err)
-
- err = s.server.Auth().UpsertSAMLConnector(ctx, connector)
- require.NoError(t, err)
- s.server.Auth().SetClock(clockwork.NewFakeClockAt(time.Date(2017, 5, 10, 18, 53, 0, 0, time.UTC)))
- clt := s.clientNoRedirects()
-
- csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
-
- baseURL, err := url.Parse(clt.Endpoint("webapi", "saml", "sso") + `?connector_id=` + connector.GetName() + `&redirect_url=http://localhost/after`)
- require.NoError(t, err)
- req, err := http.NewRequest("GET", baseURL.String(), nil)
- require.NoError(t, err)
- addCSRFCookieToReq(req, csrfToken)
- re, err := clt.Client.RoundTrip(func() (*http.Response, error) {
- return clt.Client.HTTPClient().Do(req)
- })
- require.NoError(t, err)
-
- // we got a redirect
- urlPattern := regexp.MustCompile(`URL='([^']*)'`)
- locationURL := urlPattern.FindStringSubmatch(string(re.Bytes()))[1]
- u, err := url.Parse(locationURL)
- require.NoError(t, err)
- require.Equal(t, fixtures.SAMLOktaSSO, u.Scheme+"://"+u.Host+u.Path)
- data, err := base64.StdEncoding.DecodeString(u.Query().Get("SAMLRequest"))
- require.NoError(t, err)
- buf, err := io.ReadAll(flate.NewReader(bytes.NewReader(data)))
- require.NoError(t, err)
- doc := etree.NewDocument()
- err = doc.ReadFromBytes(buf)
- require.NoError(t, err)
- id := doc.Root().SelectAttr("ID")
- require.NotNil(t, id)
-
- authRequest, err := s.server.Auth().GetSAMLAuthRequest(context.Background(), id.Value)
- require.NoError(t, err)
-
- // now swap the request id to the hardcoded one in fixtures
- authRequest.ID = fixtures.SAMLOktaAuthRequestID
- authRequest.CSRFToken = csrfToken
- err = s.server.Auth().Services.CreateSAMLAuthRequest(ctx, *authRequest, backend.Forever)
- require.NoError(t, err)
-
- // now respond with pre-recorded request to the POST url
- in := &bytes.Buffer{}
- fw, err := flate.NewWriter(in, flate.DefaultCompression)
- require.NoError(t, err)
-
- _, err = fw.Write([]byte(fixtures.SAMLOktaAuthnResponseXML))
- require.NoError(t, err)
- err = fw.Close()
- require.NoError(t, err)
- encodedResponse := base64.StdEncoding.EncodeToString(in.Bytes())
- require.NotNil(t, encodedResponse)
-
- // now send the response to the server to exchange it for auth session
- form := url.Values{}
- form.Add("SAMLResponse", encodedResponse)
- req, err = http.NewRequest("POST", clt.Endpoint("webapi", "saml", "acs"), strings.NewReader(form.Encode()))
- req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- addCSRFCookieToReq(req, csrfToken)
- require.NoError(t, err)
- authRe, err := clt.Client.RoundTrip(func() (*http.Response, error) {
- return clt.Client.HTTPClient().Do(req)
- })
-
- require.NoError(t, err)
- // This route uses a meta redirect, so expect redirect URL in body instead of location header.
- require.Equal(t, http.StatusOK, authRe.Code(), "Response: %v", string(authRe.Bytes()))
- if tc.validSession {
- // we have got valid session
- require.NotEmpty(t, authRe.Headers().Get("Set-Cookie"))
- }
- require.Contains(t, string(authRe.Bytes()), tc.expectedRedirectURL)
- })
- }
-}
-
func TestWebSessionsCRUD(t *testing.T) {
t.Parallel()
s := newWebSuite(t)
@@ -6169,19 +6032,6 @@ func waitForOutput(stream *terminalStream, substr string) error {
}
}
-func (s *WebSuite) clientNoRedirects(opts ...roundtrip.ClientParam) *client.WebClient {
- hclient := client.NewInsecureWebClient()
- hclient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
- }
- opts = append(opts, roundtrip.HTTPClient(hclient))
- wc, err := client.NewWebClient(s.url().String(), opts...)
- if err != nil {
- panic(err)
- }
- return wc
-}
-
func (s *WebSuite) client(opts ...roundtrip.ClientParam) *client.WebClient {
opts = append(opts, roundtrip.HTTPClient(client.NewInsecureWebClient()))
wc, err := client.NewWebClient(s.url().String(), opts...)
diff --git a/lib/web/saml.go b/lib/web/saml.go
deleted file mode 100644
index c7eba7ee84028..0000000000000
--- a/lib/web/saml.go
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
-Copyright 2015-2019 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 web
-
-import (
- "errors"
- "net/http"
-
- "github.com/gravitational/form"
- "github.com/gravitational/trace"
- "github.com/julienschmidt/httprouter"
-
- "github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/lib/auth"
- "github.com/gravitational/teleport/lib/client"
- "github.com/gravitational/teleport/lib/httplib"
-)
-
-func (h *Handler) samlSSO(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
- logger := h.log.WithField("auth", "saml")
- logger.Debug("Web login start.")
-
- req, err := ParseSSORequestParams(r)
- if err != nil {
- logger.WithError(err).Error("Failed to extract SSO parameters from request.")
- return client.LoginFailedRedirectURL
- }
-
- response, err := h.cfg.ProxyClient.CreateSAMLAuthRequest(r.Context(), types.SAMLAuthRequest{
- ConnectorID: req.ConnectorID,
- CSRFToken: req.CSRFToken,
- CreateWebSession: true,
- ClientRedirectURL: req.ClientRedirectURL,
- })
- if err != nil {
- logger.WithError(err).Error("Error creating auth request.")
- return client.LoginFailedRedirectURL
- }
-
- return response.RedirectURL
-}
-
-func (h *Handler) samlSSOConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
- logger := h.log.WithField("auth", "saml")
- logger.Debug("Console login start.")
-
- req := new(client.SSOLoginConsoleReq)
- if err := httplib.ReadJSON(r, req); err != nil {
- logger.WithError(err).Error("Error reading json.")
- return nil, trace.AccessDenied(SSOLoginFailureMessage)
- }
-
- if err := req.CheckAndSetDefaults(); err != nil {
- logger.WithError(err).Error("Missing request parameters.")
- return nil, trace.AccessDenied(SSOLoginFailureMessage)
- }
-
- response, err := h.cfg.ProxyClient.CreateSAMLAuthRequest(r.Context(), types.SAMLAuthRequest{
- ConnectorID: req.ConnectorID,
- ClientRedirectURL: req.RedirectURL,
- PublicKey: req.PublicKey,
- CertTTL: req.CertTTL,
- Compatibility: req.Compatibility,
- RouteToCluster: req.RouteToCluster,
- KubernetesCluster: req.KubernetesCluster,
- AttestationStatement: req.AttestationStatement.ToProto(),
- })
- if err != nil {
- logger.WithError(err).Error("Failed to create SAML auth request.")
- return nil, trace.AccessDenied(SSOLoginFailureMessage)
- }
-
- return &client.SSOLoginConsoleResponse{RedirectURL: response.RedirectURL}, nil
-}
-
-func (h *Handler) samlACS(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
- logger := h.log.WithField("auth", "saml")
- logger.Debug("Callback start.")
-
- var samlResponse string
- if err := form.Parse(r, form.String("SAMLResponse", &samlResponse, form.Required())); err != nil {
- logger.WithError(err).Error("Error parsing response.")
- return client.LoginFailedRedirectURL
- }
-
- response, err := h.cfg.ProxyClient.ValidateSAMLResponse(r.Context(), samlResponse, p.ByName("connector"))
-
- if err != nil {
- logger.WithError(err).Error("Error while processing callback.")
-
- // try to find the auth request, which bears the original client redirect URL.
- // if found, use it to terminate the flow.
- //
- // this improves the UX by terminating the failed SSO flow immediately, rather than hoping for a timeout.
- if requestID, errParse := auth.ParseSAMLInResponseTo(samlResponse); errParse == nil {
- if request, errGet := h.cfg.ProxyClient.GetSAMLAuthRequest(r.Context(), requestID); errGet == nil && !request.CreateWebSession {
- if url, errEnc := RedirectURLWithError(request.ClientRedirectURL, err); errEnc == nil {
- return url.String()
- }
- }
- }
-
- if errors.Is(err, auth.ErrSAMLNoRoles) {
- return client.LoginFailedUnauthorizedRedirectURL
- }
-
- return client.LoginFailedBadCallbackRedirectURL
- }
-
- // if we created web session, set session cookie and redirect to original url
- if response.Req.CreateWebSession {
- logger.Debug("Redirecting to web browser.")
-
- redirect := response.Req.ClientRedirectURL
- if redirect == "" {
- redirect = "/web/"
- }
-
- res := &SSOCallbackResponse{
- CSRFToken: response.Req.CSRFToken,
- Username: response.Username,
- SessionName: response.Session.GetName(),
- ClientRedirectURL: redirect,
- }
-
- if err := SSOSetWebSessionAndRedirectURL(w, r, res, response.Req.CSRFToken != ""); err != nil {
- logger.WithError(err).Error("Error setting web session.")
- return client.LoginFailedRedirectURL
- }
-
- return res.ClientRedirectURL
- }
-
- logger.Debug("Callback redirecting to console login.")
- if len(response.Req.PublicKey) == 0 {
- logger.Error("Not a web or console login request.")
- return client.LoginFailedRedirectURL
- }
-
- redirectURL, err := ConstructSSHResponse(AuthParams{
- ClientRedirectURL: response.Req.ClientRedirectURL,
- Username: response.Username,
- Identity: response.Identity,
- Session: response.Session,
- Cert: response.Cert,
- TLSCert: response.TLSCert,
- HostSigners: response.HostSigners,
- })
- if err != nil {
- logger.WithError(err).Error("Error constructing ssh response.")
- return client.LoginFailedRedirectURL
- }
-
- return redirectURL.String()
-}