From d1ceb003d0b9a9bab95522558581be7a4f957092 Mon Sep 17 00:00:00 2001 From: ThibaultHerard Date: Tue, 30 Aug 2022 15:12:58 +0000 Subject: [PATCH 1/8] feat(saml): saml 2.0 implementation Signed-off-by: ThibaultHerard Co-authored-by: sebferrer Co-authored-by: psauvage Co-authored-by: alexGNX Co-authored-by: Stoakes --- .schema/api.openapi.json | 75 +++ .schema/openapi.json | 75 +++ continuity/manager.go | 4 + continuity/manager_relaystate.go | 151 ++++++ .../email-password/identity.schema.json | 5 +- .../kratos/email-password/kratos.yml | 2 +- driver/registry_default.go | 23 +- driver/registry_default_saml.go | 11 + driver/registry_default_test.go | 4 +- embedx/config.schema.json | 250 ++++++++++ go.mod | 10 +- go.sum | 18 +- identity/credentials.go | 1 + identity/credentials_saml.go | 51 ++ internal/httpclient/.openapi-generator/FILES | 24 + internal/httpclient/README.md | 56 +++ .../httpclient/docs/SelfServiceSamlUrl.md | 72 +++ .../httpclient/model_self_service_saml_url.go | 138 ++++++ ...0_identity_credentials_types_saml.down.sql | 1 + ...000_identity_credentials_types_saml.up.sql | 1 + postgres.yaml | 33 ++ selfservice/strategy/oidc/strategy_login.go | 1 - .../strategy/saml/.schema/link.schema.json | 17 + .../saml/.schema/settings.schema.json | 19 + selfservice/strategy/saml/config_test.go | 401 +++++++++++++++ selfservice/strategy/saml/const.go | 5 + selfservice/strategy/saml/error.go | 16 + selfservice/strategy/saml/handler.go | 400 +++++++++++++++ selfservice/strategy/saml/handler_test.go | 91 ++++ selfservice/strategy/saml/metadata_test.go | 123 +++++ selfservice/strategy/saml/provider.go | 43 ++ selfservice/strategy/saml/provider_config.go | 67 +++ selfservice/strategy/saml/provider_saml.go | 67 +++ selfservice/strategy/saml/schema.go | 8 + selfservice/strategy/saml/strategy.go | 466 ++++++++++++++++++ selfservice/strategy/saml/strategy_auth.go | 70 +++ .../strategy/saml/strategy_helper_test.go | 182 +++++++ selfservice/strategy/saml/strategy_login.go | 168 +++++++ .../strategy/saml/strategy_registration.go | 163 ++++++ selfservice/strategy/saml/strategy_test.go | 297 +++++++++++ .../strategy/saml/testdata/SP_IDPMetadata.xml | 118 +++++ .../saml/testdata/SP_SamlResponse.xml | 38 ++ .../TestSPCanHandleOneloginResponse_response | 1 + selfservice/strategy/saml/testdata/cert.pem | 13 + .../saml/testdata/expected_metadata.xml | 25 + selfservice/strategy/saml/testdata/key.pem | 15 + .../strategy/saml/testdata/myservice.cert | 19 + .../strategy/saml/testdata/myservice.key | 28 ++ .../saml/testdata/registration.schema.json | 16 + .../strategy/saml/testdata/saml.jsonnet | 17 + .../strategy/saml/testdata/saml_response.xml | 11 + .../strategy/saml/testdata/samlkratos.crt | 21 + selfservice/strategy/saml/types.go | 29 ++ spec/api.json | 59 +++ spec/swagger.json | 55 ++- ui/node/node.go | 1 + x/provider.go | 5 + x/relaystate.go | 51 ++ 58 files changed, 4119 insertions(+), 12 deletions(-) create mode 100644 continuity/manager_relaystate.go mode change 100644 => 100755 contrib/quickstart/kratos/email-password/identity.schema.json mode change 100644 => 100755 contrib/quickstart/kratos/email-password/kratos.yml create mode 100644 driver/registry_default_saml.go create mode 100644 identity/credentials_saml.go create mode 100644 internal/httpclient/docs/SelfServiceSamlUrl.md create mode 100644 internal/httpclient/model_self_service_saml_url.go create mode 100644 persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.down.sql create mode 100644 persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.up.sql create mode 100644 postgres.yaml create mode 100644 selfservice/strategy/saml/.schema/link.schema.json create mode 100644 selfservice/strategy/saml/.schema/settings.schema.json create mode 100644 selfservice/strategy/saml/config_test.go create mode 100644 selfservice/strategy/saml/const.go create mode 100644 selfservice/strategy/saml/error.go create mode 100644 selfservice/strategy/saml/handler.go create mode 100644 selfservice/strategy/saml/handler_test.go create mode 100644 selfservice/strategy/saml/metadata_test.go create mode 100644 selfservice/strategy/saml/provider.go create mode 100644 selfservice/strategy/saml/provider_config.go create mode 100644 selfservice/strategy/saml/provider_saml.go create mode 100644 selfservice/strategy/saml/schema.go create mode 100644 selfservice/strategy/saml/strategy.go create mode 100644 selfservice/strategy/saml/strategy_auth.go create mode 100644 selfservice/strategy/saml/strategy_helper_test.go create mode 100644 selfservice/strategy/saml/strategy_login.go create mode 100644 selfservice/strategy/saml/strategy_registration.go create mode 100644 selfservice/strategy/saml/strategy_test.go create mode 100644 selfservice/strategy/saml/testdata/SP_IDPMetadata.xml create mode 100644 selfservice/strategy/saml/testdata/SP_SamlResponse.xml create mode 100644 selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response create mode 100644 selfservice/strategy/saml/testdata/cert.pem create mode 100644 selfservice/strategy/saml/testdata/expected_metadata.xml create mode 100644 selfservice/strategy/saml/testdata/key.pem create mode 100755 selfservice/strategy/saml/testdata/myservice.cert create mode 100755 selfservice/strategy/saml/testdata/myservice.key create mode 100644 selfservice/strategy/saml/testdata/registration.schema.json create mode 100644 selfservice/strategy/saml/testdata/saml.jsonnet create mode 100644 selfservice/strategy/saml/testdata/saml_response.xml create mode 100755 selfservice/strategy/saml/testdata/samlkratos.crt create mode 100644 selfservice/strategy/saml/types.go create mode 100644 x/relaystate.go diff --git a/.schema/api.openapi.json b/.schema/api.openapi.json index fe62c9cda00c..8abef87e0478 100644 --- a/.schema/api.openapi.json +++ b/.schema/api.openapi.json @@ -1932,6 +1932,81 @@ ] } }, + "/self-service/saml/metadata":{ + "get":{ + "description": "This endpoint is for the IDP to obtain kratos metadata", + "operationId": "getSamlMetadata", + "response": { + "302":{ + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Expose metadata of the SAML Service Provider (Kratos)", + "tags": [ + "public" + ] + } + }, + "/self-service/saml/idp":{ + "get":{ + "description": "This endpoint is to redirect the user to the idp auth flow", + "operationId": "getUrlIdp", + "response": { + "302":{ + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Redirect the user to the IDP flow", + "tags": [ + "public" + ] + } + }, + "/self-service/saml/acs":{ + "get":{ + "description": "AssertionConsumerService : handle saml response from the IDP", + "operationId": "getSamlAcs", + "response": { + "302":{ + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Handle SAML response from the IDP", + "tags": [ + "public" + ] + } + }, "/self-service/login/browser": { "get": { "description": "This endpoint initializes a browser-based user login flow. Once initialized, the browser will be redirected to\n`selfservice.flows.login.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session\nexists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter\n`?refresh=true` was set.\n\nThis endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...).\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", diff --git a/.schema/openapi.json b/.schema/openapi.json index da9c0c742d55..1c049cd787cc 100644 --- a/.schema/openapi.json +++ b/.schema/openapi.json @@ -2006,6 +2006,81 @@ ] } }, + "/self-service/saml/idp":{ + "get":{ + "description": "This endpoint is to redirect the user to the idp auth flow", + "operationId": "getUrlIdp", + "response": { + "302":{ + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Redirect the user to the IDP flow", + "tags": [ + "public" + ] + } + }, + "/self-service/saml/metadata":{ + "get":{ + "description": "This endpoint is for the IDP to obtain kratos metadata", + "operationId": "getSamlMetadata", + "response": { + "302":{ + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Expose metadata of the SAML Service Provider (Kratos)", + "tags": [ + "public" + ] + } + }, + "/self-service/saml/acs":{ + "get":{ + "description": "AssertionConsumerService : handle saml response from the IDP", + "operationId": "getSamlAcs", + "response": { + "302":{ + "$ref": "#/components/responses/emptyResponse" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/genericError" + } + } + }, + "description": "genericError" + } + }, + "summary": "Handle SAML response from the IDP", + "tags": [ + "public" + ] + } + }, "/self-service/login/flows": { "get": { "description": "This endpoint returns a login flow's context with, for example, error details and other information.\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", diff --git a/continuity/manager.go b/continuity/manager.go index 65989704faa0..27541f32b381 100644 --- a/continuity/manager.go +++ b/continuity/manager.go @@ -20,6 +20,10 @@ type ManagementProvider interface { ContinuityManager() Manager } +type ManagementProviderRelayState interface { + RelayStateContinuityManager() Manager +} + type Manager interface { Pause(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) error Continue(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) (*Container, error) diff --git a/continuity/manager_relaystate.go b/continuity/manager_relaystate.go new file mode 100644 index 000000000000..5ce1dc7ffee7 --- /dev/null +++ b/continuity/manager_relaystate.go @@ -0,0 +1,151 @@ +package continuity + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +var _ Manager = new(ManagerRelayState) +var ErrNotResumableRelayState = *herodot.ErrBadRequest.WithError("no resumable session found").WithReason("The browser does not contain the necessary RelayState value to resume the session. This is a security violation and was blocked. Please try again!") + +type ( + managerRelayStateDependencies interface { + PersistenceProvider + x.RelayStateProvider + session.ManagementProvider + } + ManagerRelayState struct { + dr managerRelayStateDependencies + dc managerCookieDependencies + } +) + +// To ensure continuity even after redirection to the IDP, we cannot use cookies because the IDP and the SP are on two different domains. +// So we have to pass the continuity value through the relaystate. +// This value corresponds to the session ID +func NewManagerRelayState(dr managerRelayStateDependencies, dc managerCookieDependencies) *ManagerRelayState { + return &ManagerRelayState{dr: dr, dc: dc} +} + +func (m *ManagerRelayState) Pause(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) error { + if len(name) == 0 { + return errors.Errorf("continuity container name must be set") + } + + o, err := newManagerOptions(opts) + if err != nil { + return err + } + c := NewContainer(name, *o) + + // We have to put the continuity value in the cookie to ensure that value are passed between API and UI + // It is also useful to pass the value between SP and IDP with POST method because RelayState will take its value from cookie + if err = x.SessionPersistValues(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, map[string]interface{}{ + name: c.ID.String(), + }); err != nil { + return err + } + + if err := m.dr.ContinuityPersister().SaveContinuitySession(r.Context(), c); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (m *ManagerRelayState) Continue(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) (*Container, error) { + container, err := m.container(ctx, w, r, name) + if err != nil { + return nil, err + } + + o, err := newManagerOptions(opts) + if err != nil { + return nil, err + } + + if err := container.Valid(o.iid); err != nil { + return nil, err + } + + if o.payloadRaw != nil && container.Payload != nil { + if err := json.NewDecoder(bytes.NewBuffer(container.Payload)).Decode(o.payloadRaw); err != nil { + return nil, errors.WithStack(err) + } + } + + if err := x.SessionUnsetKey(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, name); err != nil { + return nil, err + } + + if err := m.dc.ContinuityPersister().DeleteContinuitySession(ctx, container.ID); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { + return nil, err + } + + return container, nil +} + +func (m *ManagerRelayState) sid(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) (uuid.UUID, error) { + var sid uuid.UUID + if s, err := x.SessionGetStringRelayState(r, m.dc.ContinuityCookieManager(ctx), CookieName, name); err != nil { + return sid, errors.WithStack(ErrNotResumable.WithDebugf("%+v", err)) + + } else if sid = x.ParseUUID(s); sid == uuid.Nil { + return sid, errors.WithStack(ErrNotResumable.WithDebug("session id is not a valid uuid")) + + } + + return sid, nil +} + +func (m *ManagerRelayState) container(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) (*Container, error) { + sid, err := m.sid(ctx, w, r, name) + if err != nil { + return nil, err + } + + container, err := m.dr.ContinuityPersister().GetContinuitySession(ctx, sid) + + if err != nil { + _ = x.SessionUnsetKey(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, name) + } + + if errors.Is(err, sqlcon.ErrNoRows) { + return nil, errors.WithStack(ErrNotResumable.WithDebug("Resumable ID from RelayState could not be found in the datastore")) + } else if err != nil { + return nil, err + } + + return container, err +} + +func (m ManagerRelayState) Abort(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) error { + sid, err := m.sid(ctx, w, r, name) + if errors.Is(err, &ErrNotResumable) { + // We do not care about an error here + return nil + } else if err != nil { + return err + } + + if err := x.SessionUnsetKey(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, name); err != nil { + return err + } + + if err := m.dr.ContinuityPersister().DeleteContinuitySession(ctx, sid); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { + return errors.WithStack(err) + } + + return nil +} diff --git a/contrib/quickstart/kratos/email-password/identity.schema.json b/contrib/quickstart/kratos/email-password/identity.schema.json old mode 100644 new mode 100755 index 1a137875666e..3b86e9eb1195 --- a/contrib/quickstart/kratos/email-password/identity.schema.json +++ b/contrib/quickstart/kratos/email-password/identity.schema.json @@ -1,3 +1,6 @@ +// 20221125150145 +// https://raw.githubusercontent.com/ory/kratos/master/contrib/quickstart/kratos/email-password/identity.schema.json + { "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", @@ -46,4 +49,4 @@ "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/contrib/quickstart/kratos/email-password/kratos.yml b/contrib/quickstart/kratos/email-password/kratos.yml old mode 100644 new mode 100755 index d435f894429c..1fcc785125ec --- a/contrib/quickstart/kratos/email-password/kratos.yml +++ b/contrib/quickstart/kratos/email-password/kratos.yml @@ -93,4 +93,4 @@ identity: courier: smtp: - connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true \ No newline at end of file diff --git a/driver/registry_default.go b/driver/registry_default.go index d199a4f901a1..78a5c5b69da0 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -67,6 +67,7 @@ import ( "github.com/ory/kratos/selfservice/flow/logout" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/saml" "github.com/ory/herodot" @@ -109,6 +110,9 @@ type RegistryDefault struct { continuityManager continuity.Manager + x.RelayStateProvider + session.ManagementProvider + schemaHandler *schema.Handler sessionHandler *session.Handler @@ -131,6 +135,8 @@ type RegistryDefault struct { selfserviceLoginHandler *login.Handler selfserviceLoginRequestErrorHandler *login.ErrorHandler + selfserviceSAMLHandler *saml.Handler + selfserviceSettingsHandler *settings.Handler selfserviceSettingsErrorHandler *settings.ErrorHandler selfserviceSettingsExecutor *settings.HookExecutor @@ -175,6 +181,7 @@ func (m *RegistryDefault) Audit() *logrusx.Logger { func (m *RegistryDefault) RegisterPublicRoutes(ctx context.Context, router *x.RouterPublic) { m.LoginHandler().RegisterPublicRoutes(router) + m.SAMLHandler().RegisterPublicRoutes(router) m.RegistrationHandler().RegisterPublicRoutes(router) m.LogoutHandler().RegisterPublicRoutes(router) m.SettingsHandler().RegisterPublicRoutes(router) @@ -313,6 +320,7 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} { m.selfserviceStrategies = []interface{}{ password2.NewStrategy(m), oidc.NewStrategy(m), + saml.NewStrategy(m), profile.NewStrategy(m), code.NewStrategy(m), link.NewStrategy(m), @@ -668,12 +676,25 @@ func (m *RegistryDefault) Courier(ctx context.Context) (courier.Courier, error) } func (m *RegistryDefault) ContinuityManager() continuity.Manager { - if m.continuityManager == nil { + // If m.continuityManager is nil or not a continuity.ManagerCookie + switch m.continuityManager.(type) { + case *continuity.ManagerCookie: + default: m.continuityManager = continuity.NewManagerCookie(m) } return m.continuityManager } +func (m *RegistryDefault) RelayStateContinuityManager() continuity.Manager { + // If m.continuityManager is nil or not a continuity.ManagerRelayState + switch m.continuityManager.(type) { + case *continuity.ManagerRelayState: + default: + m.continuityManager = continuity.NewManagerRelayState(m, m) + } + return m.continuityManager +} + func (m *RegistryDefault) ContinuityPersister() continuity.Persister { return m.persister } diff --git a/driver/registry_default_saml.go b/driver/registry_default_saml.go new file mode 100644 index 000000000000..23b619b0ec82 --- /dev/null +++ b/driver/registry_default_saml.go @@ -0,0 +1,11 @@ +package driver + +import "github.com/ory/kratos/selfservice/strategy/saml" + +func (m *RegistryDefault) SAMLHandler() *saml.Handler { + if m.selfserviceSAMLHandler == nil { + m.selfserviceSAMLHandler = saml.NewHandler(m) + } + + return m.selfserviceSAMLHandler +} diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 29661c02e538..0065e8f3b208 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -809,7 +809,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { _, reg := internal.NewFastRegistryWithMocks(t) t.Run("case=all login strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "totp", "webauthn", "lookup_secret"} + expects := []string{"password", "oidc", "saml", "totp", "webauthn", "lookup_secret"} s := reg.AllLoginStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -818,7 +818,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { }) t.Run("case=all registration strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "webauthn"} + expects := []string{"password", "oidc", "saml", "webauthn"} s := reg.AllRegistrationStrategies() require.Len(t, s, len(expects)) for k, e := range expects { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 330c87642be3..1e5113d77c3b 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -368,6 +368,224 @@ } } }, + "selfServiceSAMLProvider": { + "type": "object", + "properties": { + "id": { + "title":"ID of the IdentityProvider", + "type": "string", + "examples": [ + "activedirectory1" + ] + }, + "label": { + "title": "Optional string which will be used when generating labels for UI buttons.", + "type": "string", + "examples": [ + "Microsoft Active Directory" + ] + }, + "provider": { + "title": "Provider", + "description": "Can only be generic currently.", + "type": "string", + "enum": [ + "generic" + ], + "examples": [ + "generic" + ] + }, + "public_cert_path": { + "title": "Public Certificate Path", + "description": "The Public Certificate for your SAML Messages", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/cert", + "https://foo.bar.com/path/to/cert" + ] + }, + "private_key_path": { + "title": "Private Key Path", + "description": "The Private Key for your SAML Messages", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/key" + ] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to Ory Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "idp_information": { + "type": "object", + "properties": { + "idp_metadata_url": { + "title": "IDP Metadata URL", + "description": "The URL of the metadata of the IDP", + "type": "string", + "examples": [ + "https://path/to/metadata" + ] + }, + "idp_certificate_path": { + "title": "IDP Certificate Path", + "description": "The path to the certificate of the IDP", + "type": "string", + "examples": [ + "file://path/to/certificate", + "https://foo.bar.com/path/to/certificate" + ] + }, + "idp_logout_url": { + "title": "IDP Logout URL", + "description": "The URL of the Single Log Out (SLO) API of the IDP", + "type": "string", + "examples": [ + "https://path/to/logout" + ] + }, + "idp_sso_url": { + "title": "IDP SSO URL", + "description": "The URL of the SSO Handler at the IDP", + "type": "string", + "examples": [ + "https://path/to/sso" + ] + }, + "idp_entity_id": { + "title": "The EntityID of the IDP", + "description": "It is a unique identifier representing the IDP in saml requests", + "type": "string", + "examples": [ + "https://samltest.id/saml/idp" + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "idp_metadata_url": { + "const": {} + } + } + }, + "then": { + "required": [ + "idp_logout_url", + "idp_certificate_path", + "idp_entity_id" + ] + }, + "else":{ + "properties": { + "idp_certificate_path": { + "const": {} + }, + "idp_logout_url": { + "const": {} + }, + "idp_entity_id":{ + "const":{} + }, + "idp_sso_url":{ + "const":{} + } + } + } + } + ] + }, + "attributes_map": { + "type": "object", + "properties": { + "id": { + "title": "ID", + "description": "The name of the IDP SAML Assertion attribute that will represent the user ID on Kratos", + "type": "string", + "examples": [ + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" + ] + }, + "firstname": { + "title": "Firstname", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's firstname on Kratos", + "type": "string", + "examples": [ + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" + ] + }, + "lastname": { + "title": "Lastname", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's lastname on Kratos", + "type": "string", + "examples": [ + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" + ] + }, + "nickname": { + "title": "Nickname", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's username on Kratos", + "type": "string" + }, + "gender": { + "title": "Gender", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's gender on Kratos", + "type": "string" + }, + "birthdate": { + "title": "Birthdate", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's birthdate on Kratos", + "type": "string" + }, + "picture": { + "title": "Picture", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's picture on Kratos", + "type": "string" + }, + "email": { + "title": "Email", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's email on Kratos", + "type": "string", + "examples": [ + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" + ] + }, + "roles": { + "title": "Roles", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's roles on Kratos", + "type": "string", + "examples" : [ + "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" + ] + }, + "phone_number": { + "title": "Phone Number", + "description": "The name of the IDP SAML Assertion attribute that will represent the user's phone number on Kratos", + "type": "string" + } + } + } + }, + "additionalProperties": false, + "required": [ + "id", + "label", + "public_cert_path", + "private_key_path", + "mapper_url" + ] + }, "selfServiceOIDCProvider": { "type": "object", "properties": { @@ -874,6 +1092,9 @@ "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "saml": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "hooks": { "$ref": "#/definitions/selfServiceHooks" } @@ -1568,6 +1789,35 @@ ] } }, + "saml": { + "type": "object", + "title": "Specify SAML configuration", + "showEnvVarBlockForObject": true, + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables SAML Authentication Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "title": "SAML Provider", + "description": "All information required to implement a SAML authentication", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceSAMLProvider" + } + } + } + + + } + } + }, "oidc": { "type": "object", "title": "Specify OpenID Connect and OAuth2 Configuration", diff --git a/go.mod b/go.mod index 10e07483d99e..e000f539ceb6 100644 --- a/go.mod +++ b/go.mod @@ -24,12 +24,14 @@ require ( github.com/Masterminds/sprig/v3 v3.2.2 github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 github.com/avast/retry-go/v3 v3.1.1 + github.com/beevik/etree v1.1.0 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/bwmarrin/discordgo v0.23.0 github.com/bxcodec/faker/v3 v3.3.1 github.com/cenkalti/backoff v2.2.1+incompatible github.com/coreos/go-oidc v2.2.1+incompatible github.com/cortesi/modd v0.0.0-20210323234521-b35eddab86cc + github.com/crewjam/saml v0.4.6 github.com/davecgh/go-spew v1.1.1 github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 github.com/dgraph-io/ristretto v0.1.1 @@ -81,7 +83,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/rs/cors v1.8.2 - github.com/samber/lo v1.37.0 + github.com/russellhaering/goxmldsig v1.1.1 github.com/sirupsen/logrus v1.9.0 github.com/slack-go/slack v0.7.4 github.com/spf13/cobra v1.6.1 @@ -99,7 +101,8 @@ require ( golang.org/x/net v0.7.0 golang.org/x/oauth2 v0.4.0 golang.org/x/sync v0.1.0 - golang.org/x/tools v0.5.0 + golang.org/x/tools v0.2.0 + gotest.tools v2.2.0+incompatible ) require ( @@ -135,6 +138,7 @@ require ( github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e // indirect github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/crewjam/httperr v0.2.0 // indirect github.com/docker/cli v20.10.21+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker v20.10.21+incompatible // indirect @@ -183,6 +187,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/google/pprof v0.0.0-20221010195024-131d412537ea // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -227,6 +232,7 @@ require ( github.com/lib/pq v1.10.7 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect diff --git a/go.sum b/go.sum index c0e642be0835..aa34dd7e5d8c 100644 --- a/go.sum +++ b/go.sum @@ -164,8 +164,8 @@ github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAm github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -278,6 +278,10 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= +github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/crewjam/saml v0.4.6 h1:XCUFPkQSJLvzyl4cW9OvpWUbRf0gE7VUpU8ZnilbeM4= +github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= @@ -287,6 +291,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 h1:VzPvKOw28XJ77PYwOq5gAqvFB4gk6gst0HxxiW8kfZQ= github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6/go.mod h1:+6FzxsSbK4oEuvdN06Jco8zKB2mQqIB6UduZdd0Zesk= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= @@ -594,6 +599,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v27 v27.0.1 h1:sSMFSShNn4VnqCqs+qhab6TS3uQc+uVR6TD1bW6MavM= github.com/google/go-github/v27 v27.0.1/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= @@ -945,6 +951,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -1221,6 +1229,7 @@ github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= @@ -1229,6 +1238,8 @@ github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM= +github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -1424,6 +1435,7 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= @@ -2154,6 +2166,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.3.5/go.mod h1:EGCWefLFQSVFrHGy4J8EtiHCWX5Q8t0yz2Jt9aKkGzU= gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/identity/credentials.go b/identity/credentials.go index 7b1c485579b6..07a8389fc8d0 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -71,6 +71,7 @@ const ( CredentialsTypeTOTP CredentialsType = "totp" CredentialsTypeLookup CredentialsType = "lookup_secret" CredentialsTypeWebAuthn CredentialsType = "webauthn" + CredentialsTypeSAML CredentialsType = "saml" ) const ( diff --git a/identity/credentials_saml.go b/identity/credentials_saml.go new file mode 100644 index 000000000000..c420be179f1f --- /dev/null +++ b/identity/credentials_saml.go @@ -0,0 +1,51 @@ +package identity + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + + "github.com/ory/kratos/x" +) + +// CredentialsSAML is contains the configuration for credentials of the type SAML. +// +// swagger:model identityCredentialsSAML +type CredentialsSAML struct { + Providers []CredentialsSAMLProvider `json:"providers"` +} + +// CredentialsSAMLProvider is contains a specific SAML credential for a particular connection (e.g. Google). +// +// swagger:model identityCredentialsSamlProvider +type CredentialsSAMLProvider struct { + Subject string `json:"subject"` + Provider string `json:"samlProvider"` +} + +// Create an uniq identifier for user in database. Its look like "id + the id of the saml provider" +func NewCredentialsSAML(subject string, provider string) (*Credentials, error) { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(CredentialsSAML{ + Providers: []CredentialsSAMLProvider{ + { + Subject: subject, + Provider: provider, + }}, + }); err != nil { + return nil, errors.WithStack(x.PseudoPanic. + WithDebugf("Unable to encode password options to JSON: %s", err)) + } + + return &Credentials{ + Type: CredentialsTypeSAML, + Identifiers: []string{SAMLUniqueID(provider, subject)}, + Config: b.Bytes(), + }, nil +} + +func SAMLUniqueID(provider, subject string) string { + return fmt.Sprintf("%s:%s", provider, subject) +} diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index d11e26721f68..072fd5d1de0d 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -63,6 +63,18 @@ docs/RecoveryIdentityAddress.md docs/RecoveryLinkForIdentity.md docs/RegistrationFlow.md docs/SelfServiceFlowExpiredError.md +docs/SelfServiceLoginFlow.md +docs/SelfServiceLogoutUrl.md +docs/SelfServiceRecoveryCode.md +docs/SelfServiceRecoveryFlow.md +docs/SelfServiceRecoveryFlowState.md +docs/SelfServiceRecoveryLink.md +docs/SelfServiceRegistrationFlow.md +docs/SelfServiceSamlUrl.md +docs/SelfServiceSettingsFlow.md +docs/SelfServiceSettingsFlowState.md +docs/SelfServiceVerificationFlow.md +docs/SelfServiceVerificationFlowState.md docs/Session.md docs/SessionAuthenticationMethod.md docs/SessionDevice.md @@ -163,6 +175,18 @@ model_recovery_identity_address.go model_recovery_link_for_identity.go model_registration_flow.go model_self_service_flow_expired_error.go +model_self_service_login_flow.go +model_self_service_logout_url.go +model_self_service_recovery_code.go +model_self_service_recovery_flow.go +model_self_service_recovery_flow_state.go +model_self_service_recovery_link.go +model_self_service_registration_flow.go +model_self_service_saml_url.go +model_self_service_settings_flow.go +model_self_service_settings_flow_state.go +model_self_service_verification_flow.go +model_self_service_verification_flow_state.go model_session.go model_session_authentication_method.go model_session_device.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 25fe3c7e244d..679646eba1ee 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -130,6 +130,50 @@ Class | Method | HTTP request | Description *MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. *MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status *MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status +*V0alpha2Api* | [**AdminCreateIdentity**](docs/V0alpha2Api.md#admincreateidentity) | **Post** /admin/identities | Create an Identity +*V0alpha2Api* | [**AdminCreateSelfServiceRecoveryCode**](docs/V0alpha2Api.md#admincreateselfservicerecoverycode) | **Post** /admin/recovery/code | Create a Recovery Code +*V0alpha2Api* | [**AdminCreateSelfServiceRecoveryLink**](docs/V0alpha2Api.md#admincreateselfservicerecoverylink) | **Post** /admin/recovery/link | Create a Recovery Link +*V0alpha2Api* | [**AdminDeleteIdentity**](docs/V0alpha2Api.md#admindeleteidentity) | **Delete** /admin/identities/{id} | Delete an Identity +*V0alpha2Api* | [**AdminDeleteIdentitySessions**](docs/V0alpha2Api.md#admindeleteidentitysessions) | **Delete** /admin/identities/{id}/sessions | Delete & Invalidate an Identity's Sessions +*V0alpha2Api* | [**AdminExtendSession**](docs/V0alpha2Api.md#adminextendsession) | **Patch** /admin/sessions/{id}/extend | Extend a Session +*V0alpha2Api* | [**AdminGetIdentity**](docs/V0alpha2Api.md#admingetidentity) | **Get** /admin/identities/{id} | Get an Identity +*V0alpha2Api* | [**AdminListCourierMessages**](docs/V0alpha2Api.md#adminlistcouriermessages) | **Get** /admin/courier/messages | List Messages +*V0alpha2Api* | [**AdminListIdentities**](docs/V0alpha2Api.md#adminlistidentities) | **Get** /admin/identities | List Identities +*V0alpha2Api* | [**AdminListIdentitySessions**](docs/V0alpha2Api.md#adminlistidentitysessions) | **Get** /admin/identities/{id}/sessions | List an Identity's Sessions +*V0alpha2Api* | [**AdminPatchIdentity**](docs/V0alpha2Api.md#adminpatchidentity) | **Patch** /admin/identities/{id} | Patch an Identity +*V0alpha2Api* | [**AdminUpdateIdentity**](docs/V0alpha2Api.md#adminupdateidentity) | **Put** /admin/identities/{id} | Update an Identity +*V0alpha2Api* | [**CreateSelfServiceLogoutFlowUrlForBrowsers**](docs/V0alpha2Api.md#createselfservicelogoutflowurlforbrowsers) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers +*V0alpha2Api* | [**GetIdentitySchema**](docs/V0alpha2Api.md#getidentityschema) | **Get** /schemas/{id} | +*V0alpha2Api* | [**GetSelfServiceError**](docs/V0alpha2Api.md#getselfserviceerror) | **Get** /self-service/errors | Get Self-Service Errors +*V0alpha2Api* | [**GetSelfServiceLoginFlow**](docs/V0alpha2Api.md#getselfserviceloginflow) | **Get** /self-service/login/flows | Get Login Flow +*V0alpha2Api* | [**GetSelfServiceRecoveryFlow**](docs/V0alpha2Api.md#getselfservicerecoveryflow) | **Get** /self-service/recovery/flows | Get Recovery Flow +*V0alpha2Api* | [**GetSelfServiceRegistrationFlow**](docs/V0alpha2Api.md#getselfserviceregistrationflow) | **Get** /self-service/registration/flows | Get Registration Flow +*V0alpha2Api* | [**GetSelfServiceSettingsFlow**](docs/V0alpha2Api.md#getselfservicesettingsflow) | **Get** /self-service/settings/flows | Get Settings Flow +*V0alpha2Api* | [**GetSelfServiceVerificationFlow**](docs/V0alpha2Api.md#getselfserviceverificationflow) | **Get** /self-service/verification/flows | Get Verification Flow +*V0alpha2Api* | [**GetWebAuthnJavaScript**](docs/V0alpha2Api.md#getwebauthnjavascript) | **Get** /.well-known/ory/webauthn.js | Get WebAuthn JavaScript +*V0alpha2Api* | [**InitializeSelfServiceLoginFlowForBrowsers**](docs/V0alpha2Api.md#initializeselfserviceloginflowforbrowsers) | **Get** /self-service/login/browser | Initialize Login Flow for Browsers +*V0alpha2Api* | [**InitializeSelfServiceLoginFlowWithoutBrowser**](docs/V0alpha2Api.md#initializeselfserviceloginflowwithoutbrowser) | **Get** /self-service/login/api | Initialize Login Flow for APIs, Services, Apps, ... +*V0alpha2Api* | [**InitializeSelfServiceRecoveryFlowForBrowsers**](docs/V0alpha2Api.md#initializeselfservicerecoveryflowforbrowsers) | **Get** /self-service/recovery/browser | Initialize Recovery Flow for Browsers +*V0alpha2Api* | [**InitializeSelfServiceRecoveryFlowWithoutBrowser**](docs/V0alpha2Api.md#initializeselfservicerecoveryflowwithoutbrowser) | **Get** /self-service/recovery/api | Initialize Recovery Flow for APIs, Services, Apps, ... +*V0alpha2Api* | [**InitializeSelfServiceRegistrationFlowForBrowsers**](docs/V0alpha2Api.md#initializeselfserviceregistrationflowforbrowsers) | **Get** /self-service/registration/browser | Initialize Registration Flow for Browsers +*V0alpha2Api* | [**InitializeSelfServiceRegistrationFlowWithoutBrowser**](docs/V0alpha2Api.md#initializeselfserviceregistrationflowwithoutbrowser) | **Get** /self-service/registration/api | Initialize Registration Flow for APIs, Services, Apps, ... +*V0alpha2Api* | [**InitializeSelfServiceSamlFlowForBrowsers**](docs/V0alpha2Api.md#initializeselfservicesamlflowforbrowsers) | **Get** /self-service/methods/saml/auth | Initialize Registration Flow for APIs, Services, Apps, ... +*V0alpha2Api* | [**InitializeSelfServiceSettingsFlowForBrowsers**](docs/V0alpha2Api.md#initializeselfservicesettingsflowforbrowsers) | **Get** /self-service/settings/browser | Initialize Settings Flow for Browsers +*V0alpha2Api* | [**InitializeSelfServiceSettingsFlowWithoutBrowser**](docs/V0alpha2Api.md#initializeselfservicesettingsflowwithoutbrowser) | **Get** /self-service/settings/api | Initialize Settings Flow for APIs, Services, Apps, ... +*V0alpha2Api* | [**InitializeSelfServiceVerificationFlowForBrowsers**](docs/V0alpha2Api.md#initializeselfserviceverificationflowforbrowsers) | **Get** /self-service/verification/browser | Initialize Verification Flow for Browser Clients +*V0alpha2Api* | [**InitializeSelfServiceVerificationFlowWithoutBrowser**](docs/V0alpha2Api.md#initializeselfserviceverificationflowwithoutbrowser) | **Get** /self-service/verification/api | Initialize Verification Flow for APIs, Services, Apps, ... +*V0alpha2Api* | [**ListIdentitySchemas**](docs/V0alpha2Api.md#listidentityschemas) | **Get** /schemas | +*V0alpha2Api* | [**ListSessions**](docs/V0alpha2Api.md#listsessions) | **Get** /sessions | Get Active Sessions +*V0alpha2Api* | [**RevokeSession**](docs/V0alpha2Api.md#revokesession) | **Delete** /sessions/{id} | Invalidate a Session +*V0alpha2Api* | [**RevokeSessions**](docs/V0alpha2Api.md#revokesessions) | **Delete** /sessions | Invalidate all Other Sessions +*V0alpha2Api* | [**SubmitSelfServiceLoginFlow**](docs/V0alpha2Api.md#submitselfserviceloginflow) | **Post** /self-service/login | Submit a Login Flow +*V0alpha2Api* | [**SubmitSelfServiceLogoutFlow**](docs/V0alpha2Api.md#submitselfservicelogoutflow) | **Get** /self-service/logout | Complete Self-Service Logout +*V0alpha2Api* | [**SubmitSelfServiceLogoutFlowWithoutBrowser**](docs/V0alpha2Api.md#submitselfservicelogoutflowwithoutbrowser) | **Delete** /self-service/logout/api | Perform Logout for APIs, Services, Apps, ... +*V0alpha2Api* | [**SubmitSelfServiceRecoveryFlow**](docs/V0alpha2Api.md#submitselfservicerecoveryflow) | **Post** /self-service/recovery | Complete Recovery Flow +*V0alpha2Api* | [**SubmitSelfServiceRegistrationFlow**](docs/V0alpha2Api.md#submitselfserviceregistrationflow) | **Post** /self-service/registration | Submit a Registration Flow +*V0alpha2Api* | [**SubmitSelfServiceSettingsFlow**](docs/V0alpha2Api.md#submitselfservicesettingsflow) | **Post** /self-service/settings | Complete Settings Flow +*V0alpha2Api* | [**SubmitSelfServiceVerificationFlow**](docs/V0alpha2Api.md#submitselfserviceverificationflow) | **Post** /self-service/verification | Complete Verification Flow +*V0alpha2Api* | [**ToSession**](docs/V0alpha2Api.md#tosession) | **Get** /sessions/whoami | Check Who the Current HTTP Session Belongs To ## Documentation For Models @@ -184,6 +228,18 @@ Class | Method | HTTP request | Description - [RecoveryLinkForIdentity](docs/RecoveryLinkForIdentity.md) - [RegistrationFlow](docs/RegistrationFlow.md) - [SelfServiceFlowExpiredError](docs/SelfServiceFlowExpiredError.md) + - [SelfServiceLoginFlow](docs/SelfServiceLoginFlow.md) + - [SelfServiceLogoutUrl](docs/SelfServiceLogoutUrl.md) + - [SelfServiceRecoveryCode](docs/SelfServiceRecoveryCode.md) + - [SelfServiceRecoveryFlow](docs/SelfServiceRecoveryFlow.md) + - [SelfServiceRecoveryFlowState](docs/SelfServiceRecoveryFlowState.md) + - [SelfServiceRecoveryLink](docs/SelfServiceRecoveryLink.md) + - [SelfServiceRegistrationFlow](docs/SelfServiceRegistrationFlow.md) + - [SelfServiceSamlUrl](docs/SelfServiceSamlUrl.md) + - [SelfServiceSettingsFlow](docs/SelfServiceSettingsFlow.md) + - [SelfServiceSettingsFlowState](docs/SelfServiceSettingsFlowState.md) + - [SelfServiceVerificationFlow](docs/SelfServiceVerificationFlow.md) + - [SelfServiceVerificationFlowState](docs/SelfServiceVerificationFlowState.md) - [Session](docs/Session.md) - [SessionAuthenticationMethod](docs/SessionAuthenticationMethod.md) - [SessionDevice](docs/SessionDevice.md) diff --git a/internal/httpclient/docs/SelfServiceSamlUrl.md b/internal/httpclient/docs/SelfServiceSamlUrl.md new file mode 100644 index 000000000000..c7f22694c63c --- /dev/null +++ b/internal/httpclient/docs/SelfServiceSamlUrl.md @@ -0,0 +1,72 @@ +# SelfServiceSamlUrl + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**SamlAcsUrl** | **string** | SamlAcsURL is a post endpoint to handle SAML Response format: uri | +**SamlMetadataUrl** | **string** | SamlMetadataURL is a get endpoint to get the metadata format: uri | + +## Methods + +### NewSelfServiceSamlUrl + +`func NewSelfServiceSamlUrl(samlAcsUrl string, samlMetadataUrl string, ) *SelfServiceSamlUrl` + +NewSelfServiceSamlUrl instantiates a new SelfServiceSamlUrl object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewSelfServiceSamlUrlWithDefaults + +`func NewSelfServiceSamlUrlWithDefaults() *SelfServiceSamlUrl` + +NewSelfServiceSamlUrlWithDefaults instantiates a new SelfServiceSamlUrl object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetSamlAcsUrl + +`func (o *SelfServiceSamlUrl) GetSamlAcsUrl() string` + +GetSamlAcsUrl returns the SamlAcsUrl field if non-nil, zero value otherwise. + +### GetSamlAcsUrlOk + +`func (o *SelfServiceSamlUrl) GetSamlAcsUrlOk() (*string, bool)` + +GetSamlAcsUrlOk returns a tuple with the SamlAcsUrl field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSamlAcsUrl + +`func (o *SelfServiceSamlUrl) SetSamlAcsUrl(v string)` + +SetSamlAcsUrl sets SamlAcsUrl field to given value. + + +### GetSamlMetadataUrl + +`func (o *SelfServiceSamlUrl) GetSamlMetadataUrl() string` + +GetSamlMetadataUrl returns the SamlMetadataUrl field if non-nil, zero value otherwise. + +### GetSamlMetadataUrlOk + +`func (o *SelfServiceSamlUrl) GetSamlMetadataUrlOk() (*string, bool)` + +GetSamlMetadataUrlOk returns a tuple with the SamlMetadataUrl field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSamlMetadataUrl + +`func (o *SelfServiceSamlUrl) SetSamlMetadataUrl(v string)` + +SetSamlMetadataUrl sets SamlMetadataUrl field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/model_self_service_saml_url.go b/internal/httpclient/model_self_service_saml_url.go new file mode 100644 index 000000000000..3194edbb4031 --- /dev/null +++ b/internal/httpclient/model_self_service_saml_url.go @@ -0,0 +1,138 @@ +/* + * Ory Kratos API + * + * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * + * API version: 1.0.0 + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// SelfServiceSamlUrl struct for SelfServiceSamlUrl +type SelfServiceSamlUrl struct { + // SamlAcsURL is a post endpoint to handle SAML Response format: uri + SamlAcsUrl string `json:"saml_acs_url"` + // SamlMetadataURL is a get endpoint to get the metadata format: uri + SamlMetadataUrl string `json:"saml_metadata_url"` +} + +// NewSelfServiceSamlUrl instantiates a new SelfServiceSamlUrl object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSelfServiceSamlUrl(samlAcsUrl string, samlMetadataUrl string) *SelfServiceSamlUrl { + this := SelfServiceSamlUrl{} + this.SamlAcsUrl = samlAcsUrl + this.SamlMetadataUrl = samlMetadataUrl + return &this +} + +// NewSelfServiceSamlUrlWithDefaults instantiates a new SelfServiceSamlUrl object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSelfServiceSamlUrlWithDefaults() *SelfServiceSamlUrl { + this := SelfServiceSamlUrl{} + return &this +} + +// GetSamlAcsUrl returns the SamlAcsUrl field value +func (o *SelfServiceSamlUrl) GetSamlAcsUrl() string { + if o == nil { + var ret string + return ret + } + + return o.SamlAcsUrl +} + +// GetSamlAcsUrlOk returns a tuple with the SamlAcsUrl field value +// and a boolean to check if the value has been set. +func (o *SelfServiceSamlUrl) GetSamlAcsUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SamlAcsUrl, true +} + +// SetSamlAcsUrl sets field value +func (o *SelfServiceSamlUrl) SetSamlAcsUrl(v string) { + o.SamlAcsUrl = v +} + +// GetSamlMetadataUrl returns the SamlMetadataUrl field value +func (o *SelfServiceSamlUrl) GetSamlMetadataUrl() string { + if o == nil { + var ret string + return ret + } + + return o.SamlMetadataUrl +} + +// GetSamlMetadataUrlOk returns a tuple with the SamlMetadataUrl field value +// and a boolean to check if the value has been set. +func (o *SelfServiceSamlUrl) GetSamlMetadataUrlOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SamlMetadataUrl, true +} + +// SetSamlMetadataUrl sets field value +func (o *SelfServiceSamlUrl) SetSamlMetadataUrl(v string) { + o.SamlMetadataUrl = v +} + +func (o SelfServiceSamlUrl) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["saml_acs_url"] = o.SamlAcsUrl + } + if true { + toSerialize["saml_metadata_url"] = o.SamlMetadataUrl + } + return json.Marshal(toSerialize) +} + +type NullableSelfServiceSamlUrl struct { + value *SelfServiceSamlUrl + isSet bool +} + +func (v NullableSelfServiceSamlUrl) Get() *SelfServiceSamlUrl { + return v.value +} + +func (v *NullableSelfServiceSamlUrl) Set(val *SelfServiceSamlUrl) { + v.value = val + v.isSet = true +} + +func (v NullableSelfServiceSamlUrl) IsSet() bool { + return v.isSet +} + +func (v *NullableSelfServiceSamlUrl) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSelfServiceSamlUrl(val *SelfServiceSamlUrl) *NullableSelfServiceSamlUrl { + return &NullableSelfServiceSamlUrl{value: val, isSet: true} +} + +func (v NullableSelfServiceSamlUrl) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSelfServiceSamlUrl) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.down.sql b/persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.down.sql new file mode 100644 index 000000000000..baacd751eade --- /dev/null +++ b/persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'saml'; diff --git a/persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.up.sql b/persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.up.sql new file mode 100644 index 000000000000..b58c212cb0d3 --- /dev/null +++ b/persistence/sql/migrations/sql/2022080818000000000_identity_credentials_types_saml.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT 'ff5a1823-8b47-4255-860f-4b70ed122740', 'saml' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'saml'); diff --git a/postgres.yaml b/postgres.yaml new file mode 100644 index 000000000000..54bfdba40d26 --- /dev/null +++ b/postgres.yaml @@ -0,0 +1,33 @@ +version: '3.7' + +services: + postgresd: + image: postgres:9.6 + ports: + - "5432:5432" + environment: + - POSTGRES_USER=kratos + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=kratos + networks: + - intranet + + + + kratos-migrate: + depends_on: + - postgresd + image: oryd/kratos:latest + environment: + - DSN=postgres://kratos:secret@postgresd:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + volumes: + - type: bind + source: ./contrib/quickstart/kratos/email-password + target: /etc/config/kratos + command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes + restart: on-failure + networks: + - intranet + +networks: + intranet: \ No newline at end of file diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 1b6c43e15a32..165817d18f99 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -82,7 +82,6 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login if errors.Is(err, sqlcon.ErrNoRows) { // If no account was found we're "manually" creating a new registration flow and redirecting the browser // to that endpoint. - // That will execute the "pre registration" hook which allows to e.g. disallow this request. The registration // ui however will NOT be shown, instead the user is directly redirected to the auth path. That should then // do a silent re-request. While this might be a bit excessive from a network perspective it should usually diff --git a/selfservice/strategy/saml/.schema/link.schema.json b/selfservice/strategy/saml/.schema/link.schema.json new file mode 100644 index 000000000000..95c72b83ed18 --- /dev/null +++ b/selfservice/strategy/saml/.schema/link.schema.json @@ -0,0 +1,17 @@ +{ + "$id": "file://.schema/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "samlProvider": { + "type": "string", + "minLength": 1 + }, + "traits": { + "description": "DO NOT DELETE THIS FIELD. This field will be overwritten in login.go's and registration.go's decoder() method. Do not add anything to this field as it has no effect." + } + } +} diff --git a/selfservice/strategy/saml/.schema/settings.schema.json b/selfservice/strategy/saml/.schema/settings.schema.json new file mode 100644 index 000000000000..3cac10cea831 --- /dev/null +++ b/selfservice/strategy/saml/.schema/settings.schema.json @@ -0,0 +1,19 @@ +{ + "$id": "file://.schema/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "link": { + "type": "string" + }, + "csrf_token": { + "type": "string" + }, + "unlink": { + "type": "string" + }, + "traits": { + "description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect." + } + } +} diff --git a/selfservice/strategy/saml/config_test.go b/selfservice/strategy/saml/config_test.go new file mode 100644 index 000000000000..d69274920aef --- /dev/null +++ b/selfservice/strategy/saml/config_test.go @@ -0,0 +1,401 @@ +package saml_test + +import ( + "context" + "io/ioutil" + "testing" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/kratos/x" + "github.com/stretchr/testify/assert" +) + +func TestInitSAMLWithoutProvider(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" + idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" + idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + + // Initiates without service provider + ViperSetProviderConfig( + t, + conf, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "Please indicate a SAML Identity Provider in your configuration file") +} + +func TestInitSAMLWithoutPoviderID(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" + idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" + idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "", + Label: "samlProviderLabel", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + AttributesMap: attributesMap, + IDPInformation: idpInformation, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "\"code\":404,\"status\":\"Not Found\"") +} + +func TestInitSAMLWithoutPoviderLabel(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" + idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" + idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "samlProvider", + Label: "", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + AttributesMap: attributesMap, + IDPInformation: idpInformation, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "Provider must have a label") +} + +func TestAttributesMapWithoutID(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" + idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" + idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "samlProvider", + Label: "samlProviderLabel", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + AttributesMap: attributesMap, + IDPInformation: idpInformation, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "You must have an ID field in your attribute_map") + +} + +func TestAttributesMapWithAnExtraField(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["evil"] = "evil" // Extra field + attributesMap["email"] = "mail" + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" + idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" + idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "samlProvider", + Label: "samlProviderLabel", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + AttributesMap: attributesMap, + IDPInformation: idpInformation, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "metadata") + +} + +func TestInitSAMLWithoutIDPInformation(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "samlProvider", + Label: "samlProviderLabel", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + AttributesMap: attributesMap, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "Please include your Identity Provider information in the configuration file.") +} + +func TestInitSAMLWithMissingIDPInformationField(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" + idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "samlProvider", + Label: "samlProviderLabel", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + IDPInformation: idpInformation, + AttributesMap: attributesMap, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "Please check your IDP information in the configuration file") +} + +func TestInitSAMLWithExtraIDPInformationField(t *testing.T) { + saml.DestroyMiddlewareIfExists("samlProvider") + + conf, reg := internal.NewFastRegistryWithMocks(t) + //strategy := saml.NewStrategy(reg) + errTS := testhelpers.NewErrorTestServer(t, reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" + idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" + idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" + idpInformation["evil"] = "evil" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "samlProvider", + Label: "samlProviderLabel", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + IDPInformation: idpInformation, + AttributesMap: attributesMap, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + t.Logf("Kratos Error URL: %s", errTS.URL) + + resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := ioutil.ReadAll(resp.Body) + assert.Contains(t, string(body), "Please check your IDP information in the configuration file") +} diff --git a/selfservice/strategy/saml/const.go b/selfservice/strategy/saml/const.go new file mode 100644 index 000000000000..43e852245103 --- /dev/null +++ b/selfservice/strategy/saml/const.go @@ -0,0 +1,5 @@ +package saml + +const ( + sessionName = "ory_kratos_saml_auth_code_session" +) diff --git a/selfservice/strategy/saml/error.go b/selfservice/strategy/saml/error.go new file mode 100644 index 000000000000..ab984670e1a0 --- /dev/null +++ b/selfservice/strategy/saml/error.go @@ -0,0 +1,16 @@ +package saml + +import "github.com/ory/herodot" + +var ( + ErrScopeMissing = herodot.ErrBadRequest. + WithError("authentication failed because a required scope was not granted"). + WithReason(`Unable to finish because one or more permissions were not granted. Please retry and accept all permissions.`) + + ErrIDTokenMissing = herodot.ErrBadRequest. + WithError("authentication failed because id_token is missing"). + WithReason(`Authentication failed because no id_token was returned. Please accept the "openid" permission and try again.`) + + ErrAPIFlowNotSupported = herodot.ErrBadRequest.WithError("API-based flows are not supported for this method"). + WithReason("SAML SignIn and Registeration are only supported for flows initiated using the Browser endpoint.") +) diff --git a/selfservice/strategy/saml/handler.go b/selfservice/strategy/saml/handler.go new file mode 100644 index 000000000000..1e191a872202 --- /dev/null +++ b/selfservice/strategy/saml/handler.go @@ -0,0 +1,400 @@ +package saml + +import ( + "bytes" + "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "encoding/xml" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/pkg/errors" + dsig "github.com/russellhaering/goxmldsig" + + "github.com/crewjam/saml/samlsp" + "github.com/julienschmidt/httprouter" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/errorx" + + samlidp "github.com/crewjam/saml" + + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "github.com/ory/x/jsonx" +) + +var ErrNoSession = errors.New("saml: session not present") + +var samlMiddlewares = make(map[string]*samlsp.Middleware) + +type ory_kratos_continuity struct{} + +type ( + handlerDependencies interface { + x.WriterProvider + x.CSRFProvider + session.ManagementProvider + session.PersistenceProvider + errorx.ManagementProvider + config.Provider + } + HandlerProvider interface { + LogoutHandler() *Handler + } + Handler struct { + d handlerDependencies + dx *decoderx.HTTP + } +) + +type SessionData struct { + SessionID string +} + +func NewHandler(d handlerDependencies) *Handler { + return &Handler{ + d: d, + dx: decoderx.NewHTTP(), + } +} + +func (h *Handler) RegisterPublicRoutes(router *x.RouterPublic) { + h.d.CSRFHandler().IgnoreGlob(RouteBaseAcs + "/*") + + router.GET(RouteMetadata, h.serveMetadata) + router.GET(RouteAuth, h.loginWithIdp) +} + +// Handle /selfservice/methods/saml/metadata +func (h *Handler) serveMetadata(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + config := h.d.Config() + pid := ps.ByName("provider") + + if samlMiddlewares[pid] == nil { + if err := h.instantiateMiddleware(r.Context(), *config, pid); err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + buf, _ := xml.MarshalIndent(samlMiddlewares[pid].ServiceProvider.Metadata(), "", " ") + w.Header().Set("Content-Type", "text/xml") + w.Write(buf) +} + +// swagger:route GET /self-service/methods/saml/auth v0alpha2 initializeSelfServiceSamlFlowForBrowsers +// +// Initialize Authentication Flow for SAML (Either the login or the register) +// +// If you already have a session, it will redirect you to the main page. +// +// Schemes: http, https +// +// Responses: +// 200: selfServiceRegistrationFlow +// 400: jsonError +// 500: jsonError +func (h *Handler) loginWithIdp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + // Middleware is a singleton so we have to verify that it exists + config := h.d.Config() + pid := ps.ByName("provider") + + if samlMiddlewares[pid] == nil { + if err := h.instantiateMiddleware(r.Context(), *config, pid); err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + // We have to get the SessionID from the cookie to inject it into the context to ensure continuity + cookie, err := r.Cookie(continuity.CookieName) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } + body, _ := ioutil.ReadAll(r.Body) + r2 := r.Clone(context.WithValue(r.Context(), ory_kratos_continuity{}, cookie.Value)) + r2.Body = ioutil.NopCloser(bytes.NewReader(body)) + *r = *r2 + + // Checks if the user already have an active session + if e := new(session.ErrNoActiveSessionFound); errors.As(e, &e) { + // No session exists yet, we start the auth flow and create the session + samlMiddlewares[pid].HandleStartAuthFlow(w, r) + } else { + // A session already exist, we redirect to the main page + http.Redirect(w, r, config.SelfServiceBrowserDefaultReturnTo(r.Context()).Path, http.StatusTemporaryRedirect) + } +} + +func DestroyMiddlewareIfExists(pid string) { + if samlMiddlewares[pid] != nil { + samlMiddlewares[pid] = nil + } +} + +// Instantiate the middleware SAML from the information in the configuration file +func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Config, pid string) error { + + providerConfig, err := CreateSAMLProviderConfig(config, ctx, pid) + if err != nil { + return err + } + + // Key pair to encrypt and sign SAML requests + keyPair, err := tls.LoadX509KeyPair(strings.Replace(providerConfig.PublicCertPath, "file://", "", 1), strings.Replace(providerConfig.PrivateKeyPath, "file://", "", 1)) + if err != nil { + return err + } + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + return err + } + + var idpMetadata *samlidp.EntityDescriptor + + // We check if the metadata file is provided + if providerConfig.IDPInformation["idp_metadata_url"] != "" { + + // The metadata file is provided + metadataURL := providerConfig.IDPInformation["idp_metadata_url"] + if strings.HasPrefix(metadataURL, "file://") { + metadataURL = strings.Replace(metadataURL, "file://", "", 1) + metadataURL = filepath.Clean(metadataURL) + metadataPlainText, err := ioutil.ReadFile(metadataURL) + if err != nil { + return err + } + + idpMetadata, err = samlsp.ParseMetadata([]byte(metadataPlainText)) + if err != nil { + return err + } + + } else { + idpMetadataURL, err := url.Parse(metadataURL) + if err != nil { + return err + } + // Parse the content of metadata file into a Golang struct + idpMetadata, err = samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL) + if err != nil { + return err + } + } + + } else { + // The metadata file is not provided + // So were are creating a minimalist IDP metadata based on what is provided by the user on the config file + entityIDURL, err := url.Parse(providerConfig.IDPInformation["idp_entity_id"]) + if err != nil { + return err + } + + // The IDP SSO URL + IDPSSOURL, err := url.Parse(providerConfig.IDPInformation["idp_sso_url"]) + if err != nil { + return err + } + + // The IDP Logout URL + IDPlogoutURL, err := url.Parse(providerConfig.IDPInformation["idp_logout_url"]) + if err != nil { + return err + } + + // The certificate of the IDP + certificate, err := ioutil.ReadFile(strings.Replace(providerConfig.IDPInformation["idp_certificate_path"], "file://", "", 1)) + if err != nil { + return err + } + + // We parse it into a x509.Certificate object + IDPCertificate, err := MustParseCertificate(certificate) + if err != nil { + return err + } + + // Because the metadata file is not provided, we need to simulate an IDP to create artificial metadata from the data entered in the conf file + tempIDP := samlidp.IdentityProvider{ + Key: nil, + Certificate: IDPCertificate, + Logger: nil, + MetadataURL: *entityIDURL, + SSOURL: *IDPSSOURL, + LogoutURL: *IDPlogoutURL, + } + + // Now we assign our reconstructed metadata to our SP + idpMetadata = tempIDP.Metadata() + } + + // The main URL + rootURL, err := url.Parse(config.SelfServiceBrowserDefaultReturnTo(ctx).String()) + if err != nil { + return err + } + + // Here we create a MiddleWare to transform Kratos into a Service Provider + samlMiddleWare, err := samlsp.New(samlsp.Options{ + URL: *rootURL, + Key: keyPair.PrivateKey.(*rsa.PrivateKey), + Certificate: keyPair.Leaf, + IDPMetadata: idpMetadata, + SignRequest: true, + // We have to replace the ContinuityCookie by using RelayState. We will pass the SessionID (uuid) of Kratos through RelayState + RelayStateFunc: func(w http.ResponseWriter, r *http.Request) string { + ctx := r.Context() + cipheredCookie, ok := ctx.Value(ory_kratos_continuity{}).(string) + if !ok { + _, err := w.Write([]byte("No SessionID in current context")) + if err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } + return "" + } + return cipheredCookie + }, + }) + if err != nil { + return err + } + + // It's better to use SHA256 than SHA1 + samlMiddleWare.ServiceProvider.SignatureMethod = dsig.RSASHA256SignatureMethod + + var publicUrlString = config.SelfPublicURL(ctx).String() + + // Sometimes there is an issue with double slash into the url so we prevent it + // Crewjam library use default route for ACS and metadata but we want to overwrite them + RouteSamlAcsWithSlash := strings.Replace(RouteAcs, ":provider", providerConfig.ID, 1) + if publicUrlString[len(publicUrlString)-1] != '/' { + + u, err := url.Parse(publicUrlString + RouteSamlAcsWithSlash) + if err != nil { + return err + } + samlMiddleWare.ServiceProvider.AcsURL = *u + + } else if publicUrlString[len(publicUrlString)-1] == '/' { + + publicUrlString = publicUrlString[:len(publicUrlString)-1] + u, err := url.Parse(publicUrlString + RouteSamlAcsWithSlash) + if err != nil { + return err + } + samlMiddleWare.ServiceProvider.AcsURL = *u + } + + // Crewjam library use default route for ACS and metadata but we want to overwrite them + metadata, err := url.Parse(publicUrlString + RouteMetadata) + if err != nil { + return err + } + samlMiddleWare.ServiceProvider.MetadataURL = *metadata + + // The EntityID in the AuthnRequest is the Metadata URL + samlMiddleWare.ServiceProvider.EntityID = samlMiddleWare.ServiceProvider.MetadataURL.String() + + // The issuer format is unspecified + samlMiddleWare.ServiceProvider.AuthnNameIDFormat = samlidp.UnspecifiedNameIDFormat + + samlMiddlewares[pid] = samlMiddleWare + + return nil +} + +// Return the singleton MiddleWare +func GetMiddleware(pid string) (*samlsp.Middleware, error) { + if samlMiddlewares[pid] == nil { + return nil, errors.Errorf("An error occurred while retrieving the middeware, it is null") + } + return samlMiddlewares[pid], nil +} + +func MustParseCertificate(pemStr []byte) (*x509.Certificate, error) { + b, _ := pem.Decode(pemStr) + if b == nil { + return nil, errors.Errorf("Cannot find the next PEM formatted block") + } + cert, err := x509.ParseCertificate(b.Bytes) + if err != nil { + return nil, err + } + return cert, nil +} + +// Create a SAMLProvider object from the config file +func CreateSAMLProviderConfig(config config.Config, ctx context.Context, pid string) (*Configuration, error) { + var c ConfigurationCollection + conf := config.SelfServiceStrategy(ctx, "saml").Config + if err := jsonx. + NewStrictDecoder(bytes.NewBuffer(conf)). + Decode(&c); err != nil { + return nil, errors.Wrapf(err, "Unable to decode config %v", string(conf)) + } + + if len(c.SAMLProviders) == 0 { + return nil, errors.Errorf("Please indicate a SAML Identity Provider in your configuration file") + } + + providerConfig, err := c.ProviderConfig(pid) + if err != nil { + return nil, err + } + + if providerConfig.IDPInformation == nil { + return nil, errors.Errorf("Please include your Identity Provider information in the configuration file.") + } + + // _, sso_exists := providerConfig.IDPInformation["idp_sso_url"] + _, sso_exists := providerConfig.IDPInformation["idp_sso_url"] + _, entity_id_exists := providerConfig.IDPInformation["idp_entity_id"] + _, certificate_exists := providerConfig.IDPInformation["idp_certificate_path"] + _, logout_url_exists := providerConfig.IDPInformation["idp_logout_url"] + _, metadata_exists := providerConfig.IDPInformation["idp_metadata_url"] + + if (!metadata_exists && (!sso_exists || !entity_id_exists || !certificate_exists || !logout_url_exists)) || len(providerConfig.IDPInformation) > 4 { + return nil, errors.Errorf("Please check your IDP information in the configuration file") + } + + if providerConfig.ID == "" { + return nil, errors.Errorf("Provider must have an ID") + } + + if providerConfig.Label == "" { + return nil, errors.Errorf("Provider must have a label") + } + + if providerConfig.PrivateKeyPath == "" { + return nil, errors.Errorf("Provider must have a private key") + } + + if providerConfig.PublicCertPath == "" { + return nil, errors.Errorf("Provider must have a public certificate") + } + + if providerConfig.AttributesMap == nil || len(providerConfig.AttributesMap) == 0 { + return nil, errors.Errorf("Provider must have an attributes map") + } + + if providerConfig.AttributesMap["id"] == "" { + return nil, errors.Errorf("You must have an ID field in your attribute_map") + } + + if providerConfig.Mapper == "" { + return nil, errors.Errorf("Provider must have a mapper url") + } + + return providerConfig, nil +} diff --git a/selfservice/strategy/saml/handler_test.go b/selfservice/strategy/saml/handler_test.go new file mode 100644 index 000000000000..bc7ac906c654 --- /dev/null +++ b/selfservice/strategy/saml/handler_test.go @@ -0,0 +1,91 @@ +package saml_test + +import ( + "io/ioutil" + "testing" + + "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/stretchr/testify/require" + + "gotest.tools/assert" +) + +func TestInitMiddleWareWithMetadata(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + middleWare, _, _, err := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + require.NoError(t, err) + assert.Check(t, middleWare != nil) + assert.Check(t, middleWare.ServiceProvider.IDPMetadata != nil) + assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/:provider") + assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://idp.testshib.org/idp/shibboleth") +} + +func TestInitMiddleWareWithoutMetadata(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + middleWare, _, _, err := InitTestMiddlewareWithoutMetadata(t, + "https://samltest.id/idp/profile/SAML2/Redirect/SSO", + "https://samltest.id/saml/idp", + "file://testdata/samlkratos.crt", + "https://samltest.id/idp/profile/SAML2/Redirect/SSO") + + require.NoError(t, err) + assert.Check(t, middleWare != nil) + assert.Check(t, middleWare.ServiceProvider.IDPMetadata != nil) + assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/:provider") + assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://samltest.id/saml/idp") +} + +func TestGetMiddleware(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + middleWare, err := saml.GetMiddleware("samlProvider") + + require.NoError(t, err) + assert.Check(t, middleWare != nil) + assert.Check(t, middleWare.ServiceProvider.IDPMetadata != nil) + assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/:provider") + assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://idp.testshib.org/idp/shibboleth") +} + +func TestMustParseCertificate(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + certificate, err := ioutil.ReadFile("testdata/samlkratos.crt") + require.NoError(t, err) + + cert, err := saml.MustParseCertificate(certificate) + + require.NoError(t, err) + assert.Check(t, cert.Issuer.Country[0] == "AU") + assert.Check(t, cert.Issuer.Organization[0] == "Internet Widgits Pty Ltd") + assert.Check(t, cert.Issuer.Province[0] == "Some-State") + assert.Check(t, cert.Subject.Country[0] == "AU") + assert.Check(t, cert.Subject.Organization[0] == "Internet Widgits Pty Ltd") + assert.Check(t, cert.Subject.Province[0] == "Some-State") + assert.Check(t, cert.NotBefore.String() == "2022-02-21 11:08:20 +0000 UTC") + assert.Check(t, cert.NotAfter.String() == "2023-02-21 11:08:20 +0000 UTC") + assert.Check(t, cert.SerialNumber.String() == "485646075402096403898806020771481121115125312047") +} diff --git a/selfservice/strategy/saml/metadata_test.go b/selfservice/strategy/saml/metadata_test.go new file mode 100644 index 000000000000..6f9d8b20f7a6 --- /dev/null +++ b/selfservice/strategy/saml/metadata_test.go @@ -0,0 +1,123 @@ +package saml_test + +import ( + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "net/http" + "reflect" + "testing" + + "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/stretchr/testify/require" + "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +type Metadata struct { + XMLName xml.Name `xml:"EntityDescriptor"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + ValidUntil string `xml:"validUntil,attr"` + EntityID string `xml:"entityID,attr"` + SPSSODescriptor struct { + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + ValidUntil string `xml:"validUntil,attr"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + AuthnRequestsSigned string `xml:"AuthnRequestsSigned,attr"` + WantAssertionsSigned string `xml:"WantAssertionsSigned,attr"` + KeyDescriptor []struct { + Text string `xml:",chardata"` + Use string `xml:"use,attr"` + KeyInfo struct { + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + X509Data struct { + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + X509Certificate struct { + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + } `xml:"X509Certificate"` + } `xml:"X509Data"` + } `xml:"KeyInfo"` + EncryptionMethod []struct { + Text string `xml:",chardata"` + Algorithm string `xml:"Algorithm,attr"` + } `xml:"EncryptionMethod"` + } `xml:"KeyDescriptor"` + SingleLogoutService struct { + Text string `xml:",chardata"` + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + ResponseLocation string `xml:"ResponseLocation,attr"` + } `xml:"SingleLogoutService"` + AssertionConsumerService []struct { + Text string `xml:",chardata"` + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + Index string `xml:"index,attr"` + } `xml:"AssertionConsumerService"` + } `xml:"SPSSODescriptor"` +} + +func TestXmlMetadataExist(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + _, _, ts, err := InitTestMiddlewareWithMetadata(t, "file://testdata/SP_IDPMetadata.xml") + assert.NilError(t, err) + res, err := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + assert.NilError(t, err) + body, _ := ioutil.ReadAll(res.Body) + fmt.Println(body) + assert.Check(t, is.Equal(http.StatusOK, res.StatusCode)) + assert.Check(t, is.Equal("text/xml", res.Header.Get("Content-Type"))) +} + +func TestXmlMetadataValues(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + _, _, ts, _ := InitTestMiddlewareWithMetadata(t, "file://testdata/SP_IDPMetadata.xml") + res, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + body, _ := io.ReadAll(res.Body) + + assert.Check(t, is.Equal(http.StatusOK, res.StatusCode)) + assert.Check(t, is.Equal("text/xml", + res.Header.Get("Content-Type"))) + + expectedMetadata, err := ioutil.ReadFile("./testdata/expected_metadata.xml") + assert.NilError(t, err) + + // The string is parse to a struct + var expectedStructMetadata Metadata + err = xml.Unmarshal(expectedMetadata, &expectedStructMetadata) + require.NoError(t, err) + + var obtainedStructureMetadata Metadata + err = xml.Unmarshal(body, &obtainedStructureMetadata) + require.NoError(t, err) + + // We delete data that is likely to change naturally + expectedStructMetadata.SPSSODescriptor.AssertionConsumerService[0].Location = "" + expectedStructMetadata.SPSSODescriptor.AssertionConsumerService[1].Location = "" + obtainedStructureMetadata.SPSSODescriptor.AssertionConsumerService[0].Location = "" + obtainedStructureMetadata.SPSSODescriptor.AssertionConsumerService[1].Location = "" + expectedStructMetadata.ValidUntil = "" + expectedStructMetadata.SPSSODescriptor.ValidUntil = "" + obtainedStructureMetadata.ValidUntil = "" + obtainedStructureMetadata.SPSSODescriptor.ValidUntil = "" + expectedStructMetadata.EntityID = "" + obtainedStructureMetadata.EntityID = "" + + assert.Check(t, reflect.DeepEqual(expectedStructMetadata, obtainedStructureMetadata)) +} diff --git a/selfservice/strategy/saml/provider.go b/selfservice/strategy/saml/provider.go new file mode 100644 index 000000000000..52e25212f59a --- /dev/null +++ b/selfservice/strategy/saml/provider.go @@ -0,0 +1,43 @@ +package saml + +import ( + "context" + + "github.com/crewjam/saml/samlsp" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" +) + +type Provider interface { + Claims(ctx context.Context, config *config.Config, SAMLAttribute samlsp.Attributes, pid string) (*Claims, error) + Config() *Configuration +} + +type Claims struct { + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Name string `json:"name,omitempty"` + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + LastName string `json:"last_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + Nickname string `json:"nickname,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Profile string `json:"profile,omitempty"` + Picture string `json:"picture,omitempty"` + Website string `json:"website,omitempty"` + Email string `json:"email,omitempty"` + EmailVerified x.ConvertibleBoolean `json:"email_verified,omitempty"` + Gender string `json:"gender,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + Zoneinfo string `json:"zoneinfo,omitempty"` + Locale string `json:"locale,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + HD string `json:"hd,omitempty"` + Team string `json:"team,omitempty"` + Roles []string `json:"roles,omitempty"` + Groups []string `json:"groups,omitempty"` +} diff --git a/selfservice/strategy/saml/provider_config.go b/selfservice/strategy/saml/provider_config.go new file mode 100644 index 000000000000..357c9516f965 --- /dev/null +++ b/selfservice/strategy/saml/provider_config.go @@ -0,0 +1,67 @@ +package saml + +import ( + "github.com/ory/herodot" + "github.com/pkg/errors" +) + +type Configuration struct { + // ID is the provider's ID + ID string `json:"id"` + + // Provider is "generic" for SAML provider + Provider string `json:"provider"` + + // Label represents an optional label which can be used in the UI generation. + Label string `json:"label"` + + // Represent the path of the certificate of your application + PublicCertPath string `json:"public_cert_path"` + + // Represent the path of the private key of your application + PrivateKeyPath string `json:"private_key_path"` + + // It is a map where you have to name the attributes contained in the SAML response to associate them with their value + AttributesMap map[string]string `json:"attributes_map"` + + // Information about the IDP like the sso url, slo url, entiy ID, metadata url + IDPInformation map[string]string `json:"idp_information"` + + // Mapper specifies the JSONNet code snippet + // It can be either a URL (file://, http(s)://, base64://) or an inline JSONNet code snippet. + Mapper string `json:"mapper_url"` +} + +type ConfigurationCollection struct { + SAMLProviders []Configuration `json:"providers"` +} + +func (c ConfigurationCollection) Provider(id string, reg registrationStrategyDependencies) (Provider, error) { + for k := range c.SAMLProviders { + p := c.SAMLProviders[k] + if p.ID == id { + var providerNames []string + var addProviderName = func(pn string) string { + providerNames = append(providerNames, pn) + return pn + } + + switch p.Provider { + case addProviderName("generic"): + return NewProviderSAML(&p, reg), nil + } + return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, providerNames) + } + } + return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf(`SAML Provider "%s" is unknown or has not been configured`, id)) +} + +func (c ConfigurationCollection) ProviderConfig(id string) (*Configuration, error) { + for k := range c.SAMLProviders { + p := c.SAMLProviders[k] + if p.ID == id { + return &p, nil + } + } + return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf(`SAML Provider "%s" is unknown or has not been configured`, id)) +} diff --git a/selfservice/strategy/saml/provider_saml.go b/selfservice/strategy/saml/provider_saml.go new file mode 100644 index 000000000000..23047bf7d30c --- /dev/null +++ b/selfservice/strategy/saml/provider_saml.go @@ -0,0 +1,67 @@ +package saml + +import ( + "bytes" + "context" + + "github.com/crewjam/saml/samlsp" + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/jsonx" +) + +type ProviderSAML struct { + config *Configuration + reg registrationStrategyDependencies +} + +func NewProviderSAML( + config *Configuration, + reg registrationStrategyDependencies, +) *ProviderSAML { + return &ProviderSAML{ + config: config, + reg: reg, + } +} + +// Translate attributes from saml asseryion into kratos claims +func (d *ProviderSAML) Claims(ctx context.Context, config *config.Config, attributeSAML samlsp.Attributes, pid string) (*Claims, error) { + + var c ConfigurationCollection + + conf := config.SelfServiceStrategy(ctx, "saml").Config + if err := jsonx. + NewStrictDecoder(bytes.NewBuffer(conf)). + Decode(&c); err != nil { + return nil, errors.Wrapf(err, "Unable to decode config %v", string(conf)) + } + + providerConfig, err := c.ProviderConfig(pid) + if err != nil { + return nil, err + } + + claims := &Claims{ + Issuer: "saml", + Subject: attributeSAML.Get(providerConfig.AttributesMap["id"]), + Name: attributeSAML.Get(providerConfig.AttributesMap["firstname"]), + LastName: attributeSAML.Get(providerConfig.AttributesMap["lastname"]), + Nickname: attributeSAML.Get(providerConfig.AttributesMap["nickname"]), + Gender: attributeSAML.Get(providerConfig.AttributesMap["gender"]), + Birthdate: attributeSAML.Get(providerConfig.AttributesMap["birthdate"]), + Picture: attributeSAML.Get(providerConfig.AttributesMap["picture"]), + Email: attributeSAML.Get(providerConfig.AttributesMap["email"]), + Roles: attributeSAML[providerConfig.AttributesMap["roles"]], + Groups: attributeSAML[providerConfig.AttributesMap["groups"]], + PhoneNumber: attributeSAML.Get(providerConfig.AttributesMap["phone_number"]), + EmailVerified: true, + } + + return claims, nil +} + +func (d *ProviderSAML) Config() *Configuration { + return d.config +} diff --git a/selfservice/strategy/saml/schema.go b/selfservice/strategy/saml/schema.go new file mode 100644 index 000000000000..50aa01e10193 --- /dev/null +++ b/selfservice/strategy/saml/schema.go @@ -0,0 +1,8 @@ +package saml + +import ( + _ "embed" +) + +//go:embed .schema/link.schema.json +var linkSchema []byte diff --git a/selfservice/strategy/saml/strategy.go b/selfservice/strategy/saml/strategy.go new file mode 100644 index 000000000000..cb11ceb747b5 --- /dev/null +++ b/selfservice/strategy/saml/strategy.go @@ -0,0 +1,466 @@ +package saml + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "strings" + + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + "github.com/gofrs/uuid" + "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + + "github.com/go-playground/validator/v10" + + "github.com/ory/x/decoderx" + "github.com/ory/x/fetcher" + "github.com/ory/x/jsonx" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +const ( + RouteBase = "/self-service/methods/saml" + + RouteBaseAcs = RouteBase + "/acs" + RouteBaseAuth = RouteBase + "/auth" + RouteBaseMetadata = RouteBase + "/metadata" + + RouteAcs = RouteBaseAcs + "/:provider" + RouteAuth = RouteBaseAuth + "/:provider" + RouteMetadata = RouteBaseMetadata + "/:provider" +) + +var _ identity.ActiveCredentialsCounter = new(Strategy) + +type registrationStrategyDependencies interface { + x.LoggingProvider + x.WriterProvider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + + config.Provider + + continuity.ManagementProvider + continuity.ManagementProviderRelayState + + errorx.ManagementProvider + hash.HashProvider + + registration.HandlerProvider + registration.HooksProvider + registration.ErrorHandlerProvider + registration.HookExecutorProvider + registration.FlowPersistenceProvider + + login.HooksProvider + login.ErrorHandlerProvider + login.HookExecutorProvider + login.FlowPersistenceProvider + login.HandlerProvider + + settings.FlowPersistenceProvider + settings.HookExecutorProvider + settings.HooksProvider + settings.ErrorHandlerProvider + + identity.PrivilegedPoolProvider + identity.ValidationProvider + + session.HandlerProvider + session.ManagementProvider +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeSAML +} + +func (s *Strategy) D() registrationStrategyDependencies { + return s.d +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.SAMLGroup +} + +func isForced(req interface{}) bool { + f, ok := req.(interface { + IsForced() bool + }) + return ok && f.IsForced() +} + +type Strategy struct { + d registrationStrategyDependencies + f *fetcher.Fetcher + v *validator.Validate + hd *decoderx.HTTP +} + +type authCodeContainer struct { + FlowID string `json:"flow_id"` + State string `json:"state"` + Traits json.RawMessage `json:"traits"` +} + +func NewStrategy(d registrationStrategyDependencies) *Strategy { + return &Strategy{ + d: d, + f: fetcher.NewFetcher(), + v: validator.New(), + hd: decoderx.NewHTTP(), + } +} + +// We indicate here that when the ACS endpoint receives a POST request, we call the handleCallback method to process it +func (s *Strategy) setRoutes(r *x.RouterPublic) { + wrappedHandleCallback := strategy.IsDisabled(s.d, s.ID().String(), s.handleCallback) + if handle, _, _ := r.Lookup("POST", RouteAcs); handle == nil { + r.POST(RouteAcs, wrappedHandleCallback) + } // ACS SUPPORT +} + +// Get possible SAML Request IDs +func GetPossibleRequestIDs(r *http.Request, m samlsp.Middleware) []string { + possibleRequestIDs := []string{} + if m.ServiceProvider.AllowIDPInitiated { + possibleRequestIDs = append(possibleRequestIDs, "") + } + + trackedRequests := m.RequestTracker.GetTrackedRequests(r) + for _, tr := range trackedRequests { + possibleRequestIDs = append(possibleRequestIDs, tr.SAMLRequestID) + } + + return possibleRequestIDs +} + +// Retrieves the user's attributes from the SAML Assertion +func (s *Strategy) GetAttributesFromAssertion(assertion *saml.Assertion) (map[string][]string, error) { + + if assertion == nil { + return nil, errors.New("The assertion is nil") + } + + attributes := map[string][]string{} + + for _, attributeStatement := range assertion.AttributeStatements { + for _, attr := range attributeStatement.Attributes { + claimName := attr.Name + for _, value := range attr.Values { + attributes[claimName] = append(attributes[claimName], value.Value) + } + } + } + + return attributes, nil +} + +func (s *Strategy) validateFlow(ctx context.Context, r *http.Request, rid uuid.UUID) (flow.Flow, error) { + if x.IsZeroUUID(rid) { + return nil, errors.WithStack(herodot.ErrBadRequest.WithReason("The session cookie contains invalid values and the flow could not be executed. Please try again.")) + } + + if ar, err := s.d.RegistrationFlowPersister().GetRegistrationFlow(ctx, rid); err == nil { + if ar.Type != flow.TypeBrowser { + return ar, ErrAPIFlowNotSupported + } + + if err := ar.Valid(); err != nil { + return ar, err + } + return ar, nil + } + + if ar, err := s.d.LoginFlowPersister().GetLoginFlow(ctx, rid); err == nil { + if ar.Type != flow.TypeBrowser { + return ar, ErrAPIFlowNotSupported + } + + if err := ar.Valid(); err != nil { + return ar, err + } + return ar, nil + } + + ar, err := s.d.SettingsFlowPersister().GetSettingsFlow(ctx, rid) + if err == nil { + if ar.Type != flow.TypeBrowser { + return ar, ErrAPIFlowNotSupported + } + + sess, err := s.d.SessionManager().FetchFromRequest(ctx, r) + if err != nil { + return ar, err + } + + if err := ar.Valid(sess); err != nil { + return ar, err + } + return ar, nil + } + + return ar, err // this must return the error +} + +// Check if the user is already authenticated +func (s *Strategy) alreadyAuthenticated(w http.ResponseWriter, r *http.Request, req interface{}) bool { + // we assume an error means the user has no session + if _, err := s.d.SessionManager().FetchFromRequest(r.Context(), r); err == nil { + if !isForced(req) { + http.Redirect(w, r, s.d.Config().SelfServiceBrowserDefaultReturnTo(r.Context()).String(), http.StatusSeeOther) + return true + } + } + + return false +} + +func (s *Strategy) validateCallback(w http.ResponseWriter, r *http.Request) (flow.Flow, *authCodeContainer, error) { + var cntnr authCodeContainer + if _, err := s.d.RelayStateContinuityManager().Continue(r.Context(), w, r, sessionName, continuity.WithPayload(&cntnr)); err != nil { + return nil, nil, err + } + + req, err := s.validateFlow(r.Context(), r, x.ParseUUID(cntnr.FlowID)) + if err != nil { + return nil, &cntnr, err + } + + if r.URL.Query().Get("error") != "" { + return req, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete SAML flow because the SAML Provider returned error "%s": %s`, r.URL.Query().Get("error"), r.URL.Query().Get("error_description"))) + } + + return req, &cntnr, nil +} + +// Handle /selfservice/methods/saml/acs/:provider | Receive SAML response, parse the attributes and start auth flow +func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + // We get the provider ID form the URL + pid := ps.ByName("provider") + + if err := r.ParseForm(); err != nil { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, s.handleError(w, r, nil, pid, nil, err)) + } + + req, _, err := s.validateCallback(w, r) + if err != nil { + if req != nil { + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) + } else { + s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, s.handleError(w, r, nil, pid, nil, err)) + } + return + } + + m, err := GetMiddleware(pid) + if err != nil { + s.forwardError(w, r, err) + } + + // We get the possible SAML request IDs + possibleRequestIDs := GetPossibleRequestIDs(r, *m) + assertion, err := m.ServiceProvider.ParseResponse(r, possibleRequestIDs) + if err != nil { + s.forwardError(w, r, err) + } + + // We get the user's attributes from the SAML Response (assertion) + attributes, err := s.GetAttributesFromAssertion(assertion) + if err != nil { + s.forwardError(w, r, err) + return + } + + // We get the provider information from the config file + provider, err := s.Provider(r.Context(), pid) + if err != nil { + s.forwardError(w, r, err) + return + } + + // We translate SAML Attributes into claims (To create an identity we need these claims) + claims, err := provider.Claims(r.Context(), s.d.Config(), attributes, pid) + if err != nil { + s.forwardError(w, r, err) + return + } + + switch a := req.(type) { + case *login.Flow: + // Now that we have the claims and the provider, we have to decide if we log or register the user + if ff, err := s.processLoginOrRegister(w, r, a, provider, claims); err != nil { + if ff != nil { + s.forwardError(w, r, err) + } + s.forwardError(w, r, err) + } + return + } +} + +func (s *Strategy) forwardError(w http.ResponseWriter, r *http.Request, err error) { + s.d.LoginFlowErrorHandler().WriteFlowError(w, r, nil, s.NodeGroup(), err) +} + +// Return the SAML Provider with the specific ID +func (s *Strategy) Provider(ctx context.Context, id string) (Provider, error) { + c, err := s.Config(ctx) + if err != nil { + return nil, err + } + + provider, err := c.Provider(id, s.d) + if err != nil { + return nil, err + } + + return provider, nil +} + +// Translate YAML Config file into a SAML Provider struct +func (s *Strategy) Config(ctx context.Context) (*ConfigurationCollection, error) { + var c ConfigurationCollection + + conf := s.d.Config().SelfServiceStrategy(ctx, string(s.ID())).Config + if err := jsonx. + NewStrictDecoder(bytes.NewBuffer(conf)). + Decode(&c); err != nil { + s.d.Logger().WithError(err).WithField("config", conf) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode SAML Identity Provider configuration: %s", err)) + } + + return &c, nil +} + +func (s *Strategy) populateMethod(r *http.Request, c *container.Container, message func(provider string) *text.Message) error { + conf, err := s.Config(r.Context()) + if err != nil { + return err + } + + // does not need sorting because there is only one field + c.SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(c, conf.SAMLProviders, message) + + return nil +} + +func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Flow, provider string, traits []byte, err error) error { + switch rf := f.(type) { + case *login.Flow: + return err + case *registration.Flow: + // Reset all nodes to not confuse users. + // This is kinda hacky and will probably need to be updated at some point. + + rf.UI.Nodes = node.Nodes{} + + // Adds the "Continue" button + rf.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + AddProvider(rf.UI, provider, text.NewInfoRegistrationContinue()) + + if traits != nil { + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + traitNodes, err := container.NodesFromJSONSchema(r.Context(), node.SAMLGroup, ds.String(), "", nil) + if err != nil { + return err + } + + rf.UI.Nodes = append(rf.UI.Nodes, traitNodes...) + rf.UI.UpdateNodeValuesFromJSON(traits, "traits", node.SAMLGroup) + } + + return err + case *settings.Flow: + return err + } + + return err +} + +func (s *Strategy) CountActiveCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + for _, c := range cc { + if c.Type == s.ID() && gjson.ValidBytes(c.Config) { + var conf identity.CredentialsSAML + if err = json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + + for _, ider := range c.Identifiers { + parts := strings.Split(ider, ":") + if len(parts) != 2 { + continue + } + + if parts[0] == conf.Providers[0].Provider && parts[1] == conf.Providers[0].Subject && len(conf.Providers[0].Subject) > 1 && len(conf.Providers[0].Provider) > 1 { + count++ + } + + } + } + } + return +} + +func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + for _, c := range cc { + if c.Type == s.ID() && gjson.ValidBytes(c.Config) { + // TODO MANAGE THIS + var conf identity.CredentialsSAML + if err = json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + + for _, ider := range c.Identifiers { + parts := strings.Split(ider, ":") + if len(parts) != 2 { + continue + } + + for _, prov := range conf.Providers { + if parts[0] == prov.Provider && parts[1] == prov.Subject && len(prov.Subject) > 1 && len(prov.Provider) > 1 { + count++ + } + } + } + } + } + return +} + +func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.AuthenticatorAssuranceLevel1, + } +} diff --git a/selfservice/strategy/saml/strategy_auth.go b/selfservice/strategy/saml/strategy_auth.go new file mode 100644 index 000000000000..a5b67deae5b4 --- /dev/null +++ b/selfservice/strategy/saml/strategy_auth.go @@ -0,0 +1,70 @@ +package saml + +import ( + "errors" + "net/http" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/x/sqlcon" +) + +// Handle SAML Assertion and process to either login or register +func (s *Strategy) processLoginOrRegister(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, provider Provider, claims *Claims) (*flow.Flow, error) { + + // If the user's ID is null, we have to handle error + if claims.Subject == "" { + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, errors.New("the user ID is empty: the problem probably comes from the mapping between the SAML attributes and the identity attributes")) + } + + // This is a check to see if the user exists in the database + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeSAML, identity.SAMLUniqueID(provider.Config().ID, claims.Subject)) + + if err != nil { + // ErrNoRows is returned when a SQL SELECT statement returns no rows. + if errors.Is(err, sqlcon.ErrNoRows) { + + // The user doesn't net existe yet so we register him + registerFlow, err := s.d.RegistrationHandler().NewRegistrationFlow(w, r, flow.TypeBrowser) + if err != nil { + if i == nil { + return nil, s.handleError(w, r, registerFlow, provider.Config().ID, nil, err) + } else { + return nil, s.handleError(w, r, registerFlow, provider.Config().ID, i.Traits, err) + } + } + + if err = s.processRegistration(w, r, registerFlow, provider, claims); err != nil { + if i == nil { + return nil, s.handleError(w, r, registerFlow, provider.Config().ID, nil, err) + } else { + return nil, s.handleError(w, r, registerFlow, provider.Config().ID, i.Traits, err) + } + } + + return nil, nil + + } else { + return nil, err + } + } else { + // The user already exist in database so we log him + // loginFlow, err := s.d.LoginHandler().NewLoginFlow(w, r, flow.TypeBrowser) + if err != nil { + if i == nil { + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) + } else { + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, i.Traits, err) + } + } + if _, err = s.processLogin(w, r, loginFlow, provider, c, i, claims); err != nil { + if i == nil { + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) + } else { + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, i.Traits, err) + } + } + return nil, nil + } +} diff --git a/selfservice/strategy/saml/strategy_helper_test.go b/selfservice/strategy/saml/strategy_helper_test.go new file mode 100644 index 000000000000..5c40766f10c4 --- /dev/null +++ b/selfservice/strategy/saml/strategy_helper_test.go @@ -0,0 +1,182 @@ +package saml_test + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "encoding/xml" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "testing" + "time" + + "github.com/beevik/etree" + crewjamsaml "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + "github.com/crewjam/saml/xmlenc" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "gotest.tools/golden" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/kratos/x" +) + +var TimeNow = func() time.Time { return time.Now().UTC() } +var RandReader = rand.Reader + +func ViperSetProviderConfig(t *testing.T, conf *config.Config, SAMLProvider ...saml.Configuration) { + conf.MustSet(context.Background(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeSAML)+".config", &saml.ConfigurationCollection{SAMLProviders: SAMLProvider}) + conf.MustSet(context.Background(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeSAML)+".enabled", true) +} + +func NewTestClient(t *testing.T, jar *cookiejar.Jar) *http.Client { + if jar == nil { + j, err := cookiejar.New(nil) + jar = j + require.NoError(t, err) + } + return &http.Client{ + Jar: jar, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 20 { + for k, v := range via { + t.Logf("Failed with redirect (%d): %s", k, v.URL.String()) + } + return errors.New("stopped after 20 redirects") + } + return nil + }, + } +} + +// AssertSystemError asserts an error ui response +func AssertSystemError(t *testing.T, errTS *httptest.Server, res *http.Response, body []byte, code int, reason string) { + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + + assert.Equal(t, int64(code), gjson.GetBytes(body, "code").Int(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "reason").String(), reason, "%s", body) +} + +func mustParseCertificate(pemStr []byte) *x509.Certificate { + b, _ := pem.Decode(pemStr) + if b == nil { + panic("cannot parse PEM") + } + cert, err := x509.ParseCertificate(b.Bytes) + if err != nil { + panic(err) + } + return cert +} + +func mustParsePrivateKey(pemStr []byte) crypto.PrivateKey { + b, _ := pem.Decode(pemStr) + if b == nil { + panic("cannot parse PEM") + } + k, err := x509.ParsePKCS1PrivateKey(b.Bytes) + if err != nil { + panic(err) + } + return k +} + +func InitTestMiddleware(t *testing.T, idpInformation map[string]string) (*samlsp.Middleware, *saml.Strategy, *httptest.Server, error) { + conf, reg := internal.NewFastRegistryWithMocks(t) + + strategy := saml.NewStrategy(reg) + routerP := x.NewRouterPublic() + routerA := x.NewRouterAdmin() + ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) + + attributesMap := make(map[string]string) + attributesMap["id"] = "mail" + attributesMap["firstname"] = "givenName" + attributesMap["lastname"] = "sn" + attributesMap["email"] = "mail" + + // Initiates the service provider + ViperSetProviderConfig( + t, + conf, + saml.Configuration{ + ID: "samlProvider", + Label: "samlProviderLabel", + Provider: "generic", + PublicCertPath: "file://testdata/myservice.cert", + PrivateKeyPath: "file://testdata/myservice.key", + Mapper: "file://testdata/saml.jsonnet", + AttributesMap: attributesMap, + IDPInformation: idpInformation, + }, + ) + + conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationEnabled, true) + testhelpers.SetDefaultIdentitySchema(conf, "file://testdata/registration.schema.json") + conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) + + t.Logf("Kratos Public URL: %s", ts.URL) + + // Instantiates the MiddleWare + _, err := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") + require.NoError(t, err) + middleware, err := saml.GetMiddleware("samlProvider") + require.NoError(t, err) + middleware.ServiceProvider.Key = mustParsePrivateKey(golden.Get(t, "key.pem")).(*rsa.PrivateKey) + middleware.ServiceProvider.Certificate = mustParseCertificate(golden.Get(t, "cert.pem")) + + return middleware, strategy, ts, err +} + +func InitTestMiddlewareWithMetadata(t *testing.T, metadataURL string) (*samlsp.Middleware, *saml.Strategy, *httptest.Server, error) { + idpInformation := make(map[string]string) + idpInformation["idp_metadata_url"] = metadataURL + + return InitTestMiddleware(t, idpInformation) +} + +func InitTestMiddlewareWithoutMetadata(t *testing.T, idpSsoUrl string, idpEntityId string, + idpCertifiatePath string, idpLogoutUrl string) (*samlsp.Middleware, *saml.Strategy, *httptest.Server, error) { + + idpInformation := make(map[string]string) + idpInformation["idp_sso_url"] = idpSsoUrl + idpInformation["idp_entity_id"] = idpEntityId + idpInformation["idp_certificate_path"] = idpCertifiatePath + idpInformation["idp_logout_url"] = idpLogoutUrl + + return InitTestMiddleware(t, idpInformation) +} + +func GetAndDecryptAssertion(t *testing.T, samlResponseFile string, key *rsa.PrivateKey) (*crewjamsaml.Assertion, error) { + // Load saml response test file + samlResponse, err := ioutil.ReadFile(samlResponseFile) + require.NoError(t, err) + + // Decrypt saml response assertion + doc := etree.NewDocument() + err = doc.ReadFromBytes(samlResponse) + require.NoError(t, err) + responseEl := doc.Root() + el := responseEl.FindElement("//EncryptedAssertion/EncryptedData") + plaintextAssertion, err := xmlenc.Decrypt(key, el) + require.NoError(t, err) + + assertion := &crewjamsaml.Assertion{} + err = xml.Unmarshal(plaintextAssertion, assertion) + require.NoError(t, err) + + return assertion, err +} diff --git a/selfservice/strategy/saml/strategy_login.go b/selfservice/strategy/saml/strategy_login.go new file mode 100644 index 000000000000..b31e6332b0cb --- /dev/null +++ b/selfservice/strategy/saml/strategy_login.go @@ -0,0 +1,168 @@ +package saml + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/google/go-jsonnet" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +// Implement the interface +var _ login.Strategy = new(Strategy) + +// Call at the creation of Kratos, when Kratos implement all authentication routes +func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { + s.setRoutes(r) +} + +// SubmitSelfServiceLoginFlowWithSAMLMethodBody is used to decode the login form payload +// when using the saml method. +// +// swagger:model SubmitSelfServiceLoginFlowWithSAMLMethodBody +type SubmitSelfServiceLoginFlowWithSAMLMethodBody struct { + // The provider to register with + // + // required: true + Provider string `json:"samlProvider"` + + // The CSRF Token + CSRFToken string `json:"csrf_token"` + + // Method to use + // + // This field must be set to `oidc` when using the oidc method. + // + // required: true + Method string `json:"method"` + + // The identity traits. This is a placeholder for the registration flow. + Traits json.RawMessage `json:"traits"` +} + +// Login and give a session to the user +func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login.Flow, provider Provider, c *identity.Credentials, i *identity.Identity, claims *Claims) (*registration.Flow, error) { + + s.updateIdentityTraits(i, provider, claims) + + var o identity.CredentialsSAML + if err := json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o); err != nil { + return nil, s.handleError(w, r, a, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()))) + } + + sess := session.NewInactiveSession() // Creation of an inactive session + sess.CompletedLoginFor(s.ID(), identity.AuthenticatorAssuranceLevel1) // Add saml to the Authentication Method References + + if err := s.d.LoginHookExecutor().PostLoginHook(w, r, node.SAMLGroup, a, i, sess); err != nil { + return nil, s.handleError(w, r, a, provider.Config().ID, nil, err) + } + + return nil, nil +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, ss *session.Session) (i *identity.Identity, err error) { + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, err + } + + var p SubmitSelfServiceLoginFlowWithSAMLMethodBody + if err := s.newLinkDecoder(&p, r); err != nil { + return nil, s.handleError(w, r, f, "", nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP form request: %s", err.Error()))) + } + + var pid = p.Provider // This can come from both url query and post body + if pid == "" { + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + + if err := flow.MethodEnabledAndAllowed(r.Context(), s.ID().String(), s.ID().String(), s.d); err != nil { + return nil, s.handleError(w, r, f, pid, nil, err) + } + + req, err := s.validateFlow(r.Context(), r, f.ID) + if err != nil { + return nil, s.handleError(w, r, f, pid, nil, err) + } + + if s.alreadyAuthenticated(w, r, req) { + return + } + + state := x.NewUUID().String() + if err := s.d.RelayStateContinuityManager().Pause(r.Context(), w, r, sessionName, + continuity.WithPayload(&authCodeContainer{ + State: state, + FlowID: f.ID.String(), + Traits: p.Traits, + }), + continuity.WithLifespan(time.Minute*30)); err != nil { + return nil, s.handleError(w, r, f, pid, nil, err) + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + return nil, s.handleError(w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + } + + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(RouteBaseAuth)) + } else { + http.Redirect(w, r, RouteBaseAuth+"/"+pid, http.StatusSeeOther) + } + + return nil, errors.WithStack(flow.ErrCompletedByStrategy) +} + +func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, l *login.Flow) error { + if l.Type != flow.TypeBrowser { + return nil + } + + // This strategy can only solve AAL1 + if requestedAAL > identity.AuthenticatorAssuranceLevel1 { + return nil + } + + return s.populateMethod(r, l.UI, text.NewInfoLoginWith) +} + +// In order to do a JustInTimeProvisioning, it is important to update the identity traits at each new SAML connection +func (s *Strategy) updateIdentityTraits(i *identity.Identity, provider Provider, claims *Claims) error { + jn, err := s.f.Fetch(provider.Config().Mapper) + if err != nil { + return nil + } + + var jsonClaims bytes.Buffer + if err := json.NewEncoder(&jsonClaims).Encode(claims); err != nil { + return nil + } + + vm := jsonnet.MakeVM() + vm.ExtCode("claims", jsonClaims.String()) + evaluated, err := vm.EvaluateAnonymousSnippet(provider.Config().Mapper, jn.String()) + if err != nil { + return err + } else if traits := gjson.Get(evaluated, "identity.traits"); !traits.IsObject() { + i.Traits = []byte{'{', '}'} + return errors.New("SAML Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!") + } else { + i.Traits = []byte(traits.Raw) + return nil + } + +} diff --git a/selfservice/strategy/saml/strategy_registration.go b/selfservice/strategy/saml/strategy_registration.go new file mode 100644 index 000000000000..e0422ba4c48f --- /dev/null +++ b/selfservice/strategy/saml/strategy_registration.go @@ -0,0 +1,163 @@ +package saml + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "github.com/google/go-jsonnet" + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/x/decoderx" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/x" +) + +// Implement the interface +var _ registration.Strategy = new(Strategy) + +// Call at the creation of Kratos, when Kratos implement all authentication routes +func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) { + s.setRoutes(r) +} + +func (s *Strategy) GetRegistrationIdentity(r *http.Request, ctx context.Context, provider Provider, claims *Claims, logsEnabled bool) (*identity.Identity, error) { + // Fetch fetches the file contents from the mapper file. + jn, err := s.f.Fetch(provider.Config().Mapper) + if err != nil { + return nil, err + } + + var jsonClaims bytes.Buffer + if err := json.NewEncoder(&jsonClaims).Encode(claims); err != nil { + return nil, err + } + + // Identity Creation + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + + vm := jsonnet.MakeVM() + vm.ExtCode("claims", jsonClaims.String()) + evaluated, err := vm.EvaluateAnonymousSnippet(provider.Config().Mapper, jn.String()) + if err != nil { + return nil, err + } else if traits := gjson.Get(evaluated, "identity.traits"); !traits.IsObject() { + i.Traits = []byte{'{', '}'} + if logsEnabled { + s.d.Logger(). + WithRequest(r). + WithField("Provider", provider.Config().ID). + WithSensitiveField("saml_claims", claims). + WithField("mapper_jsonnet_output", evaluated). + WithField("mapper_jsonnet_url", provider.Config().Mapper). + Error("SAML Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!") + } + } else { + i.Traits = []byte(traits.Raw) + } + + if logsEnabled { + s.d.Logger(). + WithRequest(r). + WithField("saml_provider", provider.Config().ID). + WithSensitiveField("saml_claims", claims). + WithSensitiveField("mapper_jsonnet_output", evaluated). + WithField("mapper_jsonnet_url", provider.Config().Mapper). + Debug("SAML Jsonnet mapper completed.") + + s.d.Logger(). + WithRequest(r). + WithField("saml_provider", provider.Config().ID). + WithSensitiveField("identity_traits", i.Traits). + WithSensitiveField("mapper_jsonnet_output", evaluated). + WithField("mapper_jsonnet_url", provider.Config().Mapper). + Debug("Merged form values and SAML Jsonnet output.") + } + + // Verify the identity + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { + return i, err + } + + // Create new uniq credentials identifier for user is database + creds, err := identity.NewCredentialsSAML(claims.Subject, provider.Config().ID) + if err != nil { + return i, err + } + + // Set the identifiers to the identity + i.SetCredentials(s.ID(), *creds) + + return i, nil +} + +func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a *registration.Flow, provider Provider, claims *Claims) error { + + i, err := s.GetRegistrationIdentity(r, r.Context(), provider, claims, true) + if err != nil { + if i == nil { + return s.handleError(w, r, a, provider.Config().ID, nil, err) + } else { + return s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + } + } + + if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeSAML, a, i); err != nil { + return s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + } + + return nil +} + +// Method not used but necessary to implement the interface +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + if f.Type != flow.TypeBrowser { + return nil + } + + return s.populateMethod(r, f.UI, text.NewInfoRegistrationWith) +} + +func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + raw, err := sjson.SetBytes(linkSchema, "properties.traits.$ref", ds.String()+"#/properties/traits") + if err != nil { + return errors.WithStack(err) + } + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(raw) + if err != nil { + return errors.WithStack(err) + } + + if err := s.hd.Decode(r, &p, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderSetValidatePayloads(false), + decoderx.HTTPDecoderUseQueryAndBody(), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ); err != nil { + return errors.WithStack(err) + } + + return nil +} + +// Not needed in SAML +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { + return flow.ErrStrategyNotResponsible +} diff --git a/selfservice/strategy/saml/strategy_test.go b/selfservice/strategy/saml/strategy_test.go new file mode 100644 index 000000000000..c594d1eb0f09 --- /dev/null +++ b/selfservice/strategy/saml/strategy_test.go @@ -0,0 +1,297 @@ +package saml_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "regexp" + "testing" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/x/sqlxx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + gotest "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +func TestGetAndDecryptAssertion(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + middleware, _, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + assertion, err := GetAndDecryptAssertion(t, "./testdata/SP_SamlResponse.xml", middleware.ServiceProvider.Key) + + require.NoError(t, err) + gotest.Check(t, assertion != nil) +} + +func TestGetAttributesFromAssertion(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + middleware, strategy, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + assertion, _ := GetAndDecryptAssertion(t, "./testdata/SP_SamlResponse.xml", middleware.ServiceProvider.Key) + + mapAttributes, err := strategy.GetAttributesFromAssertion(assertion) + + require.NoError(t, err) + gotest.Check(t, mapAttributes["urn:oid:0.9.2342.19200300.100.1.1"][0] == "myself") + gotest.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.1"][0] == "Member") + gotest.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.1"][1] == "Staff") + gotest.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.6"][0] == "myself@testshib.org") + gotest.Check(t, mapAttributes["urn:oid:2.5.4.4"][0] == "And I") + gotest.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.9"][0] == "Member@testshib.org") + gotest.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.9"][1] == "Staff@testshib.org") + gotest.Check(t, mapAttributes["urn:oid:2.5.4.42"][0] == "Me Myself") + gotest.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.7"][0] == "urn:mace:dir:entitlement:common-lib-terms") + gotest.Check(t, mapAttributes["urn:oid:2.5.4.3"][0] == "Me Myself And I") + gotest.Check(t, mapAttributes["urn:oid:2.5.4.20"][0] == "555-5555") + + t.Log(mapAttributes) +} + +func TestCreateAuthRequest(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + middleware, _, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + authReq, err := middleware.ServiceProvider.MakeAuthenticationRequest("https://samltest.id/idp/profile/SAML2/Redirect/SSO", "saml.HTTPPostBinding", "saml.HTTPPostBinding") + require.NoError(t, err) + + matchACS, err := regexp.MatchString(`http://127.0.0.1:\d{5}/self-service/methods/saml/acs`, authReq.AssertionConsumerServiceURL) + require.NoError(t, err) + gotest.Check(t, matchACS) + + matchMetadata, err := regexp.MatchString(`http://127.0.0.1:\d{5}/self-service/methods/saml/metadata`, authReq.Issuer.Value) + require.NoError(t, err) + gotest.Check(t, matchMetadata) + + gotest.Check(t, is.Equal(authReq.Destination, "https://samltest.id/idp/profile/SAML2/Redirect/SSO")) +} + +func TestProvider(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + _, strategy, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + provider, err := strategy.Provider(context.Background(), "samlProvider") + require.NoError(t, err) + gotest.Check(t, provider != nil) + gotest.Check(t, provider.Config().ID == "samlProvider") + gotest.Check(t, provider.Config().Label == "samlProviderLabel") +} + +func TestConfig(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + _, strategy, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + config, err := strategy.Config(context.Background()) + require.NoError(t, err) + gotest.Check(t, config != nil) + gotest.Check(t, len(config.SAMLProviders) == 1) + gotest.Check(t, config.SAMLProviders[0].ID == "samlProvider") + gotest.Check(t, config.SAMLProviders[0].Label == "samlProviderLabel") +} + +func TestID(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + _, strategy, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + id := strategy.ID() + gotest.Check(t, id == "saml") +} + +func TestCountActiveCredentials(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + _, strategy, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + mapCredentials := make(map[identity.CredentialsType]identity.Credentials) + + var b bytes.Buffer + err := json.NewEncoder(&b).Encode(identity.CredentialsSAML{ + Providers: []identity.CredentialsSAMLProvider{ + { + Subject: "testUserID", + Provider: "saml", + }}, + }) + require.NoError(t, err) + + mapCredentials[identity.CredentialsTypeSAML] = identity.Credentials{ + Type: identity.CredentialsTypeSAML, + Identifiers: []string{"saml:testUserID"}, + Config: b.Bytes(), + } + + count, err := strategy.CountActiveCredentials(mapCredentials) + require.NoError(t, err) + gotest.Check(t, count == 1) +} + +func TestGetRegistrationIdentity(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + + middleware, strategy, _, _ := InitTestMiddlewareWithMetadata(t, + "file://testdata/SP_IDPMetadata.xml") + + provider, _ := strategy.Provider(context.Background(), "samlProvider") + assertion, _ := GetAndDecryptAssertion(t, "./testdata/SP_SamlResponse.xml", middleware.ServiceProvider.Key) + attributes, _ := strategy.GetAttributesFromAssertion(assertion) + claims, _ := provider.Claims(context.Background(), strategy.D().Config(), attributes, "samlProvider") + + i, err := strategy.GetRegistrationIdentity(nil, context.Background(), provider, claims, false) + require.NoError(t, err) + gotest.Check(t, i != nil) +} + +func TestCountActiveFirstFactorCredentials(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + strategy := saml.NewStrategy(reg) + + toJson := func(c identity.CredentialsSAML) []byte { + out, err := json.Marshal(&c) + require.NoError(t, err) + return out + } + + for k, tc := range []struct { + in identity.CredentialsCollection + expected int + }{ + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Config: sqlxx.JSONRawMessage{}, + }}, + }, + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Config: toJson(identity.CredentialsSAML{Providers: []identity.CredentialsSAMLProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Identifiers: []string{""}, + Config: toJson(identity.CredentialsSAML{Providers: []identity.CredentialsSAMLProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Identifiers: []string{"bar:"}, + Config: toJson(identity.CredentialsSAML{Providers: []identity.CredentialsSAMLProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Identifiers: []string{":foo"}, + Config: toJson(identity.CredentialsSAML{Providers: []identity.CredentialsSAMLProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Identifiers: []string{"not-bar:foo"}, + Config: toJson(identity.CredentialsSAML{Providers: []identity.CredentialsSAMLProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Identifiers: []string{"bar:not-foo"}, + Config: toJson(identity.CredentialsSAML{Providers: []identity.CredentialsSAMLProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + }, + { + in: identity.CredentialsCollection{{ + Type: strategy.ID(), + Identifiers: []string{"bar:foo"}, + Config: toJson(identity.CredentialsSAML{Providers: []identity.CredentialsSAMLProvider{ + {Subject: "foo", Provider: "bar"}, + }}), + }}, + expected: 1, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + in := make(map[identity.CredentialsType]identity.Credentials) + for _, v := range tc.in { + in[v.Type] = v + } + actual, err := strategy.CountActiveFirstFactorCredentials(in) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestModifyIdentityTraits(t *testing.T) { + if testing.Short() { + t.Skip() + } + + saml.DestroyMiddlewareIfExists("samlProvider") + +} diff --git a/selfservice/strategy/saml/testdata/SP_IDPMetadata.xml b/selfservice/strategy/saml/testdata/SP_IDPMetadata.xml new file mode 100644 index 000000000000..dcaf7f051dae --- /dev/null +++ b/selfservice/strategy/saml/testdata/SP_IDPMetadata.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + testshib.org + + TestShib Test IdP + TestShib IdP. Use this as a source of attributes + for your test SP. + https://www.testshib.org/testshibtwo.jpg + + + + + + MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV + MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD + VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4 + MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI + EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl + c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C + yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe + 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT + NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614 + kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH + gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G + A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86 + 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl + bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo + aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL + I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo + 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4 + /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj + Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr + 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA== + + + + + + + + + + + + urn:mace:shibboleth:1.0:nameIdentifier + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + + + + + + + + MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV + MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD + VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4 + MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI + EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl + c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C + yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe + 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT + NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614 + kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH + gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G + A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86 + 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl + bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo + aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL + I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo + 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4 + /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj + Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr + 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA== + + + + + + + + + + + + urn:mace:shibboleth:1.0:nameIdentifier + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + TestShib Two Identity Provider + TestShib Two + http://www.testshib.org/testshib-two/ + + + Nate + Klingenstein + ndk@internet2.edu + + \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/SP_SamlResponse.xml b/selfservice/strategy/saml/testdata/SP_SamlResponse.xml new file mode 100644 index 000000000000..f5dbc10766ab --- /dev/null +++ b/selfservice/strategy/saml/testdata/SP_SamlResponse.xml @@ -0,0 +1,38 @@ + + + https://idp.testshib.org/idp/shibboleth + + + + + + + + + + + + + + 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 + + + + \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response b/selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response new file mode 100644 index 000000000000..1031d30218df --- /dev/null +++ b/selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJwZnhlZDg4YzQzZC02NTA0LWUxZjEtNWFmMC00MGJlN2YyNzlmYzUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE2LTAxLTA1VDE3OjUzOjExWiIgRGVzdGluYXRpb249Imh0dHBzOi8vMjllZTZkMmUubmdyb2suaW8vc2FtbC9hY3MiIEluUmVzcG9uc2VUbz0iaWQtZDQwYzE1YzEwNGI1MjY5MWVjY2YwYTJhNWM4YTE1NTk1YmU3NTQyMyI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzUwMzk4Mzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+PGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhlZDg4YzQzZC02NTA0LWUxZjEtNWFmMC00MGJlN2YyNzlmYzUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPlNWQWFRZzh2bW1TUUw2L1lCbVMyeWRLUlA3ST08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+c0JlVFZQMGJab1BSK2JmeUFrVnY2STNDVjdZOFhxbkoycjhmMStXbXIyZ0ZnblJGODVOdnZTUCtyMUJvN250dU9zd080ZkI0Uks0SHlTYnlsZzRiS0hLSDE5WDkxaFZBekpTeXNmbVMvZDV3ZzFDZmlXV3Q1UzJIQTUwOHRoWHVabndHM1h6NktuV0s4a1JkeDFkYytZUldnYUZ5ZDRnTEc5YUJUc1hPWjd2eC83UDRicnpORW00d1A5LzB0dWZ4Rytuc1k2RHB3bkVHQ2psK1ZVS3BnekVxd05OalFxWUZZU0FYRWsrVnQrWDNjMmQwSElyWlF2WW5OaDAyS3h1d1ZCVGhuM01helFOYU54Qy9zeWYza0RRQ1JyWkNZbytZdER1ZHpKVTlwM0EwWVhIVFFjc2RldHNIWlhDTWozbXV2emMwbUVCbHc0TGJjaEttbmJ5Wm1nPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUVDRENDQXZDZ0F3SUJBZ0lVWHVuMDhDc2xMUldTTHFObkRFMU50R0plZmwwd0RRWUpLb1pJaHZjTkFRRUZCUUF3VXpFTE1Ba0dBMVVFQmhNQ1ZWTXhEREFLQmdOVkJBb01BMk4wZFRFVk1CTUdBMVVFQ3d3TVQyNWxURzluYVc0Z1NXUlFNUjh3SFFZRFZRUUREQlpQYm1WTWIyZHBiaUJCWTJOdmRXNTBJRE15TmpFME1CNFhEVEV6TURrek1ERTVNelUwTkZvWERURTRNVEF3TVRFNU16VTBORm93VXpFTE1Ba0dBMVVFQmhNQ1ZWTXhEREFLQmdOVkJBb01BMk4wZFRFVk1CTUdBMVVFQ3d3TVQyNWxURzluYVc0Z1NXUlFNUjh3SFFZRFZRUUREQlpQYm1WTWIyZHBiaUJCWTJOdmRXNTBJRE15TmpFME1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME9HOFY4bWhvdmtqNHJoR2hqcmJFeFJZYnpLVjJaeGZ2R2ZFR1hHVXZYYzZEcWVqWUVkaFoybUlmQ0RvamhRamswQnl3aWlyQUtNT3QxR051SDdhV0lFNDdEMGV3dEs1eWxFQW03ZVZtb1k0a3hMQ2FXNXdZckMxU3pNbnBlaXRVeHF2c2JuS3ozalVLWUhSZ2dwZnZWajRzaUhEWmVJWmE5YTVyVXZwTW5uYk9vRmlaQ0lFTnBxM1RDMzNpdk9TWmhFTlJUem12bms1R0RvTEh3LzhxQWdRaXlUM0QxeENrU0JiNTRQSGdrUTVScTFvZExNL2hKK0wwanpDVVFINGd4cFdsRUFhYjRLOXM4ZnBCVUJCaDVnbUpDWWk4VWJJbGhxTzhOMm15bnVtMzNCVS92SjNQbmF3VDRZWWtUd1JVeDZZKzNmcG1SQkhxbDRoODNTTWV3SURBUUFCbzRIVE1JSFFNQXdHQTFVZEV3RUIvd1FDTUFBd0hRWURWUjBPQkJZRUZPZkZGakhGajlhNnhwbmdiMTFycmhnTWU5QXJNSUdRQmdOVkhTTUVnWWd3Z1lXQUZPZkZGakhGajlhNnhwbmdiMTFycmhnTWU5QXJvVmVrVlRCVE1Rc3dDUVlEVlFRR0V3SlZVekVNTUFvR0ExVUVDZ3dEWTNSMU1SVXdFd1lEVlFRTERBeFBibVZNYjJkcGJpQkpaRkF4SHpBZEJnTlZCQU1NRms5dVpVeHZaMmx1SUVGalkyOTFiblFnTXpJMk1UU0NGRjdwOVBBckpTMFZraTZqWnd4TlRiUmlYbjVkTUE0R0ExVWREd0VCL3dRRUF3SUhnREFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBTWdsbjROUE1RbjhHeXZxOENUUCtjMmU2Q1V6Y3ZSRUtuVGhqeFQ5V2N2VjFaVlhNQk5QbTRjVHFUMzYxRWRMelk1eVdMVVdYZDRBdkZuY2lxQjNNSFlhMm5xVG1udkxnbWhrV2UraGRGb05lNStJQThBeEduK25xVUlTbXlCZUN4dVVVQWJSTXVvd2lBcndISXB6cEV5UklZZFNaUk5GMGR2Z2lQWXlyL01pUFhJY3pwSDVuTGt2YkxwY0FGK1I4Wmg5bndZMGcxSlZ5YzZBQjZqN1lleHVVUVpwSEg0czBWZHgvbldtcmNGZUxaS0NUeGNhaEh2VTUwZTF5S1g1dGhmVmFKcUk4UVE3eFp4eXUwVFRzaWFYMHV3NTFKUE96UHVBUHBoMHo2eG9TOW9ZeHV6WjF5OXNOSEg2a0g4R0ZudlMyTXF5SGlOejBoMFNxL3E2bit3PT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBWZXJzaW9uPSIyLjAiIElEPSJBZDk0NWFlZGEzOGE1MDhmOGZhYzliYzk2MTNkNTk2NDJjMGQyZDhjYiIgSXNzdWVJbnN0YW50PSIyMDE2LTAxLTA1VDE3OjUzOjExWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzUwMzk4Mzwvc2FtbDpJc3N1ZXI+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnJvc3NAa25kci5vcmc8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMTYtMDEtMDVUMTc6NTY6MTFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vMjllZTZkMmUubmdyb2suaW8vc2FtbC9hY3MiIEluUmVzcG9uc2VUbz0iaWQtZDQwYzE1YzEwNGI1MjY5MWVjY2YwYTJhNWM4YTE1NTk1YmU3NTQyMyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE2LTAxLTA1VDE3OjUwOjExWiIgTm90T25PckFmdGVyPSIyMDE2LTAxLTA1VDE3OjU2OjExWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovLzI5ZWU2ZDJlLm5ncm9rLmlvL3NhbWwvbWV0YWRhdGE8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE2LTAxLTA1VDE3OjUzOjEwWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNi0wMS0wNlQxNzo1MzoxMVoiIFNlc3Npb25JbmRleD0iX2ViZGNiZTgwLTk1ZmYtMDEzMy1kODcxLTM4Y2EzYTY2MmYxYyI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyIgTmFtZT0iVXNlci5lbWFpbCI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cm9zc0BrbmRyLm9yZzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIiBOYW1lPSJtZW1iZXJPZiI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyIvPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IlVzZXIuTGFzdE5hbWUiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPktpbmRlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIiBOYW1lPSJQZXJzb25JbW11dGFibGVJRCI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyIvPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IlVzZXIuRmlyc3ROYW1lIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5Sb3NzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+Cgo= \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/cert.pem b/selfservice/strategy/saml/testdata/cert.pem new file mode 100644 index 000000000000..52667ef39ff2 --- /dev/null +++ b/selfservice/strategy/saml/testdata/cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0 +MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9 +ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmH +O8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKv +Rsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgk +akpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeT +QLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvn +OwJlNCASPZRH/JmF8tX0hoHuAQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/expected_metadata.xml b/selfservice/strategy/saml/testdata/expected_metadata.xml new file mode 100644 index 000000000000..05039766f75f --- /dev/null +++ b/selfservice/strategy/saml/testdata/expected_metadata.xml @@ -0,0 +1,25 @@ + + + + + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + + + + + + + + + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + + + + + + + \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/key.pem b/selfservice/strategy/saml/testdata/key.pem new file mode 100644 index 000000000000..48284dac33a1 --- /dev/null +++ b/selfservice/strategy/saml/testdata/key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDU8wdiaFmPfTyRYuFlVPi866WrH/2JubkHzp89bBQopDaLXYxi +3PTu3O6Q/KaKxMOFBqrInwqpv/omOGZ4ycQ51O9I+Yc7ybVlW94lTo2gpGf+Y/8E +PsVbnZaFutRctJ4dVIp9aQ2TpLiGT0xX1OzBO/JEgq9GzDRf+B+eqSuglwIDAQAB +AoGBAMuy1eN6cgFiCOgBsB3gVDdTKpww87Qk5ivjqEt28SmXO13A1KNVPS6oQ8SJ +CT5Azc6X/BIAoJCURVL+LHdqebogKljhH/3yIel1kH19vr4E2kTM/tYH+qj8afUS +JEmArUzsmmK8ccuNqBcllqdwCZjxL4CHDUmyRudFcHVX9oyhAkEA/OV1OkjM3CLU +N3sqELdMmHq5QZCUihBmk3/N5OvGdqAFGBlEeewlepEVxkh7JnaNXAXrKHRVu/f/ +fbCQxH+qrwJBANeQERF97b9Sibp9xgolb749UWNlAdqmEpmlvmS202TdcaaT1msU +4rRLiQN3X9O9mq4LZMSVethrQAdX1whawpkCQQDk1yGf7xZpMJ8F4U5sN+F4rLyM +Rq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA +yfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr +vBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6 +hU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/myservice.cert b/selfservice/strategy/saml/testdata/myservice.cert new file mode 100755 index 000000000000..a815f8f44742 --- /dev/null +++ b/selfservice/strategy/saml/testdata/myservice.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDITCCAgmgAwIBAgIUAKe3G3G4JRoPJDbHcFfUC0M1vUwwDQYJKoZIhvcNAQEL +BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTIxMTIyODEw +MTcxOFoXDTIyMTIyODEwMTcxOFowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA456eHhpbTabo +JD9IurVIakdb4Y1CtM1cWEgeDB/owu+h13pqj+wk/1AlFUNIYKfzJNmP+CoJv5pS +vUeJaMdA7vKUCHPMY7SNoZdaX0eGV4Z9Q7Q6pSkV+heoamojl+Lq9VIVvWnz4ra9 +3xjvJJ4bACyIz7k9u32jAb+v3Rh3axVlPfYJqCx0gU+tcMxb/Lc7HH7ynAjFGc4N +iG7qOqE2nmzRanKw4dMJhkzhNyFQbqtd4DmEzV70XixyztxmbENVfNdvOrCc34/e +JR4q7w5YEGMwUIPip7/zz/itqsrk0x4/VF1lExMOihf8dfYnqdF3+SdywoBf5UC4 +AUyFS/3FgQIDAQABo1MwUTAdBgNVHQ4EFgQUdG+6zhMmsR2yenGz22Iacjeh6BUw +HwYDVR0jBBgwFoAUdG+6zhMmsR2yenGz22Iacjeh6BUwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEAU5eJKGCBsJpMgL6AgrtpY47iT2KtIkeiI5RC +L+2z2pORG2jFzvY+3kcYA+Nj7EwVyBGmn2lL2JCgk3Qr1YsO4IMJ6sZYbDi6I1SR +z14QMYDRWqPY7VoyqiDzdIS9ENWm80gCG4BChSMtEtN2kmjdTOM++Cr4LY/LLhM4 +9aSNfXHTx4kklP1VVc8dGWw+bFtzZUeP6O+ssrFhcse4V6DoQAxYSU4MAAjePhAP +0IS2I3sSzLe/LCsJMPZv0r1q8YQCGBrijAXSnQiu8KFh8hEQusxilIZV9XPDGB98 +EwTT5cbtUtOIbrZ6kdBs49O27xCTymaIuysidFtywwTaDdrc1g== +-----END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/myservice.key b/selfservice/strategy/saml/testdata/myservice.key new file mode 100755 index 000000000000..e7b461f2f228 --- /dev/null +++ b/selfservice/strategy/saml/testdata/myservice.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDjnp4eGltNpugk +P0i6tUhqR1vhjUK0zVxYSB4MH+jC76HXemqP7CT/UCUVQ0hgp/Mk2Y/4Kgm/mlK9 +R4lox0Du8pQIc8xjtI2hl1pfR4ZXhn1DtDqlKRX6F6hqaiOX4ur1UhW9afPitr3f +GO8knhsALIjPuT27faMBv6/dGHdrFWU99gmoLHSBT61wzFv8tzscfvKcCMUZzg2I +buo6oTaebNFqcrDh0wmGTOE3IVBuq13gOYTNXvReLHLO3GZsQ1V81286sJzfj94l +HirvDlgQYzBQg+Knv/PP+K2qyuTTHj9UXWUTEw6KF/x19iep0Xf5J3LCgF/lQLgB +TIVL/cWBAgMBAAECggEAAn9H/s6NN+Hf5B3pn1rDy56yzFuvYqpqG/HWmo1zEUht +vx5xstiFY2OutHgDgEP3b+0PHkrfxoFb7QWu5T5iYPy6UQlsMZ/WefJeJHN1btpj +321Hw24a9p5x05EMiOsNZtmasXRLH66fkKYGYaF2bF8QtS60Fa2AL1G6DTPqg3s4 +T+ijNYPr1xUk5GSh8Ea0DjLhzL6WgSHj+eBKgfEdYPDlOaQaYQuV2OJg9JyqxV6h +/Fa1HDc6RgpIhalLhP+9OqhSr9vmXSzEidzu+WTQSPpabwlVIae30Qh8XT9bYF5v +TElDXv5e5FwFmIJTnhAHyGlpnJ3KzaEHkmGbAxLOQQKBgQD2P4++d0WzrugKnfpz +hMpIVwk4jl1l2LUe3LoKEtF85lj6NjmvUNEPfJ0MIwKAjQYZ9AJWgCPP2/kjDBRv +dwwtSDIjFf79y810MNTGhAKv8nf7Lf5tSiJbvWgwtiiqF/ivUlxOKL9jqc6qj2s9 +psFoPOSAHQz6NqNpGyNza/7+CQKBgQDsojNWLJUXVzeUCMCzF+tn8lgs1aGrjHB7 +ZMHpr5nZCBdXjAzZR6yQH653Fa3OzNnVjq8CiO1ZdvbwW/KgVUHB4Mb/4kJ0Uxbm +WOF7zQjsMleoABFTi5mCcSqEK+u1qnrG8Ful9L6F8WhP7mdDmRXQM3f9rG2NDb1H +/OJuj/LpuQKBgQDK0+31Z069QtsUK62oSv9G+JG6yOC7S/Vbt1lxhLCSnTU620FG +W13n0K+W2JtuATq+U9M9JozY4ApkyMVoTnl0LtxFNA/1QlI3WyVXYlLIVAJpnSfN +I1wLjoZsYQ47lEUdO8yWAFAsqih1Km6duGXkEwvvTn5q9mhA4b6giprc6QKBgQCR +knMcd068ziXdxsitJHDoQHkoE8BiZYIpFuIIHcP6dPTPIdQhsusguqy8i7Sh/Pmh +XCaj25KQMBRX52jKY8iROfOSJSIWp6r1yAXnAEqV655rNqdyCvZD/dRW/SIDXz4q +tmDbJkYy5kDys0oJltqJe7A8eV/nn2UrLRIrTBj22QKBgQCFMmXVRqRje9k0Aqfe +KGYYCEPzeFzY4PzufwoOyhsGkLCwKthf43jXjWy53+u82Od1oKiNCjIhQHOtL720 +mTIhl2AzTJ1VMWoqUIHtGxhaIC3zhDjAaTMHZNDXFU78hPOhcBPtKikh3Hj2bfGG +TK1KTG49VMcWHmYJhJXwVevKAg== +-----END PRIVATE KEY----- diff --git a/selfservice/strategy/saml/testdata/registration.schema.json b/selfservice/strategy/saml/testdata/registration.schema.json new file mode 100644 index 000000000000..c7005d87ce8d --- /dev/null +++ b/selfservice/strategy/saml/testdata/registration.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } + } + } +} diff --git a/selfservice/strategy/saml/testdata/saml.jsonnet b/selfservice/strategy/saml/testdata/saml.jsonnet new file mode 100644 index 000000000000..87103e26bc6b --- /dev/null +++ b/selfservice/strategy/saml/testdata/saml.jsonnet @@ -0,0 +1,17 @@ +local claims = { + email_verified: false +} + std.extVar('claims'); + +{ + identity: { + traits: { + // Allowing unverified email addresses enables account + // enumeration attacks, especially if the value is used for + // e.g. verification or as a password login identifier. + // + // Therefore we only return the email if it (a) exists and (b) is marked verified + // by Discord. + [if "email" in claims && claims.email_verified then "email" else null]: claims.email, + }, + }, +} \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/saml_response.xml b/selfservice/strategy/saml/testdata/saml_response.xml new file mode 100644 index 000000000000..22ea0920a014 --- /dev/null +++ b/selfservice/strategy/saml/testdata/saml_response.xml @@ -0,0 +1,11 @@ +https://idp.testshib.org/idp/shibbolethMIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE +CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX +DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x +EjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308 +kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTv +SPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gf +nqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90Dv +TLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+ +cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ==i/wh2ubXbhTH5W3hwc5VEf4DH1xifeTuxoe64ULopGJ0M0XxBKgDEIfTg59JUMmDYB4L8UStTFfqJk9BRGcMeYWVfckn5gCwLptD9cz26irw+7Ud7MIorA7z68v8rEyzwagKjz8VKvX1afgec0wobVTNN3M1Bn+SOyMhAu+Z4tE=a6PZohc8i16b2HG5irLqbzAt8zMI6OAjBprhcDb+w6zvjU2Pi9KgGRBAESLKmVfBR0Nf6C/cjozCGyelfVMtx9toIV1C3jtanoI45hq2EZZVprKMKGdCsAbXbhwYrd06QyGYvLjTn9iqako6+ifxtoFHJOkhMQShDMv8l3p5n36iFrJ4kUT3pSOIl4a479INcayp2B4u9MVJybvN7iqp/5dMEG5ZLRCmtczfo6NsUmu+bmT7O/Xs0XeDmqICrfI3TTLzKSOb8r0iZOaii5qjfTALDQ10hlqxV4fgd51FFGG7eHr+HHD+FT6Q9vhNjKd+4UVT2LZlaEiMw888vyBKtfl6gTsuJbln0fHRPmOGYeoJlAdfpukhxqTbgdzOke2NY5VLw72ieUWREAEdVXBolrzbSaafumQGuW7c8cjLCDPOlaYIvWsQzQOp5uL5mw4y4S7yNPtTAa5czcf+xgw4MGatcWeDFv0gMTlnBAGIT+QNLK/+idRSpnYwjPO407UNNa2HSX3QpZsutbxyskqvuMgp08DcI2+7+NrTXtQjR5knhCwRNkGTOqVxEBD6uExSjbLBbFmd4jgKn73SqHStk0wCkKatxbZMD8YosTu9mrU2wuWacZ1GFRMlk28oaeXl9qUDnqBwZ5EoxT/jDjWIMWw9b40InvZK6kKzn+v3BSGKqzq2Ecj9yxE7u5/51NC+tFyZiN2J9Lu9yehvW46xRrqFWqCyioFza5bw1yd3bzkuMMpd6UvsZPHKvWwap3+O6ngc8bMBBCLltJVOaTn/cBGsUvoARY6Rfftsx7BamrfGURd8vqq+AI6Z1OC8N3bcRCymIzw0nXdbUSqhKWwbw6P2szvAB6kCdu4+C3Bo01CEQyerCCbpfn/cZ+rPsBVlGdBOLl5eCW8oJOODruYgSRshrTnDffLQprxCddj7vSnFbVHirU8a0KwpCVCdAAL9nKppTHs0Mq2YaiMDo8mFvx+3kan/IBnJSOVL19vdLfHDbZqVh7UVFtiuWv3T15BoiefDdF/aR5joN0zRWf8l6IYcjBOskk/xgxOZhZzbJl8DcgTawD8giJ31SJ1NoOqgrSD4wBHGON4mInHkO0X5+vw1jVNPGF3BwHw0kxoCT3ZKdSsi8O4tlf1y227cf794AGnyQe13O032jYgOmM5qNkET6PyfkyD/h0ufgQq2vJvxSOiRv76Kdg0SeRuNPW9MyjO/5APHl7tBlDBEVq+LWDHl4g9h/bw+Fsi0WN4pLN1Yv9RANWpIsXWyvxTWIZHTuZEjNbHqFKpsefx/oY1b9cSzKR5fQ9vc32e17WykL0O7pwpzV6TrFN874GdmW5lG5zfqnRHUQh1aV2WwBJ74mB4tv/y5rmRjTe5h/rN90kN+eQGeR3eG7XUHLhK/yCV+xq8KKPxNZexcdHGA905rvYokbtmr/jIN5kAMBdlOU8akPAZdSMMh+g/RZo5MO50/gdg6MTpB4onU2FBd54FNDp2fuBUxBsnTqpZXkDcAPEfSBr+z2l8jTRmxMricWyeC55ILgxM4er68n0xYjwb2jyQum3IQq7TSYYU/qjNiH1fQBtdRmBkzXJYYk+9q7C6OZJUdR96ERnTIi93NaYmtpSEvZU9vS6MV1VBOnEf8UzUUT9ibMpP9XDSINX7dN24rKIufSY+3+70orQB07XOWp6++SWKgA+WThaoPhp8sWWMeSZuda/wq6jdVTAB8FOPiP3lNl0BqxagQEPmNxDWXwTplSFSR3SP0e4sHMSjLvysibV9Z87LZa1FG0cWU2hrhiyOLsIWMnd4vdTLaWjhXuGlrDShxSAiI39wsl5RB59E+DXVSTBQAoAkHCKGK69YiMKU9K8K/LeodApgw46oPL08EWvleKPCbdTyjKUADtxfAujR84GMEUz9Aml4Q497MfvABQOW6Hwg54Z3UbwLczDCOZyK1wIwZTyS9w3eTH/6EBeyzhtt4G2e/60jkywHOKn17wQgww2ZsDcukdsCMfo4FV0NzfhSER8BdL+hdLJS3R1F/Vf4aRBEuOuycv2AqB1ZqHhcjZh7yDv0RpBvn3+2rzfzmYIBlqL16d1aBnvL4C03I0J59AtXN9WlfJ8SlJhrduW/PF4pSCAQEyHGprP9hVhaXCOUuXCbjA2FI57NkxALQ2HpCVpXKGw0qO0rYxRYIRlKTl43VFcrSGJdVYOFUk0ZV3b+k+KoxLVSgBjIUWxio/tvVgUYDZsO3M3x0I+0r9xlWZSFFmhwdOFouD+Xy1NPTmgwlUXqZ4peyIE1oVntpcrTJuev2jNScXbU9PG8b589GM4Z09KS/fAyytTFKmUpBuTme969qu0eA7/kBSHAkKvbfj0hsrbkkF9y/rXi8xgcMXNgYayW8MHEhm506AyPIvJAreZL637/BENO1ABdWS1Enj/uGaLM1ED8UY94boh/lMhqa9jALgEOHHxspavexi3HIFwJ55s4ocQnjb4p6op4CRPUdPCfli5st9m3NtQoH9kT1FTRZa9sG8Ybhey5wP17YgPIg9ZZtvlvpSTwCwZxHZ348wXJWhbtId9DyOcIzsyK5HaJcRsp8SQVR5nbRW0pUyC/bFAtX1KOGJmtro/QfmnLG9ksuaZvxP6+bH1K+CibEFIRDllAUFFPiuT+2b3Yp3Tu1VvXokMAgmcB5iFDgTAglw5meJYJ99uIBmj0EVZm8snMhRrHjMPTAYD5kwPK/YDShPFFV3XEIFzLD3iYrzb7sub/Z4gTTELWzzS3bCpYPAh4KWeTih+p7Xj0Xf04nSONHZXsQnNenc+PNae+Zj5iCfJ/PpqhMn61n/YBP7gipYYEtOZYzDtvMz+mytYRUOaZTq3W4Wp64f+XVekn49CLarLm6qPyiz5kJwaT8lJ+VEZDPpS/ChLM4eq90GogJBvK0jxmQ1AGvnKpV2lw9XCudf3PXbaTb+r2QPcihKnmqcEgPgYlN8VLclicNW1WyjBJ+HvDTQPbs1r1/KnBK4O5HTT6ehuHpJsYlBN9vzjsD+ov6SRkBqiGPUg9CoKKmWS6dirxwOXi3OUFzkWFVDyDezfkJAzqkmG0nlEGb9mTHdVDfX010bPJ4ZQzQSyHp7Ht2mATyQwOEem2AMB/RpNwlOKXWIdsQ5p3dHF+kmsJHI8xjEv2GeUa/aXX3MF3fPfUA7La8J8fbnaDLbnEqMCLMfdfc9+kY7EKyqPiE5KFpF0EhQBrHl8SiPuFQCoxvlH2u+ujncW7Z5JiBmMKUWOXUHhIe4NckP1awRsEcfhEs664DqOp9CbLwTXk71hHVBtINylFcf7uBZwjxNW+hCfZEoVEjjs/V4J9QeXCxpTu5TcXxBxwN5zBdkCodNFPLUg+3UicaykaH0+wrGoTu/ugjF9rz7OezMMs3pep+bzLp+yZbFAL/z/yATY3UG+lpk6Rw4SkjbnAxBSedaEdqbotddkGzVQubHvHqCiKpkAw58rAa2v15hc+UmkrRFslS8SYxTIPXs2sTNhnCCrUn8nlKufeoAm65vgYtEQ4NzmG9tqKtTeBfZAvSToYaiQq+kPii1ssuu1OULAVuSx8x/CYO6orgX7h5wI0R/Ug1nux7cb2/+pFLbNyGvwKf1TLym2NvFMJpvFlTsOJJ4DxXM/v2JkC9umm93quXLsojx7KTEOFDQLsnMKsVo6ZzRQidEwK5gQPyZL1yjGirJcEuGMAEf6LA2AsKIIZhsMEPlLpzMiVo5Y0LoL6NFsXigceLaaJMEMuYNJJdh+uxyfW57+PoQ7V8KkzSHFsKan14GnpWeOV7r13uopwCPeIsEKUVG77ypd+ILQkbKxH2lQdsFyjpofqkbgEVM5XAnVbdhfwyebNHn5OJtadVkOMcJc/WMWJef1idcSfvP5ENkwp3pKg9Ljoi+hU2Chp1vTmksO2HJt0of4QnQ8jGlcqnOrAMiWUCd2W/8AmhRBjevt3UqxnqELVvg+HJPlyqFyuUlDxx25mXEdW0COpA3s9OlSgcMjvQbIJ42NUhGFZLoK1pvPLZo711w2Ex3Lm5qqcr/7I4+vTntd/Id5aJiP18LQpslTy614Wd4eD8+RfjEtmDAPXhgvfekVkS/rDnI/9H0k3AdHc78fJCJRPNwJrDTozzjxTvmVv9r4MtpoDELmnMxb3o7ZibUMxgptCTyDF+Q5m6T3GeD9G5ehgB3Tqsx3gcUGuDtP6KIqMGbj8YCFt8tjihDctYFAXj4AwPnIjMiI4T7skXwfrBLWCKfN1j5XrIn2paQgKln9hvaiRUpNpD3IXVyFl1WNrb21IcRinfkuCtrP2tTHqct6eSEh8sOzRkvZEArBQYD5paYyuNBcbVtsnl6PNE+DIcSIGvCVnzpMw1BeUExvQZoNdpHwhTQ3FSd1XN1nt0EWx6lve0Azl/zJBhj5hTdCd2RHdJWDtCZdOwWy/G+4dx3hEed0x6SoopOYdt5bq3lW+Ol0mbRzr1QJnuvt8FYjIfL8cIBqidkTpDjyh6V88yg1DNHDOBBqUz8IqOJ//vY0bmQMJp9gb+05UDW7u/Oe4gGIODQlswv534KF2DcaXW9OB7JQyl6f5+O8W6+zBYZ6DAL+J2vtf3CWKSZFomTwu65vrVaLRmTXIIBjQmZEUxWVeC4xN+4Cj5ORvO8GwzoePGDvqwKzrKoupSjqkL5eKqMpCLouOn8n/x5UWtHQS1NlKgMDFhRObzKMqQhS1S4mz84F3L492GFAlie0xRhywnF+FvAkm+ZIRO0UqM4IwvUXdlqTajjmUz2T0+eXKTKTR5UoNRgP51gdUMT5A4ggT5wU9WkRx7CR9KdWJwwcWzv2YrchoHIXBidQSk+f1ZSzqR7krKSOwFTVJUvEenU17qVaHoAf2he0dMgURJ8PM9JxnSr7p2pZeNPu/O5oPmLuOCmEPVRPSahJL7yj9PK5z3q57e5POIp/wXqFoniFdxRmtmpfZBxoKVlADkwRy34h8k6ZmgtqPTQfUUk/+yH2CAoQu+HyOtUnQof8vc1k4zs8nCTrCSjqvFPjU8mHtVHy1RY0qmK9t99ugXyAKaGON3PlseetIC8WCTt84nM5XGD3VQpbv139yhSPhp2Oiz0IiOsr+L9idVKSvfNSkdNq9aUC7963uAQNud8c4GuDmbENvZYvGNIMxxZhYA86n1RMNtGDZJs6/4hZTL18Kz1yCY9zbbSXTxWTmkaHJziHtgrEPoYpUeb85J229PDEX08yHOkj2HXVdnKKmEaHw3VkB4eM3PhGGdrw2CSUejSaqPQFLdhabcB2zdB4lj/AUnZvNaJc23nHHIauHnhhVrxh/KQ1H4YaYKT9ji/69BIfrTgvoGaPZC10pQKinBHEPMXoFrCd1RX1vutnXXcyT2KTBP4GG+Or0j6Sqxtp5WhxR0aJqIKM6LqMHtTooI0QhWbmSqDEBX/wRS70csVeJSrZ4dqRKit+hz8OalHA7At9e+7gSWTfHAwjl5JhtrltyAab/FII4yKQeZWG8j1fSFGHN+EbOrum2uWuVhxkUPy4coMu+yKY4GxlXfvP+yEVK5GrMECRmFBlySetJK3JOoQXiuLirlHUq+0u88QFMdAJ9+fIdU4+FxneqgW7qM7CHRE8jV4pPSWGFbGzxVZ9CWRWaYIw26VsC1qQJe1WmU7Mrp26IxmWHGwHvZ50uB0mjAHFCiln5QAvqTm2/fsY+Puk+Irt3LQbMwGVWPnb4eona2dSha+eMLOiAQkBvbaitsRqqrAVnndP7gHmO+nYZEKNx/740zTRrFBpOelrGdOa0/eV2mPhUQfozGooxoRADmT8fAcDXo0SsXCHzg9tBnmVMvInQ7+8nXfhcF/fEBjvW3gIWOmp2EWutHQ/sl73MieJWnP/n3DMk2HHcatoIZOMUzo4S4uztODHoSiOJDA1hVj7qADvKB37/OX0opnbii9o6W8naFkWG5Ie7+EWQZdo+xeVYpwGOzcNwDRrxbZpV3fTvWyWKToovncZq+TQj7c4Yhz6XDF0ffljN5hTm4ONwYViFNB4gTJlFxFX00wcWfwWah4uJs2Oa8dHPVT+7viagZiPrSDk/gythdY8glGm+F0DWlzQpWbgSI3ZbdiUQ+ox4GtLUtYgGIQFUvRYbuHqH6CXQ3SM6vkbhV/nAn6UDEWKXdJsO0u5q6UpXci7MlWDNLxoQ9dfGjSc28mX+q+4hkyho4u1XSMy9B6IdH304J7fuAQ88tTorT67AiqvqR6qnZ0icV+MMLh95moxFbrvch6sGAmMEixqeujmiZzBqBmNbzZVORiv9qcbe3CQ6X2i+9D8hMpaWj5jI0u+0wk3bRFK4uDn8T1mnD6l4TrJayf3cZI+duhKcabNj71i5w76S8RZSC6RX4ks0x+XIDc5v3223NmGvceYklbuOJtJa0/MBTOcSDKCM2kUXqPV2BlA9Za8WEO2UrdcyP+AXgM20af3thjlZvA494zdZ0mqjrsKp+VS2MVrBBtj+puSuSHJYf6bnA5/yjqQtbGvAp8hfXQURC53J5oD8rb9F7vQRqdfqpe6xd7DVd+wWZS86mWjyZYKXw312t8nM/gxo0pdvZ8F0x9y3xb9UBM2pZtdYvk3hPz6swhuE1N5j2u7nwtXuEDNcGCSfr+IempeFHFRqO8n8ikASEdKcq2XHGJwfc3lVXOQ5K4JlewcC7yQL1uNtL6iNKCtJmjJiH2PMmXrtpmCeTspFNZlwmiICyPWV9B5ce9H/qP1xjndBzFz0rn75SGDnWUhNZI/aYKNVyzkOleS5VSNxBx1hoiFuG8r+6ctYwF7XL94b95tXQ/+0V5dt0H1xVaOZ7QluoDtMSzuUjV4yUoQESa3zCfZwnW+b5SKndX5nx0GYrVxydMkUdfimZpX/fezcMiaAGwG/jgWF0zS+EL4T7gR8I5R3qUNTifKFJKJL1+AL8CgL+SRB1lgHDp2wQ7cqgqcmskAsT60qisL/UZGgmnlgZ8FkNhv0vAMkzIsz7o6cuLo15hZnrsZveIo+mZKY2cMJjJb4ZlJLcE+YcnpiM84OYjypa9lA7kv4XJaDX9oirhsl9IO/ImbFgYpR73y+xSolXYdDKfZjf/8NR7vE8fu+LYXGoZHO/hxousED6y3sCo/ItECYHWYIui+V5SmAoEvVV8FY8fFMYIc+Llc2CoX5HQISfUAtLu+fGNNV0muidXnBdtnJo25UEqxwvoENdI1lGPhlrXY6/h4kIT5djmsxxSG/EgG/4fPnrThgF9/fbG8n/3LweXvQOGjX0F1Ngt5wuMIWRQk5vtLdvv2M+BNwthHZ7xzIU7zqSVvngVPwgcsTr2d5pTVOxauT1K6ffiBF04jVZEcna+NXhJM5EcRHNuT/iOb0ncn1yuKU8JJnztEzMDjO1qCmaBTyWBR7nQS6K+nfstd/AnBWyGeC5Yi3wlvZAVMpc0m7I7McXb+rXiHM0mHoq0Z/2HOki5LP2cBuIkk84tJ3SRZwWnocrz4aTEIOmwftqMATy5Ur0KRxoUSFNMJYyc1iOfjk3H2JjgecWlQdYHcIEjxGDGeo4S9EKTRokMGNUN2nTj3SO2nHoWbx9WhGe6uB3OgDENGL9aNoPnYKXs4WcobctMxQjjBWa/zpCFwP8nr78xIFfy/64ZtsFBrxSrEHxeXiPa2Kpv456aQ9kDQjJt9XrWKe+JBawtpPUYHmWkUb3Gznp3tC2LbowvJlEe/17srb5yi+sUHEF1z/8Uk4eVYcUUXzyq3YEuqumIBIYqO8J3K5Us7tEXyzhHH8TMLNSQxmDi/w5oYccIwNFMM1+xRTsyjHHtB/rHYJjPW/50Xxb0CZF84NqotCcgIMrR4nUiPnAPd8ZvHeB/235gS1NtzBWtfcDmP8khibSQpY3JW+fdY/9W6iGlPyPIwOgH06fJayaT44sPFIm+QGIkPKSAJOFDeJNG8oc6SAqrYSfCffYfOAx3IsjSdnxQy9JAcS0HxjWnEO3rgSh7bNEecO3f4hb3TRNlczdzhfrwgxUZ0rURI3LfMCpGntF+8NrhtB7RT8sEOaa4NM13T7LWjykRQJFYKNZY0siPBP2WJxjBqL0KynlTPhAcfFyiLZbAhe7YC0XmYo8iJQqdzJQwBK9iOoDkg1XuGy7+Kfe0scamvHN2Z85umcPSiPEQRP3zAWcP5kRNDath7DKrBfQtvOJvEHiihE+qiASrCZep+m7jTD261U9vQGAnR4xBY08ChSh8XItWHvDHARN+GP08h9u6nlJ3rpOoVn9y22NNgx7bOe6QIYe9f6iYbbAzLR1/7AP1A4CQwFi39eZI9BZteze5eas+6JR2s1LqH9tncOmWAhXjE8p3hOtplh/tMbrx+pySNX4BKfZva54zccIa+e59NUifTRsq27AwAtcxg2Bk1Tu7B+LT9Yw2K8tRH6XTcGlvqDM4sYjNBqzh3yAga5iro706tg/Qaa50eln8rjISularEHlfaggogjvd+wNLg44Rj8pMr25+xxS0e9KoEGon5SutuhJ/HBGnEj3+4qNxHu27nkAmZIADiF+Jh53osDuA1fsUnRXf2lJABa30KDkG8E/eci+TkESrdfsPMo6yhWoyjtjYdJbGkjtsQCMW5DOSNYDH0FqDiiVU0nBLJ4+A4ep6aWTrv6w/ozuO4educ7x9IBpGmEY30rsXWwiGJbLGyIo+6qz6J5JBKdjNBsDO7RRweDNMp8ospaGNQSa4NKAHTG8BsGqJSP8oebpVqYpgPS1TiBWnYZKQSRJ5NFs+ULpdICekxevVXAH8uh+De9GT7KsJJzg0CFjALDbC0YrbmCigspJAh2455I6/xyWbPXCYMXwBzbioMgWcNhQBJJ6oIoQ7shwf2TP0Z+X/3NoMpWHmGpoV/JZind8lb9lcxoI44uf37+xc03O1R1bNucf0F5ljrgj2sZlGz/591EJen5GZhrT6qSTIcMu+xIyxyA/zzhy0jjkVfkDKfQ8mE9AmVtbbzHAQNy2PhDIeu7ngoFN635tSOJLR2c6pC/m6n50slFbo0oeHbbiGHyxDk7q3zXHWoHzeF1k4iVdHumYg/nwZOuRzms6rvkmwkJv59Z1p05jxA+Y0yHvDeq1WR8PfS/esm3RHfP3fM+zTlj9ZBJfzvn4OL+IIHRQ5l8pGKAeRL58OjeaU5QU98lAKHydOPDGBalsEHyIKD6iy3RZ65qIm956zQd98htZ1Vgkd7LVC7LSnLb9jRbqS1vHN7lR6bQMmXtQBYSA/+ZW2RQqSo7sToVh+Pxl3EVmsgyO8dXPL4biz7XM8eVz7CqHkrQUinnr79HJWC6Uk19cBurOD6PeOqNYy08Og/A0hbHOgN3dKmVRAPf7itK6x0eb5F70T2zVqG12GHVZieXwIcp/vahuFvriHLJtuM04laiRWNXSiL2MPHQ8e9rr8NIlWDm9uev55FI9zZxwFUPBSewawPe5vkqRLfwZCYd5mZoxtBhNBWvY3ZOVD/21dIUlQanG1n6RygbmAwCHnIB4c7EH2CBYEMDToRQuAuIssviIfdaJglwDgHbLWKNUVDOdqeclBNZjfQfVXbVukPk8DfWLqj9pD4xAOzDeVQcdmg2aLvNKgpZsWs4d+6GlKrpS7qEGvoBkIFh/cVY7DMYrt/JXYuF6DpwB+HbfnuDFc2p47SPNhnmt/ez6/DACBPQ+tgpyWYXUsiviGSp72JNTzd8uFJJZNeKUJZw1c0UTjxdwigh5tL/hWhPl48DY937zymSr1xVqC3RV6wSIpuplH+hss/rsRPAp1/TfxvhJuFsoPbW0586y9YzqEHT4FUu6WSRy0gMJLP2sLqiiZXZ6kPicXsW7M55mV3ugbGQjB7YS7EVqsQzvJTiQbOlcPqwoKK7DTqaeCOXd8kH1tNoe7hjx/UNNdLQQ7IhrJIzxqTTgwcXYMCxhoezDsIHReTIymsHPkCurfteTQcbfwoKN5E9zC2hINOPmhAxLvONzaLXQGMqofuTbFshkB4eUj8U4vBCNp+60iCLnibt4rPuyoWKEHWBYa6FfIykxVKuXkfcb64dCdGCWjv7x1XqkbpHxQB80qhipoSo244pyhIsN91ASu1Q7L75LxGXibY3jb0Y4KZ5zIWsH4kVlvPhangohDO1J9gmL9inGr9hy5BHTQiMcktGoUgOIbFJ72381vYpPxn3ngBbp48mVZd0w6xV8RBaqR3l7CxI9vvMAPYPoXBB18ERoZypza8mAlzv2QxIkNGuRzFENh1SXegBfN7eiazZnwnhbyeMghJpnXzfvHACyjkdH3shRYcJ+oMiOSpInGxm/hxFQxHJZA0Ft/lza \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/samlkratos.crt b/selfservice/strategy/saml/testdata/samlkratos.crt new file mode 100755 index 000000000000..3dfdeb703e1c --- /dev/null +++ b/selfservice/strategy/saml/testdata/samlkratos.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUVREfiVXf4z/hq8AsbyNnkuWn6i8wDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjAyMjExMTA4MjBaFw0yMzAy +MjExMTA4MjBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCjvij3wZV+OhbEbwcs7cpc1hGR+uK4Y0y/ItHkAqlV +ddl+D28iDJeHci4LA8XmG0loFMTxdC9PG5t4ewn8G18+EeYRV0K3BMMWfxrO6ibG +z1ElTxQvVSw9tgPpjIgZqL8Qso8UO1ji98yoPhqP77F29pCNqiHrKJI1c52OCPHq +NBCZa76DmCGcXKAwRQaTo+tig6HJ1/3qCLGq57O396mQRFvjB535mceLzKSpFHsh +45beytXiBjTkvOEmNIUGVKIidXxqDtuTHz5QqhHTHMSsFH8cT648sSB9K9jPZ6ai +VCq5z/McyaYFlb/wt7PApJTSRjU0Any4876eBca59ca/AgMBAAGjUzBRMB0GA1Ud +DgQWBBQml5ORluABegdU+rLlpn++esD9fjAfBgNVHSMEGDAWgBQml5ORluABegdU ++rLlpn++esD9fjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCL +X5bpRKtMY7FsPtMsO/KBz5GT7P6aqe8pS0m3uXap6KkQwxa2wyyyH+in6uds8Sxm +bsdsGpSpCfGQCMqmu0yCjhfwI8nFA6q1YxLNgmx7kEIAQQQG2+jZJE7adXzSk2vT +tiNQ55mfiO9Wv+JpaB7ldAX3Q+O2uqVLJG/NlvC3ZAq0FXMyeitddLYSmEE0xrcM +QTB7vb7LpZk7Owa2UJ2VcQyZcxLWMonikIg4u3ALHGR0SvEgMwGhWr354RDGLYSO +Ii5O1foUR1O71jffr7CgELauyz3AXv6PNYLkyOCQP5gNB2NEMLJBRn5U4IhCHKzD +t1/BujsTuZV5r6aj3J9+ +-----END CERTIFICATE----- diff --git a/selfservice/strategy/saml/types.go b/selfservice/strategy/saml/types.go new file mode 100644 index 000000000000..9a38bcc64ea0 --- /dev/null +++ b/selfservice/strategy/saml/types.go @@ -0,0 +1,29 @@ +package saml + +import ( + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/stringsx" +) + +type FlowMethod struct { + *container.Container +} + +func AddProviders(c *container.Container, providers []Configuration, message func(provider string) *text.Message) { + for _, p := range providers { + AddProvider(c, p.ID, message( + stringsx.Coalesce(p.Label, p.ID))) + } +} + +func AddProvider(c *container.Container, providerID string, message *text.Message) { + c.GetNodes().Append( + node.NewInputField("samlProvider", providerID, node.SAMLGroup, node.InputAttributeTypeSubmit).WithMetaLabel(message), + ) +} + +func NewFlowMethod(f *container.Container) *FlowMethod { + return &FlowMethod{Container: f} +} diff --git a/spec/api.json b/spec/api.json index 24a7ed80f033..6c9775bdccbc 100755 --- a/spec/api.json +++ b/spec/api.json @@ -1618,6 +1618,23 @@ ], "type": "object" }, + "selfServiceSamlUrl": { + "properties": { + "saml_acs_url": { + "description": "SamlAcsURL is a post endpoint to handle SAML Response\n\nformat: uri", + "type": "string" + }, + "saml_metadata_url": { + "description": "SamlMetadataURL is a get endpoint to get the metadata\n\nformat: uri", + "type": "string" + } + }, + "required": [ + "saml_metadata_url", + "saml_acs_url" + ], + "type": "object" + }, "selfServiceFlowExpiredError": { "description": "Is sent when a flow is expired", "properties": { @@ -5044,6 +5061,48 @@ ] } }, + "/self-service/methods/saml/auth": { + "get": { + "description": "This endpoint initiates a registration flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error\nwill be returned unless the URL query parameter `?refresh=true` is set.\n\nTo fetch an existing registration flow call `/self-service/registration/flows?flow=\u003cflow_id\u003e`.\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nIn the case of an error, the `error.id` of the JSON response body can be one of:\n\n`session_already_available`: The user is already signed in.\n`security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "operationId": "initializeSelfServiceSamlFlowForBrowsers", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/selfServiceRegistrationFlow" + } + } + }, + "description": "selfServiceRegistrationFlow" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + } + }, + "summary": "Initialize Registration Flow for APIs, Services, Apps, ...", + "tags": [ + "v0alpha2" + ] + } + }, "/self-service/recovery": { "post": { "description": "Use this endpoint to complete a recovery flow. This endpoint\nbehaves differently for API and browser flows and has several states:\n\n`choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent\nand works with API- and Browser-initiated flows.\nFor API clients and Browser clients with HTTP Header `Accept: application/json` it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid.\nand a HTTP 303 See Other redirect with a fresh recovery flow if the flow was otherwise invalid (e.g. expired).\nFor Browser clients without HTTP Header `Accept` or with `Accept: text/*` it returns a HTTP 303 See Other redirect to the Recovery UI URL with the Recovery Flow ID appended.\n`sent_email` is the success state after `choose_method` for the `link` method and allows the user to request another recovery email. It\nworks for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state.\n`passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow (\"sending a recovery link\")\ndoes not have any API capabilities. The server responds with a HTTP 303 See Other redirect either to the Settings UI URL\n(if the link was valid) and instructs the user to update their password, or a redirect to the Recover UI URL with\na new Recovery Flow ID which contains an error message that the recovery link was invalid.\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery).", diff --git a/spec/swagger.json b/spec/swagger.json index ba7757488e67..13a2cede5743 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1705,6 +1705,40 @@ } } }, + "/self-service/methods/saml/auth": { + "get": { + "description": "This endpoint initiates a registration flow for API clients such as mobile devices, smart TVs, and so on.\n\nIf a valid provided session cookie or session token is provided, a 400 Bad Request error\nwill be returned unless the URL query parameter `?refresh=true` is set.\n\nTo fetch an existing registration flow call `/self-service/registration/flows?flow=\u003cflow_id\u003e`.\n\nYou MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server\nPages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make\nyou vulnerable to a variety of CSRF attacks.\n\nIn the case of an error, the `error.id` of the JSON response body can be one of:\n\n`session_already_available`: The user is already signed in.\n`security_csrf_violation`: Unable to fetch the flow because a CSRF violation occurred.\n\nThis endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).\n\nMore information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).", + "schemes": [ + "http", + "https" + ], + "tags": [ + "v0alpha2" + ], + "summary": "Initialize Registration Flow for APIs, Services, Apps, ...", + "operationId": "initializeSelfServiceSamlFlowForBrowsers", + "responses": { + "200": { + "description": "selfServiceRegistrationFlow", + "schema": { + "$ref": "#/definitions/selfServiceRegistrationFlow" + } + }, + "400": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + }, + "500": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + } + } + } + }, "/self-service/recovery": { "post": { "description": "Use this endpoint to complete a recovery flow. This endpoint\nbehaves differently for API and browser flows and has several states:\n\n`choose_method` expects `flow` (in the URL query) and `email` (in the body) to be sent\nand works with API- and Browser-initiated flows.\nFor API clients and Browser clients with HTTP Header `Accept: application/json` it either returns a HTTP 200 OK when the form is valid and HTTP 400 OK when the form is invalid.\nand a HTTP 303 See Other redirect with a fresh recovery flow if the flow was otherwise invalid (e.g. expired).\nFor Browser clients without HTTP Header `Accept` or with `Accept: text/*` it returns a HTTP 303 See Other redirect to the Recovery UI URL with the Recovery Flow ID appended.\n`sent_email` is the success state after `choose_method` for the `link` method and allows the user to request another recovery email. It\nworks for both API and Browser-initiated flows and returns the same responses as the flow in `choose_method` state.\n`passed_challenge` expects a `token` to be sent in the URL query and given the nature of the flow (\"sending a recovery link\")\ndoes not have any API capabilities. The server responds with a HTTP 303 See Other redirect either to the Settings UI URL\n(if the link was valid) and instructs the user to update their password, or a redirect to the Recover UI URL with\na new Recovery Flow ID which contains an error message that the recovery link was invalid.\n\nMore information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery).", @@ -4695,8 +4729,25 @@ } } }, - "uiContainer": { - "description": "Container represents a HTML Form. The container can work with both HTTP Form and JSON requests", + "selfServiceSamlUrl": { + "type": "object", + "required": [ + "saml_metadata_url", + "saml_acs_url" + ], + "properties": { + "saml_acs_url": { + "description": "SamlAcsURL is a post endpoint to handle SAML Response\n\nformat: uri", + "type": "string" + }, + "saml_metadata_url": { + "description": "SamlMetadataURL is a get endpoint to get the metadata\n\nformat: uri", + "type": "string" + } + } + }, + "selfServiceSettingsFlow": { + "description": "This flow is used when an identity wants to update settings\n(e.g. profile data, passwords, ...) in a selfservice manner.\n\nWe recommend reading the [User Settings Documentation](../self-service/flows/user-settings)", "type": "object", "required": [ "action", diff --git a/ui/node/node.go b/ui/node/node.go index c1c2aa64f1c0..345a4da76dea 100644 --- a/ui/node/node.go +++ b/ui/node/node.go @@ -42,6 +42,7 @@ const ( DefaultGroup UiNodeGroup = "default" PasswordGroup UiNodeGroup = "password" OpenIDConnectGroup UiNodeGroup = "oidc" + SAMLGroup UiNodeGroup = "saml" ProfileGroup UiNodeGroup = "profile" LinkGroup UiNodeGroup = "link" CodeGroup UiNodeGroup = "code" diff --git a/x/provider.go b/x/provider.go index f87f3f0fdc95..ec2ae5736ce5 100644 --- a/x/provider.go +++ b/x/provider.go @@ -29,6 +29,11 @@ type CookieProvider interface { ContinuityCookieManager(ctx context.Context) sessions.StoreExact } +type RelayStateProvider interface { + RelayStateManager(ctx context.Context) sessions.StoreExact + ContinuityRelayStateManager(ctx context.Context) sessions.StoreExact +} + type TracingProvider interface { Tracer(ctx context.Context) *otelx.Tracer } diff --git a/x/relaystate.go b/x/relaystate.go new file mode 100644 index 000000000000..a28c97024d58 --- /dev/null +++ b/x/relaystate.go @@ -0,0 +1,51 @@ +package x + +import ( + "net/http" + + "github.com/gorilla/sessions" + "github.com/pkg/errors" +) + +// SessionGetRelayState returns a string of the content of the relaystate for the current session. +func SessionGetStringRelayState(r *http.Request, s sessions.StoreExact, id string, key interface{}) (string, error) { + + cipherRelayState := r.PostForm.Get("RelayState") + if cipherRelayState == "" { + return "", errors.New("The RelayState is empty or not exists") + } + + // Reconstructs the cookie from the ciphered value + continuityCookie := &http.Cookie{ + Name: id, + Value: cipherRelayState, + MaxAge: 300, + } + + r2 := r.Clone(r.Context()) + r2.AddCookie(continuityCookie) + + check := func(v map[interface{}]interface{}) (string, error) { + vv, ok := v[key] + if !ok { + return "", errors.Errorf("key %s does not exist in cookie: %+v", key, id) + } else if vvv, ok := vv.(string); !ok { + return "", errors.Errorf("value of key %s is not of type string in cookie", key) + } else { + return vvv, nil + } + } + + var exactErr error + sessionCookie, err := s.GetExact(r2, id, func(s *sessions.Session) bool { + _, exactErr = check(s.Values) + return exactErr == nil + }) + if err != nil { + return "", err + } else if exactErr != nil { + return "", exactErr + } + + return check(sessionCookie.Values) +} From 6e81af40e72711e80b90047e2a2a6395e741306e Mon Sep 17 00:00:00 2001 From: ThibaultHerard Date: Fri, 25 Nov 2022 16:14:12 +0000 Subject: [PATCH 2/8] feat(saml): use ory/x fetcher Signed-off-by: ThibaultHerard Co-authored-by: sebferrer --- selfservice/strategy/saml/handler.go | 42 +++++++++---------- selfservice/strategy/saml/handler_test.go | 6 ++- selfservice/strategy/saml/metadata_test.go | 8 +++- .../strategy/saml/strategy_helper_test.go | 6 ++- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/selfservice/strategy/saml/handler.go b/selfservice/strategy/saml/handler.go index 1e191a872202..bfcc82c0acf5 100644 --- a/selfservice/strategy/saml/handler.go +++ b/selfservice/strategy/saml/handler.go @@ -11,7 +11,6 @@ import ( "io/ioutil" "net/http" "net/url" - "path/filepath" "strings" "github.com/pkg/errors" @@ -29,6 +28,7 @@ import ( "github.com/ory/kratos/session" "github.com/ory/kratos/x" "github.com/ory/x/decoderx" + "github.com/ory/x/fetcher" "github.com/ory/x/jsonx" ) @@ -168,29 +168,20 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // The metadata file is provided metadataURL := providerConfig.IDPInformation["idp_metadata_url"] - if strings.HasPrefix(metadataURL, "file://") { - metadataURL = strings.Replace(metadataURL, "file://", "", 1) - metadataURL = filepath.Clean(metadataURL) - metadataPlainText, err := ioutil.ReadFile(metadataURL) - if err != nil { - return err - } - idpMetadata, err = samlsp.ParseMetadata([]byte(metadataPlainText)) - if err != nil { - return err - } + metadataBuffer, err := fetcher.NewFetcher().Fetch(metadataURL) + if err != nil { + return err + } - } else { - idpMetadataURL, err := url.Parse(metadataURL) - if err != nil { - return err - } - // Parse the content of metadata file into a Golang struct - idpMetadata, err = samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL) - if err != nil { - return err - } + metadata, err := ioutil.ReadAll(metadataBuffer) + if err != nil { + return err + } + + idpMetadata, err = samlsp.ParseMetadata(metadata) + if err != nil { + return err } } else { @@ -214,7 +205,12 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi } // The certificate of the IDP - certificate, err := ioutil.ReadFile(strings.Replace(providerConfig.IDPInformation["idp_certificate_path"], "file://", "", 1)) + certificateBuffer, err := fetcher.NewFetcher().Fetch(providerConfig.IDPInformation["idp_certificate_path"]) + if err != nil { + return err + } + + certificate, err := ioutil.ReadAll(certificateBuffer) if err != nil { return err } diff --git a/selfservice/strategy/saml/handler_test.go b/selfservice/strategy/saml/handler_test.go index bc7ac906c654..c0d15540da57 100644 --- a/selfservice/strategy/saml/handler_test.go +++ b/selfservice/strategy/saml/handler_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/x/fetcher" "github.com/stretchr/testify/require" "gotest.tools/assert" @@ -73,7 +74,10 @@ func TestMustParseCertificate(t *testing.T) { saml.DestroyMiddlewareIfExists("samlProvider") - certificate, err := ioutil.ReadFile("testdata/samlkratos.crt") + certificateBuffer, err := fetcher.NewFetcher().Fetch("file://testdata/samlkratos.crt") + require.NoError(t, err) + + certificate, err := ioutil.ReadAll(certificateBuffer) require.NoError(t, err) cert, err := saml.MustParseCertificate(certificate) diff --git a/selfservice/strategy/saml/metadata_test.go b/selfservice/strategy/saml/metadata_test.go index 6f9d8b20f7a6..8a5bce69c115 100644 --- a/selfservice/strategy/saml/metadata_test.go +++ b/selfservice/strategy/saml/metadata_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/x/fetcher" "github.com/stretchr/testify/require" "gotest.tools/assert" is "gotest.tools/assert/cmp" @@ -95,8 +96,11 @@ func TestXmlMetadataValues(t *testing.T) { assert.Check(t, is.Equal("text/xml", res.Header.Get("Content-Type"))) - expectedMetadata, err := ioutil.ReadFile("./testdata/expected_metadata.xml") - assert.NilError(t, err) + expectedMetadataBuffer, err := fetcher.NewFetcher().Fetch("file://testdata/expected_metadata.xml") + require.NoError(t, err) + + expectedMetadata, err := ioutil.ReadAll(expectedMetadataBuffer) + require.NoError(t, err) // The string is parse to a struct var expectedStructMetadata Metadata diff --git a/selfservice/strategy/saml/strategy_helper_test.go b/selfservice/strategy/saml/strategy_helper_test.go index 5c40766f10c4..3fb92f5ee3f0 100644 --- a/selfservice/strategy/saml/strategy_helper_test.go +++ b/selfservice/strategy/saml/strategy_helper_test.go @@ -31,6 +31,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/strategy/saml" "github.com/ory/kratos/x" + "github.com/ory/x/fetcher" ) var TimeNow = func() time.Time { return time.Now().UTC() } @@ -162,7 +163,10 @@ func InitTestMiddlewareWithoutMetadata(t *testing.T, idpSsoUrl string, idpEntity func GetAndDecryptAssertion(t *testing.T, samlResponseFile string, key *rsa.PrivateKey) (*crewjamsaml.Assertion, error) { // Load saml response test file - samlResponse, err := ioutil.ReadFile(samlResponseFile) + samlResponseBuffer, err := fetcher.NewFetcher().Fetch("file://" + samlResponseFile) + require.NoError(t, err) + + samlResponse, err := ioutil.ReadAll(samlResponseBuffer) require.NoError(t, err) // Decrypt saml response assertion From 3c42efe37a13729115a4de45fa6fe142c3752829 Mon Sep 17 00:00:00 2001 From: ThibaultHerard Date: Fri, 25 Nov 2022 17:52:34 +0000 Subject: [PATCH 3/8] feat(saml): improved error handling Signed-off-by: ThibaultHerard Co-authored-by: sebferrer --- selfservice/strategy/saml/config_test.go | 2 +- selfservice/strategy/saml/error.go | 28 ++++++++++- selfservice/strategy/saml/handler.go | 61 +++++++++++++----------- selfservice/strategy/saml/strategy.go | 14 +++--- 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/selfservice/strategy/saml/config_test.go b/selfservice/strategy/saml/config_test.go index d69274920aef..a6d90c8e5d19 100644 --- a/selfservice/strategy/saml/config_test.go +++ b/selfservice/strategy/saml/config_test.go @@ -102,7 +102,7 @@ func TestInitSAMLWithoutPoviderID(t *testing.T) { resp, _ := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") body, _ := ioutil.ReadAll(resp.Body) - assert.Contains(t, string(body), "\"code\":404,\"status\":\"Not Found\"") + assert.Contains(t, string(body), "Invalid SAML configuration in the configuration file") } func TestInitSAMLWithoutPoviderLabel(t *testing.T) { diff --git a/selfservice/strategy/saml/error.go b/selfservice/strategy/saml/error.go index ab984670e1a0..68031dff7bda 100644 --- a/selfservice/strategy/saml/error.go +++ b/selfservice/strategy/saml/error.go @@ -1,6 +1,11 @@ package saml -import "github.com/ory/herodot" +import ( + "net/http" + + "github.com/ory/herodot" + "google.golang.org/grpc/codes" +) var ( ErrScopeMissing = herodot.ErrBadRequest. @@ -13,4 +18,25 @@ var ( ErrAPIFlowNotSupported = herodot.ErrBadRequest.WithError("API-based flows are not supported for this method"). WithReason("SAML SignIn and Registeration are only supported for flows initiated using the Browser endpoint.") + + ErrInvalidSAMLMetadataError = herodot.DefaultError{ + StatusField: http.StatusText(http.StatusOK), + ErrorField: "Not valid SAML metadata file", + CodeField: http.StatusOK, + GRPCCodeField: codes.InvalidArgument, + } + + ErrInvalidCertificateError = herodot.DefaultError{ + StatusField: http.StatusText(http.StatusOK), + ErrorField: "Not valid certificate", + CodeField: http.StatusOK, + GRPCCodeField: codes.InvalidArgument, + } + + ErrInvalidSAMLConfiguration = herodot.DefaultError{ + StatusField: http.StatusText(http.StatusOK), + ErrorField: "Invalid SAML configuration in the configuration file", + CodeField: http.StatusOK, + GRPCCodeField: codes.InvalidArgument, + } ) diff --git a/selfservice/strategy/saml/handler.go b/selfservice/strategy/saml/handler.go index bfcc82c0acf5..6ffac7ae70fc 100644 --- a/selfservice/strategy/saml/handler.go +++ b/selfservice/strategy/saml/handler.go @@ -19,6 +19,7 @@ import ( "github.com/crewjam/saml/samlsp" "github.com/julienschmidt/httprouter" + "github.com/ory/herodot" "github.com/ory/kratos/continuity" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/selfservice/errorx" @@ -154,34 +155,33 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // Key pair to encrypt and sign SAML requests keyPair, err := tls.LoadX509KeyPair(strings.Replace(providerConfig.PublicCertPath, "file://", "", 1), strings.Replace(providerConfig.PrivateKeyPath, "file://", "", 1)) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } var idpMetadata *samlidp.EntityDescriptor // We check if the metadata file is provided if providerConfig.IDPInformation["idp_metadata_url"] != "" { - // The metadata file is provided metadataURL := providerConfig.IDPInformation["idp_metadata_url"] metadataBuffer, err := fetcher.NewFetcher().Fetch(metadataURL) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } metadata, err := ioutil.ReadAll(metadataBuffer) if err != nil { - return err + return herodot.ErrInternalServerError.WithTrace(err) } idpMetadata, err = samlsp.ParseMetadata(metadata) if err != nil { - return err + return ErrInvalidSAMLMetadataError.WithTrace(err) } } else { @@ -189,36 +189,36 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // So were are creating a minimalist IDP metadata based on what is provided by the user on the config file entityIDURL, err := url.Parse(providerConfig.IDPInformation["idp_entity_id"]) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } // The IDP SSO URL IDPSSOURL, err := url.Parse(providerConfig.IDPInformation["idp_sso_url"]) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } // The IDP Logout URL IDPlogoutURL, err := url.Parse(providerConfig.IDPInformation["idp_logout_url"]) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } // The certificate of the IDP certificateBuffer, err := fetcher.NewFetcher().Fetch(providerConfig.IDPInformation["idp_certificate_path"]) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } certificate, err := ioutil.ReadAll(certificateBuffer) if err != nil { - return err + return herodot.ErrInternalServerError.WithTrace(err) } // We parse it into a x509.Certificate object IDPCertificate, err := MustParseCertificate(certificate) if err != nil { - return err + return ErrInvalidCertificateError.WithTrace(err) } // Because the metadata file is not provided, we need to simulate an IDP to create artificial metadata from the data entered in the conf file @@ -238,7 +238,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // The main URL rootURL, err := url.Parse(config.SelfServiceBrowserDefaultReturnTo(ctx).String()) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } // Here we create a MiddleWare to transform Kratos into a Service Provider @@ -263,7 +263,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi }, }) if err != nil { - return err + return herodot.ErrInternalServerError.WithTrace(err) } // It's better to use SHA256 than SHA1 @@ -278,7 +278,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi u, err := url.Parse(publicUrlString + RouteSamlAcsWithSlash) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } samlMiddleWare.ServiceProvider.AcsURL = *u @@ -287,7 +287,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi publicUrlString = publicUrlString[:len(publicUrlString)-1] u, err := url.Parse(publicUrlString + RouteSamlAcsWithSlash) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } samlMiddleWare.ServiceProvider.AcsURL = *u } @@ -295,7 +295,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // Crewjam library use default route for ACS and metadata but we want to overwrite them metadata, err := url.Parse(publicUrlString + RouteMetadata) if err != nil { - return err + return herodot.ErrNotFound.WithTrace(err) } samlMiddleWare.ServiceProvider.MetadataURL = *metadata @@ -337,22 +337,25 @@ func CreateSAMLProviderConfig(config config.Config, ctx context.Context, pid str if err := jsonx. NewStrictDecoder(bytes.NewBuffer(conf)). Decode(&c); err != nil { - return nil, errors.Wrapf(err, "Unable to decode config %v", string(conf)) + return nil, ErrInvalidSAMLConfiguration.WithReasonf("Unable to decode config %v", string(conf)).WithTrace(err) } if len(c.SAMLProviders) == 0 { - return nil, errors.Errorf("Please indicate a SAML Identity Provider in your configuration file") + return nil, ErrInvalidSAMLConfiguration.WithReason("Please indicate a SAML Identity Provider in your configuration file") } providerConfig, err := c.ProviderConfig(pid) if err != nil { - return nil, err + return nil, ErrInvalidSAMLConfiguration.WithTrace(err) } if providerConfig.IDPInformation == nil { - return nil, errors.Errorf("Please include your Identity Provider information in the configuration file.") + return nil, ErrInvalidSAMLConfiguration.WithReasonf("Please include your Identity Provider information in the configuration file.").WithTrace(err) } + /** + * SAMLTODO errors + */ // _, sso_exists := providerConfig.IDPInformation["idp_sso_url"] _, sso_exists := providerConfig.IDPInformation["idp_sso_url"] _, entity_id_exists := providerConfig.IDPInformation["idp_entity_id"] @@ -361,35 +364,35 @@ func CreateSAMLProviderConfig(config config.Config, ctx context.Context, pid str _, metadata_exists := providerConfig.IDPInformation["idp_metadata_url"] if (!metadata_exists && (!sso_exists || !entity_id_exists || !certificate_exists || !logout_url_exists)) || len(providerConfig.IDPInformation) > 4 { - return nil, errors.Errorf("Please check your IDP information in the configuration file") + return nil, ErrInvalidSAMLConfiguration.WithReason("Please check your IDP information in the configuration file").WithTrace(err) } if providerConfig.ID == "" { - return nil, errors.Errorf("Provider must have an ID") + return nil, ErrInvalidSAMLConfiguration.WithReason("Provider must have an ID").WithTrace(err) } if providerConfig.Label == "" { - return nil, errors.Errorf("Provider must have a label") + return nil, ErrInvalidSAMLConfiguration.WithReason("Provider must have a label").WithTrace(err) } if providerConfig.PrivateKeyPath == "" { - return nil, errors.Errorf("Provider must have a private key") + return nil, ErrInvalidSAMLConfiguration.WithReason("Provider must have a private key").WithTrace(err) } if providerConfig.PublicCertPath == "" { - return nil, errors.Errorf("Provider must have a public certificate") + return nil, ErrInvalidSAMLConfiguration.WithReason("Provider must have a public certificate").WithTrace(err) } if providerConfig.AttributesMap == nil || len(providerConfig.AttributesMap) == 0 { - return nil, errors.Errorf("Provider must have an attributes map") + return nil, ErrInvalidSAMLConfiguration.WithReason("Provider must have an attributes map").WithTrace(err) } if providerConfig.AttributesMap["id"] == "" { - return nil, errors.Errorf("You must have an ID field in your attribute_map") + return nil, ErrInvalidSAMLConfiguration.WithReason("You must have an ID field in your attribute_map").WithTrace(err) } if providerConfig.Mapper == "" { - return nil, errors.Errorf("Provider must have a mapper url") + return nil, ErrInvalidSAMLConfiguration.WithReason("Provider must have a mapper url").WithTrace(err) } return providerConfig, nil diff --git a/selfservice/strategy/saml/strategy.go b/selfservice/strategy/saml/strategy.go index cb11ceb747b5..93ecf7533a34 100644 --- a/selfservice/strategy/saml/strategy.go +++ b/selfservice/strategy/saml/strategy.go @@ -357,7 +357,7 @@ func (s *Strategy) Config(ctx context.Context) (*ConfigurationCollection, error) func (s *Strategy) populateMethod(r *http.Request, c *container.Container, message func(provider string) *text.Message) error { conf, err := s.Config(r.Context()) if err != nil { - return err + return ErrInvalidSAMLConfiguration.WithTrace(err) } // does not need sorting because there is only one field @@ -370,7 +370,7 @@ func (s *Strategy) populateMethod(r *http.Request, c *container.Container, messa func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Flow, provider string, traits []byte, err error) error { switch rf := f.(type) { case *login.Flow: - return err + return ErrAPIFlowNotSupported.WithTrace(err) case *registration.Flow: // Reset all nodes to not confuse users. // This is kinda hacky and will probably need to be updated at some point. @@ -384,24 +384,24 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl if traits != nil { ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) if err != nil { - return err + return ErrInvalidSAMLConfiguration.WithTrace(err) } traitNodes, err := container.NodesFromJSONSchema(r.Context(), node.SAMLGroup, ds.String(), "", nil) if err != nil { - return err + return herodot.ErrInternalServerError.WithTrace(err) } rf.UI.Nodes = append(rf.UI.Nodes, traitNodes...) rf.UI.UpdateNodeValuesFromJSON(traits, "traits", node.SAMLGroup) } - return err + return herodot.ErrInternalServerError.WithTrace(err) case *settings.Flow: - return err + return ErrAPIFlowNotSupported.WithTrace(err) } - return err + return herodot.ErrInternalServerError.WithTrace(err) } func (s *Strategy) CountActiveCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { From 1ecf470db7b38627fbf6ae176e67a4562c20083c Mon Sep 17 00:00:00 2001 From: sebferrer Date: Wed, 14 Dec 2022 16:01:39 +0000 Subject: [PATCH 4/8] feat(saml): relaystate continuity fix + unit tests Signed-off-by: sebferrer Co-authored-by: ThibaultHerard --- continuity/manager_relaystate_test.go | 241 ++++++++++++++++++++++++++ x/relaystate.go | 8 +- 2 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 continuity/manager_relaystate_test.go diff --git a/continuity/manager_relaystate_test.go b/continuity/manager_relaystate_test.go new file mode 100644 index 000000000000..c37b4d5b8d45 --- /dev/null +++ b/continuity/manager_relaystate_test.go @@ -0,0 +1,241 @@ +// Copyright © 2022 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package continuity_test + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/ory/kratos/driver/config" + + "github.com/ory/kratos/internal/testhelpers" + + "github.com/ory/x/ioutilx" + + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/x/logrusx" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/x" +) + +func TestManagerRelayState(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + + testhelpers.SetDefaultIdentitySchema(conf, "file://../test/stub/identity/empty.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh") + i := identity.NewIdentity("") + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + + var newServer = func(t *testing.T, p continuity.Manager, tc *persisterTestCase) *httptest.Server { + writer := herodot.NewJSONWriter(logrusx.New("", "")) + router := httprouter.New() + router.PUT("/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if err := p.Pause(r.Context(), w, r, ps.ByName("name"), tc.ro...); err != nil { + writer.WriteError(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) + }) + + router.POST("/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + relayState := r.URL.Query().Get("RelayState") + + r.PostForm = make(url.Values) + r.PostForm.Set("RelayState", relayState) + + c, err := p.Continue(r.Context(), w, r, ps.ByName("name"), tc.wo...) + if err != nil { + writer.WriteError(w, r, err) + return + } + writer.Write(w, r, c) + }) + + router.DELETE("/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + relayState := r.URL.Query().Get("RelayState") + + r.PostForm = make(url.Values) + r.PostForm.Set("RelayState", relayState) + + err := p.Abort(r.Context(), w, r, ps.ByName("name")) + if err != nil { + writer.WriteError(w, r, err) + return + } + w.WriteHeader(http.StatusNoContent) + }) + + ts := httptest.NewServer(router) + t.Cleanup(func() { + ts.Close() + }) + return ts + } + + var newClient = func() *http.Client { + return &http.Client{Jar: x.EasyCookieJar(t, nil)} + } + + p := reg.RelayStateContinuityManager() + cl := newClient() + + t.Run("case=continue cookie persists with same http client", func(t *testing.T) { + ts := newServer(t, p, new(persisterTestCase)) + name := x.NewUUID().String() + href := ts.URL + "/" + name + + res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + req := x.NewTestHTTPRequest(t, "POST", href, nil) + require.Len(t, res.Cookies(), 1) + + res, err = cl.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + body := ioutilx.MustReadAll(res.Body) + assert.Contains(t, gjson.GetBytes(body, "name").String(), name) + + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Len(t, res.Cookies(), 1) + assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName) + }) + + t.Run("case=continue cookie reconstructed and delivered with valid relaystate", func(t *testing.T) { + ts := newServer(t, p, new(persisterTestCase)) + name := x.NewUUID().String() + href := ts.URL + "/" + name + + res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + var relayState string + + for _, c := range res.Cookies() { + relayState = c.Value + } + + req := x.NewTestHTTPRequest(t, "POST", href+"?RelayState="+url.QueryEscape(relayState), nil) + require.Len(t, res.Cookies(), 1) + + res, err = http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + body := ioutilx.MustReadAll(res.Body) + assert.Contains(t, gjson.GetBytes(body, "name").String(), name) + + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Len(t, res.Cookies(), 1) + assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName) + }) + + t.Run("case=continue cookie not delivered with invalid relaystate", func(t *testing.T) { + ts := newServer(t, p, new(persisterTestCase)) + name := x.NewUUID().String() + href := ts.URL + "/" + name + + res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + var relayState string + + for _, c := range res.Cookies() { + relayState = c.Value + relayState = strings.Replace(relayState, "a", "b", 1) + } + require.Len(t, res.Cookies(), 1) + + req := x.NewTestHTTPRequest(t, "POST", href+"?RelayState="+url.QueryEscape(relayState), nil) + + res, err = http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body := ioutilx.MustReadAll(res.Body) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable.ReasonField) + + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Len(t, res.Cookies(), 0, "the cookie couldn't be reconstructed without a valid relaystate") + }) + + t.Run("case=continue cookie not delivered without relaystate", func(t *testing.T) { + ts := newServer(t, p, new(persisterTestCase)) + name := x.NewUUID().String() + href := ts.URL + "/" + name + + res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + require.Len(t, res.Cookies(), 1) + + req := x.NewTestHTTPRequest(t, "POST", href, nil) + + res, err = http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body := ioutilx.MustReadAll(res.Body) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable.ReasonField) + + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Len(t, res.Cookies(), 0, "the cookie couldn't be reconstructed without a valid relaystate") + }) + + t.Run("case=pause, abort, and continue session with failure", func(t *testing.T) { + ts := newServer(t, p, new(persisterTestCase)) + name := x.NewUUID().String() + href := ts.URL + "/" + name + + res, err := cl.Do(x.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + req := x.NewTestHTTPRequest(t, "DELETE", href, nil) + + res, err = cl.Do(req) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + req = x.NewTestHTTPRequest(t, "POST", href, nil) + + res, err = cl.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + + body := ioutilx.MustReadAll(res.Body) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable.ReasonField) + + t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + + require.Len(t, res.Cookies(), 0, "the cookie couldn't be reconstructed without a valid relaystate") + }) +} diff --git a/x/relaystate.go b/x/relaystate.go index a28c97024d58..04e3760741a4 100644 --- a/x/relaystate.go +++ b/x/relaystate.go @@ -11,9 +11,6 @@ import ( func SessionGetStringRelayState(r *http.Request, s sessions.StoreExact, id string, key interface{}) (string, error) { cipherRelayState := r.PostForm.Get("RelayState") - if cipherRelayState == "" { - return "", errors.New("The RelayState is empty or not exists") - } // Reconstructs the cookie from the ciphered value continuityCookie := &http.Cookie{ @@ -22,8 +19,7 @@ func SessionGetStringRelayState(r *http.Request, s sessions.StoreExact, id strin MaxAge: 300, } - r2 := r.Clone(r.Context()) - r2.AddCookie(continuityCookie) + r.AddCookie(continuityCookie) check := func(v map[interface{}]interface{}) (string, error) { vv, ok := v[key] @@ -37,7 +33,7 @@ func SessionGetStringRelayState(r *http.Request, s sessions.StoreExact, id strin } var exactErr error - sessionCookie, err := s.GetExact(r2, id, func(s *sessions.Session) bool { + sessionCookie, err := s.GetExact(r, id, func(s *sessions.Session) bool { _, exactErr = check(s.Values) return exactErr == nil }) From 7f08a08c59bf47eefd0b0e97532f30c814934190 Mon Sep 17 00:00:00 2001 From: ThibaultHerard Date: Fri, 16 Dec 2022 16:22:16 +0000 Subject: [PATCH 5/8] feat(saml): vulnerabilities check + update saml tests Signed-off-by: ThibaultHerard Co-authored-by: sebferrer --- go.mod | 13 +- go.sum | 16 +- selfservice/strategy/saml/config_test.go | 6 +- selfservice/strategy/saml/handler.go | 21 +- selfservice/strategy/saml/handler_test.go | 22 +- selfservice/strategy/saml/metadata_test.go | 3 - selfservice/strategy/saml/provider_saml.go | 2 +- .../saml/saml_vulnerabilities_check.md | 52 + selfservice/strategy/saml/strategy.go | 11 +- .../strategy/saml/strategy_helper_test.go | 16 +- .../TestSPCanHandleOneloginResponse_response | 1 - .../strategy/saml/testdata/authn_request.url | 1 + .../strategy/saml/testdata/evilcert.crt | 32 + .../strategy/saml/testdata/evilkey.key | 52 + .../saml/testdata/expected_metadata.xml | 7 +- .../saml/testdata/{cert.pem => idp_cert.pem} | 2 +- .../saml/testdata/{key.pem => idp_key.pem} | 2 +- .../strategy/saml/testdata/idp_metadata.xml | 31 + .../strategy/saml/testdata/myservice.cert | 19 - .../strategy/saml/testdata/myservice.key | 28 - .../strategy/saml/testdata/saml.jsonnet | 1 + .../strategy/saml/testdata/saml_response.xml | 2 +- .../strategy/saml/testdata/samlkratos.crt | 21 - .../strategy/saml/testdata/sp2_cert.pem | 32 + .../strategy/saml/testdata/sp2_key.pem | 52 + .../strategy/saml/testdata/sp_cert.pem | 13 + selfservice/strategy/saml/testdata/sp_key.pem | 15 + selfservice/strategy/saml/testdata/token.json | 46 + .../saml/vulnerabilities_helper_test.go | 330 ++++ .../strategy/saml/vulnerabilities_test.go | 1336 +++++++++++++++++ 30 files changed, 2053 insertions(+), 132 deletions(-) create mode 100644 selfservice/strategy/saml/saml_vulnerabilities_check.md delete mode 100644 selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response create mode 100644 selfservice/strategy/saml/testdata/authn_request.url create mode 100644 selfservice/strategy/saml/testdata/evilcert.crt create mode 100644 selfservice/strategy/saml/testdata/evilkey.key rename selfservice/strategy/saml/testdata/{cert.pem => idp_cert.pem} (96%) rename selfservice/strategy/saml/testdata/{key.pem => idp_key.pem} (96%) create mode 100644 selfservice/strategy/saml/testdata/idp_metadata.xml delete mode 100755 selfservice/strategy/saml/testdata/myservice.cert delete mode 100755 selfservice/strategy/saml/testdata/myservice.key delete mode 100755 selfservice/strategy/saml/testdata/samlkratos.crt create mode 100644 selfservice/strategy/saml/testdata/sp2_cert.pem create mode 100644 selfservice/strategy/saml/testdata/sp2_key.pem create mode 100644 selfservice/strategy/saml/testdata/sp_cert.pem create mode 100644 selfservice/strategy/saml/testdata/sp_key.pem create mode 100644 selfservice/strategy/saml/testdata/token.json create mode 100644 selfservice/strategy/saml/vulnerabilities_helper_test.go create mode 100644 selfservice/strategy/saml/vulnerabilities_test.go diff --git a/go.mod b/go.mod index e000f539ceb6..fce674c9b2ce 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/coreos/go-oidc v2.2.1+incompatible github.com/cortesi/modd v0.0.0-20210323234521-b35eddab86cc - github.com/crewjam/saml v0.4.6 + github.com/crewjam/saml v0.4.10 github.com/davecgh/go-spew v1.1.1 github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 github.com/dgraph-io/ristretto v0.1.1 @@ -44,9 +44,9 @@ require ( github.com/go-swagger/go-swagger v0.30.3 github.com/gobuffalo/fizz v1.14.4 github.com/gobuffalo/httptest v1.5.2 - github.com/gobuffalo/pop/v6 v6.1.2-0.20230124165254-ec9229dbf7d7 - github.com/gofrs/uuid v4.3.1+incompatible - github.com/golang-jwt/jwt/v4 v4.1.0 + github.com/gobuffalo/pop/v6 v6.0.8 + github.com/gofrs/uuid v4.3.0+incompatible + github.com/golang-jwt/jwt/v4 v4.4.2 github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 github.com/golang/mock v1.6.0 github.com/google/go-github/v27 v27.0.1 @@ -59,6 +59,7 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/imdario/mergo v0.3.13 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf + github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65 github.com/jarcoal/httpmock v1.0.5 github.com/jteeuwen/go-bindata v3.0.7+incompatible github.com/julienschmidt/httprouter v1.3.0 @@ -102,6 +103,7 @@ require ( golang.org/x/oauth2 v0.4.0 golang.org/x/sync v0.1.0 golang.org/x/tools v0.2.0 + google.golang.org/grpc v1.50.1 gotest.tools v2.2.0+incompatible ) @@ -326,8 +328,7 @@ require ( golang.org/x/time v0.1.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect - google.golang.org/grpc v1.52.0 // indirect + google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index aa34dd7e5d8c..427d9b4cccdb 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= -github.com/crewjam/saml v0.4.6 h1:XCUFPkQSJLvzyl4cW9OvpWUbRf0gE7VUpU8ZnilbeM4= -github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A= +github.com/crewjam/saml v0.4.10 h1:Rjs6x4s/aQFXiaPjw3uhB4VdxRqoxHXOJrrj4BsMn9o= +github.com/crewjam/saml v0.4.10/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= @@ -291,7 +291,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 h1:VzPvKOw28XJ77PYwOq5gAqvFB4gk6gst0HxxiW8kfZQ= github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6/go.mod h1:+6FzxsSbK4oEuvdN06Jco8zKB2mQqIB6UduZdd0Zesk= -github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= @@ -532,8 +531,9 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 h1:xisWqjiKEff2B0KfFYGpCqc3M3zdTz+OHQHRc09FeYk= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -785,6 +785,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65 h1:T25FL3WEzgmKB0m6XCJNZ65nw09/QIp3T1yXr487D+A= +github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65/go.mod h1:nYhEREG/B7HUY7P+LKOrqy53TpIqmJ9JyUShcaEKtGw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -1117,8 +1119,8 @@ github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OU github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= -github.com/ory/x v0.0.537 h1:FB8Tioza6pihvy/RsVNzX08Qg3/VpIhI9vBnEQ4iFmQ= -github.com/ory/x v0.0.537/go.mod h1:CQopDsCC9t0tQsddE9UlyRFVEFd2xjKBVcw4nLMMMS0= +github.com/ory/x v0.0.519 h1:T8/LbbQQqm+3P7bfI838T7eECv6+laXlvIyCp0QB+R8= +github.com/ory/x v0.0.519/go.mod h1:xUtRpoiRARyJNPVk/fcCNKzyp25Foxt9GPlj8pd7egY= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -1435,7 +1437,6 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= @@ -1578,6 +1579,7 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/selfservice/strategy/saml/config_test.go b/selfservice/strategy/saml/config_test.go index a6d90c8e5d19..65b394949506 100644 --- a/selfservice/strategy/saml/config_test.go +++ b/selfservice/strategy/saml/config_test.go @@ -225,7 +225,7 @@ func TestAttributesMapWithAnExtraField(t *testing.T) { idpInformation := make(map[string]string) idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" - idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" + idpInformation["idp_certificate_path"] = "file://testdata/idp_cert.pem" idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" // Initiates the service provider @@ -235,8 +235,8 @@ func TestAttributesMapWithAnExtraField(t *testing.T) { saml.Configuration{ ID: "samlProvider", Label: "samlProviderLabel", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", AttributesMap: attributesMap, IDPInformation: idpInformation, diff --git a/selfservice/strategy/saml/handler.go b/selfservice/strategy/saml/handler.go index 6ffac7ae70fc..eacb5e742c3f 100644 --- a/selfservice/strategy/saml/handler.go +++ b/selfservice/strategy/saml/handler.go @@ -93,18 +93,7 @@ func (h *Handler) serveMetadata(w http.ResponseWriter, r *http.Request, ps httpr w.Write(buf) } -// swagger:route GET /self-service/methods/saml/auth v0alpha2 initializeSelfServiceSamlFlowForBrowsers -// -// Initialize Authentication Flow for SAML (Either the login or the register) -// -// If you already have a session, it will redirect you to the main page. -// -// Schemes: http, https -// -// Responses: -// 200: selfServiceRegistrationFlow -// 400: jsonError -// 500: jsonError +// TODO Swagger func (h *Handler) loginWithIdp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Middleware is a singleton so we have to verify that it exists config := h.d.Config() @@ -153,9 +142,9 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi } // Key pair to encrypt and sign SAML requests - keyPair, err := tls.LoadX509KeyPair(strings.Replace(providerConfig.PublicCertPath, "file://", "", 1), strings.Replace(providerConfig.PrivateKeyPath, "file://", "", 1)) + keyPair, err := tls.LoadX509KeyPair(strings.Replace(providerConfig.PublicCertPath, "file://", "", 1), strings.Replace(providerConfig.PrivateKeyPath, "file://", "", 1)) // TODO : Fetcher if err != nil { - return herodot.ErrNotFound.WithTrace(err) + return herodot.ErrNotFound.WithTrace(err) // TODO : Replace with File not found error } keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { @@ -313,7 +302,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // Return the singleton MiddleWare func GetMiddleware(pid string) (*samlsp.Middleware, error) { if samlMiddlewares[pid] == nil { - return nil, errors.Errorf("An error occurred while retrieving the middeware, it is null") + return nil, errors.Errorf("An error occurred while retrieving the middeware, it is null") // TODO : Improve error message } return samlMiddlewares[pid], nil } @@ -321,7 +310,7 @@ func GetMiddleware(pid string) (*samlsp.Middleware, error) { func MustParseCertificate(pemStr []byte) (*x509.Certificate, error) { b, _ := pem.Decode(pemStr) if b == nil { - return nil, errors.Errorf("Cannot find the next PEM formatted block") + return nil, errors.Errorf("Cannot find the next PEM formatted block while parsing the certificate") } cert, err := x509.ParseCertificate(b.Bytes) if err != nil { diff --git a/selfservice/strategy/saml/handler_test.go b/selfservice/strategy/saml/handler_test.go index c0d15540da57..cd8b40626438 100644 --- a/selfservice/strategy/saml/handler_test.go +++ b/selfservice/strategy/saml/handler_test.go @@ -38,7 +38,7 @@ func TestInitMiddleWareWithoutMetadata(t *testing.T) { middleWare, _, _, err := InitTestMiddlewareWithoutMetadata(t, "https://samltest.id/idp/profile/SAML2/Redirect/SSO", "https://samltest.id/saml/idp", - "file://testdata/samlkratos.crt", + "file://testdata/idp_cert.pem", "https://samltest.id/idp/profile/SAML2/Redirect/SSO") require.NoError(t, err) @@ -74,7 +74,7 @@ func TestMustParseCertificate(t *testing.T) { saml.DestroyMiddlewareIfExists("samlProvider") - certificateBuffer, err := fetcher.NewFetcher().Fetch("file://testdata/samlkratos.crt") + certificateBuffer, err := fetcher.NewFetcher().Fetch("file://testdata/sp_cert.pem") require.NoError(t, err) certificate, err := ioutil.ReadAll(certificateBuffer) @@ -83,13 +83,13 @@ func TestMustParseCertificate(t *testing.T) { cert, err := saml.MustParseCertificate(certificate) require.NoError(t, err) - assert.Check(t, cert.Issuer.Country[0] == "AU") - assert.Check(t, cert.Issuer.Organization[0] == "Internet Widgits Pty Ltd") - assert.Check(t, cert.Issuer.Province[0] == "Some-State") - assert.Check(t, cert.Subject.Country[0] == "AU") - assert.Check(t, cert.Subject.Organization[0] == "Internet Widgits Pty Ltd") - assert.Check(t, cert.Subject.Province[0] == "Some-State") - assert.Check(t, cert.NotBefore.String() == "2022-02-21 11:08:20 +0000 UTC") - assert.Check(t, cert.NotAfter.String() == "2023-02-21 11:08:20 +0000 UTC") - assert.Check(t, cert.SerialNumber.String() == "485646075402096403898806020771481121115125312047") + assert.Check(t, cert.Issuer.Country[0] == "US") + assert.Check(t, cert.Issuer.Organization[0] == "foo") + assert.Check(t, cert.Issuer.Province[0] == "GA") + assert.Check(t, cert.Subject.Country[0] == "US") + assert.Check(t, cert.Subject.Organization[0] == "foo") + assert.Check(t, cert.Subject.Province[0] == "GA") + assert.Check(t, cert.NotBefore.String() == "2013-10-02 00:08:51 +0000 UTC") + assert.Check(t, cert.NotAfter.String() == "2014-10-02 00:08:51 +0000 UTC") + assert.Check(t, cert.SerialNumber.String() == "14253244695696570161") } diff --git a/selfservice/strategy/saml/metadata_test.go b/selfservice/strategy/saml/metadata_test.go index 8a5bce69c115..a422e3a93433 100644 --- a/selfservice/strategy/saml/metadata_test.go +++ b/selfservice/strategy/saml/metadata_test.go @@ -2,7 +2,6 @@ package saml_test import ( "encoding/xml" - "fmt" "io" "io/ioutil" "net/http" @@ -75,8 +74,6 @@ func TestXmlMetadataExist(t *testing.T) { assert.NilError(t, err) res, err := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") assert.NilError(t, err) - body, _ := ioutil.ReadAll(res.Body) - fmt.Println(body) assert.Check(t, is.Equal(http.StatusOK, res.StatusCode)) assert.Check(t, is.Equal("text/xml", res.Header.Get("Content-Type"))) } diff --git a/selfservice/strategy/saml/provider_saml.go b/selfservice/strategy/saml/provider_saml.go index 23047bf7d30c..e0917dfd44ea 100644 --- a/selfservice/strategy/saml/provider_saml.go +++ b/selfservice/strategy/saml/provider_saml.go @@ -26,7 +26,7 @@ func NewProviderSAML( } } -// Translate attributes from saml asseryion into kratos claims +// Translate attributes from saml assertion into kratos claims func (d *ProviderSAML) Claims(ctx context.Context, config *config.Config, attributeSAML samlsp.Attributes, pid string) (*Claims, error) { var c ConfigurationCollection diff --git a/selfservice/strategy/saml/saml_vulnerabilities_check.md b/selfservice/strategy/saml/saml_vulnerabilities_check.md new file mode 100644 index 000000000000..f9424d007b33 --- /dev/null +++ b/selfservice/strategy/saml/saml_vulnerabilities_check.md @@ -0,0 +1,52 @@ +# **SAML Crewjam over Kratos vulnerabilities checks** + +|Status|Summary|Description|Comment| +| :- | :- | :- | :- | +|**OK**|**Check that it's not possible to modify the signed SAML Response**||| +|OK|- By adding an attribute||| +|OK|- By adding an element||| +|OK|- By modifying the indent||| +|**OK**|**Check that it's not possible to modify the signed SAML Assertion**|**If the SAML Response isn't signed**|| +|OK|- By adding an attribute||| +|OK|- By adding an element||| +|**OK**|**Check that it's not possible to remove the signature**||| +|OK|- Not possible to remove SAML Response signature value|A signature must contain a signature value|| +|OK|- Possible to remove SAML Response signature if the SAML Assertion is signed|Either the SAML Response or the SAML Assertion must be signed|| +|OK|- Not possible to remove SAML Assertion signature value|A signature must contain a signature value|| +|OK|- Not possible to remove SAML Assertion signature|If the SAML Response is still signed, any SAML Assertion modification is an unauthorized SAML Response modification|| +|OK|- Not possible to remove both SAML Response signature and SAML Assertion signature|Either the SAML Response or the SAML Assertion must be signed|| +|**OK**|**Prevent from Signature Wrapping Attacks (XSW)**||| +|OK|- XSW1 response wrap 1|XSW #1 manipulates SAML Responses. It does this by making a copy of the SAML Response and Assertion, then inserting the original Signature into the XML as a child element of the copied Response. The assumption being that the XML parser finds and uses the copied Response at the top of the document after signature validation instead of the original signed Response.|| +|OK|- XSW2 response wrap 2|Similar to XSW #1, XSW #2 manipulates SAML Responses. XSW #1 and XSW #2 are the only two that deal with Responses. The key difference between #1 and #2 is that the type of Signature used is a detached signature where XSW #1 used an enveloping signature. The location of the malicious Response remains the same.|| +|OK|- XSW3 assertion wrap 1|XSW #3 is the first example of an XSW that wraps the Assertion element. SAML Raider inserts the copied Assertion as the first child of the root Response element. The original Assertion is a sibling of the copied Assertion.|| +|OK|- XSW4 assertion wrap 2|XSW #4 is similar to #3, except in this case the original Assertion becomes a child of the copied Assertion.|| +|OK|- XSW5 assertion wrap 3|XSW #5 is the first instance of Assertion wrapping we see where the Signature and the original Assertion aren’t in one of the three standard configurations (enveloped/enveloping/detached). In this case, the copied Assertion envelopes the Signature.|| +|OK|- XSW6 assertion wrap 4|XSW #6 inserts its copied Assertion into the same location as #’s 4 and 5. The interesting piece here is that the copied Assertion envelopes the Signature, which in turn envelopes the original Assertion.|| +|OK|- XSW7 assertion wrap 5|XSW #7 inserts an Extensions element and adds the copied Assertion as a child. Extensions is a valid XML element with a less restrictive schema definition. OpenSAML used schema validation to correctly compare the ID used during signature validation to the ID of the processed Assertion. The authors found in cases where copied Assertions with the same ID of the original Assertion were children of an element with a less restrictive schema definition, they were able to bypass this particular countermeasure.|| +|OK|- XSW8 assertion wrap 6|XSW #8 uses another less restrictive XML element to perform a variation of the attack pattern used in XSW #7. This time around the original Assertion is the child of the less restrictive element instead of the copied Assertion.|| +|**OK**|**Analyse the application behaviour when adding XML comments**|**In the beginning, middle, and end of an attribute (such as username)**|| +|OK|- The XML comments aren't removed||| +|OK|- The XML comments don't allow the user to authenticate with another identity||| +|**OK**|**Prevent from signing the SAML Response with own certificate**|**Depending on the case: assertion, message, or both**|| +|OK|- Prevent from signing the SAML assertion with own certificate||| +|OK|- Prevent from signing the SAML response with own certificate||| +|OK|- Prevent from signing both the response and the assertion||| +|**OK**|**Prevent from XXE and XSLT attacks**||| +|**OK**|**Check if there are any known vulnerabilities for the SAML library or software in use**||| +|**OK**|**Check if it is possible to send the same SAML Response twice (Replay Attack)**||| +|**OK**|**Check if the SP uses the same attribute as IdP to identify the user**||**There is a mapping**| +|**N/A**|**Check if IdP allows anonymous registration**||| +|**N/A**|**Verify Single Log Out (if required)**||| +|**OK**|**Check if the validity time window is short**|**3-5 minutes**|**90sec in our implementation**| +|**OK**|**Check if the time window is validated**|**Try to use the same SAML Response after it has expired**|| +|**N/A**|**Check for Cross-Site Request Forgery attack**|**Unsolicited Response**|| +|**OK**|**Check if the recipient is validated**|**Token Recipient Confusion**|| +|**N/A**|**Check for Open Redirect in RelayState**||| +|**OK**|**Check the signature algorithm in use**||**SHA256**| +|**OK**|**Check that the SAML response is associated with an AuthnRequest already performed on the IdP**|**Check that the ID field of the SAML Request corresponds to the InResponseTo field of the SAML Response**|| + +## **Sources** +- [SAML – what can go wrong? Security check](https://www.securing.pl/en/saml-what-can-go-wrong-security-check/) +- [How to Hunt Bugs in SAML; a Methodology - Part II](https://epi052.gitlab.io/notes-to-self/blog/2019-03-13-how-to-test-saml-a-methodology-part-two/) +- [On Breaking SAML: Be Whoever You Want to Be](https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final91.pdf) +- [SAMLRaider](https://github.com/CompassSecurity/SAMLRaider) diff --git a/selfservice/strategy/saml/strategy.go b/selfservice/strategy/saml/strategy.go index 93ecf7533a34..fec366df3359 100644 --- a/selfservice/strategy/saml/strategy.go +++ b/selfservice/strategy/saml/strategy.go @@ -135,7 +135,7 @@ func NewStrategy(d registrationStrategyDependencies) *Strategy { // We indicate here that when the ACS endpoint receives a POST request, we call the handleCallback method to process it func (s *Strategy) setRoutes(r *x.RouterPublic) { - wrappedHandleCallback := strategy.IsDisabled(s.d, s.ID().String(), s.handleCallback) + wrappedHandleCallback := strategy.IsDisabled(s.d, s.ID().String(), s.HandleCallback) if handle, _, _ := r.Lookup("POST", RouteAcs); handle == nil { r.POST(RouteAcs, wrappedHandleCallback) } // ACS SUPPORT @@ -256,18 +256,20 @@ func (s *Strategy) validateCallback(w http.ResponseWriter, r *http.Request) (flo } // Handle /selfservice/methods/saml/acs/:provider | Receive SAML response, parse the attributes and start auth flow -func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // We get the provider ID form the URL pid := ps.ByName("provider") if err := r.ParseForm(); err != nil { s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, s.handleError(w, r, nil, pid, nil, err)) + return } req, _, err := s.validateCallback(w, r) if err != nil { if req != nil { s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) + return } else { s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, s.handleError(w, r, nil, pid, nil, err)) } @@ -277,13 +279,17 @@ func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps htt m, err := GetMiddleware(pid) if err != nil { s.forwardError(w, r, err) + return } // We get the possible SAML request IDs possibleRequestIDs := GetPossibleRequestIDs(r, *m) + + // We parse the SAML Response to get the SAML Assertion assertion, err := m.ServiceProvider.ParseResponse(r, possibleRequestIDs) if err != nil { s.forwardError(w, r, err) + return } // We get the user's attributes from the SAML Response (assertion) @@ -313,6 +319,7 @@ func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps htt if ff, err := s.processLoginOrRegister(w, r, a, provider, claims); err != nil { if ff != nil { s.forwardError(w, r, err) + return } s.forwardError(w, r, err) } diff --git a/selfservice/strategy/saml/strategy_helper_test.go b/selfservice/strategy/saml/strategy_helper_test.go index 3fb92f5ee3f0..bb55a8f8fccb 100644 --- a/selfservice/strategy/saml/strategy_helper_test.go +++ b/selfservice/strategy/saml/strategy_helper_test.go @@ -23,7 +23,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "gotest.tools/golden" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" @@ -103,10 +102,11 @@ func InitTestMiddleware(t *testing.T, idpInformation map[string]string) (*samlsp ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, routerP, routerA) attributesMap := make(map[string]string) - attributesMap["id"] = "mail" + attributesMap["id"] = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" attributesMap["firstname"] = "givenName" attributesMap["lastname"] = "sn" - attributesMap["email"] = "mail" + attributesMap["email"] = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" + attributesMap["groups"] = "urn:oid:1.3.6.1.4.1.5923.1.1.1.1" // Initiates the service provider ViperSetProviderConfig( @@ -116,8 +116,8 @@ func InitTestMiddleware(t *testing.T, idpInformation map[string]string) (*samlsp ID: "samlProvider", Label: "samlProviderLabel", Provider: "generic", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", AttributesMap: attributesMap, IDPInformation: idpInformation, @@ -129,15 +129,15 @@ func InitTestMiddleware(t *testing.T, idpInformation map[string]string) (*samlsp conf.MustSet(context.Background(), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeSAML.String()), []config.SelfServiceHook{{Name: "session"}}) - t.Logf("Kratos Public URL: %s", ts.URL) + // t.Logf("Kratos Public URL: %s", ts.URL) // Instantiates the MiddleWare _, err := NewTestClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata/samlProvider") require.NoError(t, err) middleware, err := saml.GetMiddleware("samlProvider") require.NoError(t, err) - middleware.ServiceProvider.Key = mustParsePrivateKey(golden.Get(t, "key.pem")).(*rsa.PrivateKey) - middleware.ServiceProvider.Certificate = mustParseCertificate(golden.Get(t, "cert.pem")) + //middleware.ServiceProvider.Key = mustParsePrivateKey(golden.Get(t, "sp_key.pem")).(*rsa.PrivateKey) + //middleware.ServiceProvider.Certificate = mustParseCertificate(golden.Get(t, "sp_cert.pem")) return middleware, strategy, ts, err } diff --git a/selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response b/selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response deleted file mode 100644 index 1031d30218df..000000000000 --- a/selfservice/strategy/saml/testdata/TestSPCanHandleOneloginResponse_response +++ /dev/null @@ -1 +0,0 @@ -PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJwZnhlZDg4YzQzZC02NTA0LWUxZjEtNWFmMC00MGJlN2YyNzlmYzUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE2LTAxLTA1VDE3OjUzOjExWiIgRGVzdGluYXRpb249Imh0dHBzOi8vMjllZTZkMmUubmdyb2suaW8vc2FtbC9hY3MiIEluUmVzcG9uc2VUbz0iaWQtZDQwYzE1YzEwNGI1MjY5MWVjY2YwYTJhNWM4YTE1NTk1YmU3NTQyMyI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzUwMzk4Mzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+PGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhlZDg4YzQzZC02NTA0LWUxZjEtNWFmMC00MGJlN2YyNzlmYzUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPlNWQWFRZzh2bW1TUUw2L1lCbVMyeWRLUlA3ST08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+c0JlVFZQMGJab1BSK2JmeUFrVnY2STNDVjdZOFhxbkoycjhmMStXbXIyZ0ZnblJGODVOdnZTUCtyMUJvN250dU9zd080ZkI0Uks0SHlTYnlsZzRiS0hLSDE5WDkxaFZBekpTeXNmbVMvZDV3ZzFDZmlXV3Q1UzJIQTUwOHRoWHVabndHM1h6NktuV0s4a1JkeDFkYytZUldnYUZ5ZDRnTEc5YUJUc1hPWjd2eC83UDRicnpORW00d1A5LzB0dWZ4Rytuc1k2RHB3bkVHQ2psK1ZVS3BnekVxd05OalFxWUZZU0FYRWsrVnQrWDNjMmQwSElyWlF2WW5OaDAyS3h1d1ZCVGhuM01helFOYU54Qy9zeWYza0RRQ1JyWkNZbytZdER1ZHpKVTlwM0EwWVhIVFFjc2RldHNIWlhDTWozbXV2emMwbUVCbHc0TGJjaEttbmJ5Wm1nPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUVDRENDQXZDZ0F3SUJBZ0lVWHVuMDhDc2xMUldTTHFObkRFMU50R0plZmwwd0RRWUpLb1pJaHZjTkFRRUZCUUF3VXpFTE1Ba0dBMVVFQmhNQ1ZWTXhEREFLQmdOVkJBb01BMk4wZFRFVk1CTUdBMVVFQ3d3TVQyNWxURzluYVc0Z1NXUlFNUjh3SFFZRFZRUUREQlpQYm1WTWIyZHBiaUJCWTJOdmRXNTBJRE15TmpFME1CNFhEVEV6TURrek1ERTVNelUwTkZvWERURTRNVEF3TVRFNU16VTBORm93VXpFTE1Ba0dBMVVFQmhNQ1ZWTXhEREFLQmdOVkJBb01BMk4wZFRFVk1CTUdBMVVFQ3d3TVQyNWxURzluYVc0Z1NXUlFNUjh3SFFZRFZRUUREQlpQYm1WTWIyZHBiaUJCWTJOdmRXNTBJRE15TmpFME1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME9HOFY4bWhvdmtqNHJoR2hqcmJFeFJZYnpLVjJaeGZ2R2ZFR1hHVXZYYzZEcWVqWUVkaFoybUlmQ0RvamhRamswQnl3aWlyQUtNT3QxR051SDdhV0lFNDdEMGV3dEs1eWxFQW03ZVZtb1k0a3hMQ2FXNXdZckMxU3pNbnBlaXRVeHF2c2JuS3ozalVLWUhSZ2dwZnZWajRzaUhEWmVJWmE5YTVyVXZwTW5uYk9vRmlaQ0lFTnBxM1RDMzNpdk9TWmhFTlJUem12bms1R0RvTEh3LzhxQWdRaXlUM0QxeENrU0JiNTRQSGdrUTVScTFvZExNL2hKK0wwanpDVVFINGd4cFdsRUFhYjRLOXM4ZnBCVUJCaDVnbUpDWWk4VWJJbGhxTzhOMm15bnVtMzNCVS92SjNQbmF3VDRZWWtUd1JVeDZZKzNmcG1SQkhxbDRoODNTTWV3SURBUUFCbzRIVE1JSFFNQXdHQTFVZEV3RUIvd1FDTUFBd0hRWURWUjBPQkJZRUZPZkZGakhGajlhNnhwbmdiMTFycmhnTWU5QXJNSUdRQmdOVkhTTUVnWWd3Z1lXQUZPZkZGakhGajlhNnhwbmdiMTFycmhnTWU5QXJvVmVrVlRCVE1Rc3dDUVlEVlFRR0V3SlZVekVNTUFvR0ExVUVDZ3dEWTNSMU1SVXdFd1lEVlFRTERBeFBibVZNYjJkcGJpQkpaRkF4SHpBZEJnTlZCQU1NRms5dVpVeHZaMmx1SUVGalkyOTFiblFnTXpJMk1UU0NGRjdwOVBBckpTMFZraTZqWnd4TlRiUmlYbjVkTUE0R0ExVWREd0VCL3dRRUF3SUhnREFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBTWdsbjROUE1RbjhHeXZxOENUUCtjMmU2Q1V6Y3ZSRUtuVGhqeFQ5V2N2VjFaVlhNQk5QbTRjVHFUMzYxRWRMelk1eVdMVVdYZDRBdkZuY2lxQjNNSFlhMm5xVG1udkxnbWhrV2UraGRGb05lNStJQThBeEduK25xVUlTbXlCZUN4dVVVQWJSTXVvd2lBcndISXB6cEV5UklZZFNaUk5GMGR2Z2lQWXlyL01pUFhJY3pwSDVuTGt2YkxwY0FGK1I4Wmg5bndZMGcxSlZ5YzZBQjZqN1lleHVVUVpwSEg0czBWZHgvbldtcmNGZUxaS0NUeGNhaEh2VTUwZTF5S1g1dGhmVmFKcUk4UVE3eFp4eXUwVFRzaWFYMHV3NTFKUE96UHVBUHBoMHo2eG9TOW9ZeHV6WjF5OXNOSEg2a0g4R0ZudlMyTXF5SGlOejBoMFNxL3E2bit3PT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBWZXJzaW9uPSIyLjAiIElEPSJBZDk0NWFlZGEzOGE1MDhmOGZhYzliYzk2MTNkNTk2NDJjMGQyZDhjYiIgSXNzdWVJbnN0YW50PSIyMDE2LTAxLTA1VDE3OjUzOjExWiI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzUwMzk4Mzwvc2FtbDpJc3N1ZXI+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnJvc3NAa25kci5vcmc8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMTYtMDEtMDVUMTc6NTY6MTFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vMjllZTZkMmUubmdyb2suaW8vc2FtbC9hY3MiIEluUmVzcG9uc2VUbz0iaWQtZDQwYzE1YzEwNGI1MjY5MWVjY2YwYTJhNWM4YTE1NTk1YmU3NTQyMyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE2LTAxLTA1VDE3OjUwOjExWiIgTm90T25PckFmdGVyPSIyMDE2LTAxLTA1VDE3OjU2OjExWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovLzI5ZWU2ZDJlLm5ncm9rLmlvL3NhbWwvbWV0YWRhdGE8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE2LTAxLTA1VDE3OjUzOjEwWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNi0wMS0wNlQxNzo1MzoxMVoiIFNlc3Npb25JbmRleD0iX2ViZGNiZTgwLTk1ZmYtMDEzMy1kODcxLTM4Y2EzYTY2MmYxYyI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyIgTmFtZT0iVXNlci5lbWFpbCI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cm9zc0BrbmRyLm9yZzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIiBOYW1lPSJtZW1iZXJPZiI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyIvPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IlVzZXIuTGFzdE5hbWUiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPktpbmRlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIiBOYW1lPSJQZXJzb25JbW11dGFibGVJRCI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyIvPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiIE5hbWU9IlVzZXIuRmlyc3ROYW1lIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5Sb3NzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+Cgo= \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/authn_request.url b/selfservice/strategy/saml/testdata/authn_request.url new file mode 100644 index 000000000000..1adc1dbf5f25 --- /dev/null +++ b/selfservice/strategy/saml/testdata/authn_request.url @@ -0,0 +1 @@ +https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO?RelayState=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmkiOiIvIn0.eoUmy2fQduAz--6N82xIOmufY1ZZeRi5x--B7m1pNIY&SAMLRequest=lJJBj9MwEIX%2FSuR7Yzt10sZKIpWtkCotsGqB%2B5BMW4vELp4JsP8et4DYE5Tr%2BPnN957dbGY%2B%2Bz1%2BmZE4%2Bz6NnloxR28DkCPrYUKy3NvD5s2jLXJlLzFw6MMosg0RRnbBPwRP84TxgPGr6%2FHD%2FrEVZ%2BYLWSl1WVXaGJP7UwyfcxckwTQWEnoS2TbtdB6uHn9uuOGSczqgs%2FuUh3i6DmTaenQjyitGIfc4uIg9y8Phnch221a4YVFjpVflcqgM1sUajiWsYGk01KujKVRfJyHRjDtPDJ5bUShdLrReLNX7QtmysrrMK6Pqem3MeqFKq5TInn6lfeX84PypFSL7iJFuwKkN0TU303hPc%2FC7L5G9DnEC%2Frv8OkmxjjepRc%2BOn0X3r14nZBiAoZE%2FwbrmbfLZbZ%2FC6Prn%2F3zgcQzfHiICYys4zii6%2B4E5gieXsBv5kqBr5Msf1%2F0IAAD%2F%2Fw%3D%3D \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/evilcert.crt b/selfservice/strategy/saml/testdata/evilcert.crt new file mode 100644 index 000000000000..9276d9dcf75d --- /dev/null +++ b/selfservice/strategy/saml/testdata/evilcert.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFbTCCA1WgAwIBAgIUWq4yeMsGByiaIrpB6pTtIdcjhcEwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzAxMTkxMDA1NTFaGA8yMjk2 +MTEwMjEwMDU1MVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAMjjZ/jQqiR+TnOxerWMWnSCOssESP12KF2K5gHV +d6ymjb3/hwuDKFEV02m1VlGgtrqjKfsRrLpuvkFBDhN0Erpi9P9cE/lOdYecMilz +1HUBKneL+k5dJ2AJjV/jkp3FYSmoPhRXZyDpLZthpaBOiiHBcRctrOmY5f9cuyXL +nQpaXHLUbQtaYv2fwgy+wbcnR7sjIHDRTszQjvRkLA7hjMBIIMnlqugt85mEYYqQ +QYNXxu2K/M8tK8vAOxvrwvPGEsOX2G2pXFSHe65EUIhjEeO2yojMHWYz3eTSCKXt +cRnXfZIjXN2v5I0mVlo7SWP1iHC42EiufesoCltmOIHwC1Gg22Cg335cWQAxqacA +pJkkGSgeo1to/QszoTu+VrqjuC+hvLbDzqS8asnRsDwZoXJv/hKMglqo21JrVCJ7 +SnhVXXLfrkx4N55YHqGojdvu7ntkh2IRxGY8mWydSYCGnZNWtNJuOyr0gHcqwMl8 +vKD5EmjeLWcDNja+bvxY+K5OaRZB89QNZc9DeCzuHLOIMDGk40fISQ8vAwWoYECT +y+KPfEBozzUHxf7m4jjJnpm1RI9q0iJOJ1lf65geDuDolMa4iVlnOjmC2+MuzCDz +wd9WAR9XEqhNOAU23COQa+Lwgay21W9Xkor+xVxCr4/b1EmD4fThXoDarVGpau5D +tgSLAgMBAAGjUzBRMB0GA1UdDgQWBBSvI5Jvly7KgEmzj+RBxFZJW72duTAfBgNV +HSMEGDAWgBSvI5Jvly7KgEmzj+RBxFZJW72duTAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQBBSnjAJaPQiprZwyLXhrX6UwDgHLFOduddRa27/ASf +jUWSitPmiTHFsm+rJaLU3HuDCJPfeb9Rs7pJNNWffpMxPgDs+8apzxpfRCrtsGlU +bxJf7F7olWzStAfgXHhTuvx2X0Q1mTHy8cQALjAxY08IvvMSkKoEXZnXBK6jNU1b +srIjAzejTJO3swY0tXu1CDD+Iwr16gtJ4uuev9LDFhO5nooTgwwzdM+LP2R53bwT +pMJon6lWsvyjhBCNn9psO7V70/xMdnDAoJxqR+byngC+E6Y4XuAFunzWjG7Htzno +1D/6VGVcXQJa3hGr9QSx+f3AA6VXUlYojIm+uZkYVxbFrSKMt+ry216Nvr5vqmfZ +IaMBq8/gegvHvdo0IPF/urEduG8ELHrLCYyRZeIYQqQM9N8TeWtX2shEMRd+LpIX +wRjuQfdLUAWI1Sre8iXHF3gk88UY3pc7bDk1dcL9l9tHys39NmOhltOof6397+7S +xGSw2MSwAazTYv1eAsFZXh/r3PYmha2H7mdBvmjhOm5EttPuv9ZZfQTE+r2UaqbP +4iggnJcrnXy6ahwLP1l3dLxU0wtHkRZ2IQD0OQ5OyQpl6ORubwDBbTo5kwVsOmu2 +0yyUl2zdkWWgK2XYnMvrTG7U7n6kZxU755tWfb47f0lDxgTIj5W0Zo0FgDAc4AUH +qw== +-----END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/evilkey.key b/selfservice/strategy/saml/testdata/evilkey.key new file mode 100644 index 000000000000..7048d5784532 --- /dev/null +++ b/selfservice/strategy/saml/testdata/evilkey.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +uRuG0/FW/PEfgJ18NVy8GwjInUbZ6ZU= +-----END PRIVATE KEY----- diff --git a/selfservice/strategy/saml/testdata/expected_metadata.xml b/selfservice/strategy/saml/testdata/expected_metadata.xml index 05039766f75f..a2ddb8cd5479 100644 --- a/selfservice/strategy/saml/testdata/expected_metadata.xml +++ b/selfservice/strategy/saml/testdata/expected_metadata.xml @@ -1,4 +1,4 @@ - + @@ -19,7 +19,8 @@ - - + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/cert.pem b/selfservice/strategy/saml/testdata/idp_cert.pem similarity index 96% rename from selfservice/strategy/saml/testdata/cert.pem rename to selfservice/strategy/saml/testdata/idp_cert.pem index 52667ef39ff2..cba15632963d 100644 --- a/selfservice/strategy/saml/testdata/cert.pem +++ b/selfservice/strategy/saml/testdata/idp_cert.pem @@ -10,4 +10,4 @@ Rsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgk akpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeT QLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvn OwJlNCASPZRH/JmF8tX0hoHuAQ== ------END CERTIFICATE----- \ No newline at end of file +-----END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/key.pem b/selfservice/strategy/saml/testdata/idp_key.pem similarity index 96% rename from selfservice/strategy/saml/testdata/key.pem rename to selfservice/strategy/saml/testdata/idp_key.pem index 48284dac33a1..c4530a84babb 100644 --- a/selfservice/strategy/saml/testdata/key.pem +++ b/selfservice/strategy/saml/testdata/idp_key.pem @@ -12,4 +12,4 @@ Rq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA yfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr vBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6 hU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA== ------END RSA PRIVATE KEY----- \ No newline at end of file +-----END RSA PRIVATE KEY----- diff --git a/selfservice/strategy/saml/testdata/idp_metadata.xml b/selfservice/strategy/saml/testdata/idp_metadata.xml new file mode 100644 index 000000000000..210c78b7104f --- /dev/null +++ b/selfservice/strategy/saml/testdata/idp_metadata.xml @@ -0,0 +1,31 @@ + + + + + + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + + + + + + + MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmHO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKvRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgkakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeTQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvnOwJlNCASPZRH/JmF8tX0hoHuAQ== + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/myservice.cert b/selfservice/strategy/saml/testdata/myservice.cert deleted file mode 100755 index a815f8f44742..000000000000 --- a/selfservice/strategy/saml/testdata/myservice.cert +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDITCCAgmgAwIBAgIUAKe3G3G4JRoPJDbHcFfUC0M1vUwwDQYJKoZIhvcNAQEL -BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTIxMTIyODEw -MTcxOFoXDTIyMTIyODEwMTcxOFowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w -bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA456eHhpbTabo -JD9IurVIakdb4Y1CtM1cWEgeDB/owu+h13pqj+wk/1AlFUNIYKfzJNmP+CoJv5pS -vUeJaMdA7vKUCHPMY7SNoZdaX0eGV4Z9Q7Q6pSkV+heoamojl+Lq9VIVvWnz4ra9 -3xjvJJ4bACyIz7k9u32jAb+v3Rh3axVlPfYJqCx0gU+tcMxb/Lc7HH7ynAjFGc4N -iG7qOqE2nmzRanKw4dMJhkzhNyFQbqtd4DmEzV70XixyztxmbENVfNdvOrCc34/e -JR4q7w5YEGMwUIPip7/zz/itqsrk0x4/VF1lExMOihf8dfYnqdF3+SdywoBf5UC4 -AUyFS/3FgQIDAQABo1MwUTAdBgNVHQ4EFgQUdG+6zhMmsR2yenGz22Iacjeh6BUw -HwYDVR0jBBgwFoAUdG+6zhMmsR2yenGz22Iacjeh6BUwDwYDVR0TAQH/BAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAQEAU5eJKGCBsJpMgL6AgrtpY47iT2KtIkeiI5RC -L+2z2pORG2jFzvY+3kcYA+Nj7EwVyBGmn2lL2JCgk3Qr1YsO4IMJ6sZYbDi6I1SR -z14QMYDRWqPY7VoyqiDzdIS9ENWm80gCG4BChSMtEtN2kmjdTOM++Cr4LY/LLhM4 -9aSNfXHTx4kklP1VVc8dGWw+bFtzZUeP6O+ssrFhcse4V6DoQAxYSU4MAAjePhAP -0IS2I3sSzLe/LCsJMPZv0r1q8YQCGBrijAXSnQiu8KFh8hEQusxilIZV9XPDGB98 -EwTT5cbtUtOIbrZ6kdBs49O27xCTymaIuysidFtywwTaDdrc1g== ------END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/myservice.key b/selfservice/strategy/saml/testdata/myservice.key deleted file mode 100755 index e7b461f2f228..000000000000 --- a/selfservice/strategy/saml/testdata/myservice.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDjnp4eGltNpugk -P0i6tUhqR1vhjUK0zVxYSB4MH+jC76HXemqP7CT/UCUVQ0hgp/Mk2Y/4Kgm/mlK9 -R4lox0Du8pQIc8xjtI2hl1pfR4ZXhn1DtDqlKRX6F6hqaiOX4ur1UhW9afPitr3f -GO8knhsALIjPuT27faMBv6/dGHdrFWU99gmoLHSBT61wzFv8tzscfvKcCMUZzg2I -buo6oTaebNFqcrDh0wmGTOE3IVBuq13gOYTNXvReLHLO3GZsQ1V81286sJzfj94l -HirvDlgQYzBQg+Knv/PP+K2qyuTTHj9UXWUTEw6KF/x19iep0Xf5J3LCgF/lQLgB -TIVL/cWBAgMBAAECggEAAn9H/s6NN+Hf5B3pn1rDy56yzFuvYqpqG/HWmo1zEUht -vx5xstiFY2OutHgDgEP3b+0PHkrfxoFb7QWu5T5iYPy6UQlsMZ/WefJeJHN1btpj -321Hw24a9p5x05EMiOsNZtmasXRLH66fkKYGYaF2bF8QtS60Fa2AL1G6DTPqg3s4 -T+ijNYPr1xUk5GSh8Ea0DjLhzL6WgSHj+eBKgfEdYPDlOaQaYQuV2OJg9JyqxV6h -/Fa1HDc6RgpIhalLhP+9OqhSr9vmXSzEidzu+WTQSPpabwlVIae30Qh8XT9bYF5v -TElDXv5e5FwFmIJTnhAHyGlpnJ3KzaEHkmGbAxLOQQKBgQD2P4++d0WzrugKnfpz -hMpIVwk4jl1l2LUe3LoKEtF85lj6NjmvUNEPfJ0MIwKAjQYZ9AJWgCPP2/kjDBRv -dwwtSDIjFf79y810MNTGhAKv8nf7Lf5tSiJbvWgwtiiqF/ivUlxOKL9jqc6qj2s9 -psFoPOSAHQz6NqNpGyNza/7+CQKBgQDsojNWLJUXVzeUCMCzF+tn8lgs1aGrjHB7 -ZMHpr5nZCBdXjAzZR6yQH653Fa3OzNnVjq8CiO1ZdvbwW/KgVUHB4Mb/4kJ0Uxbm -WOF7zQjsMleoABFTi5mCcSqEK+u1qnrG8Ful9L6F8WhP7mdDmRXQM3f9rG2NDb1H -/OJuj/LpuQKBgQDK0+31Z069QtsUK62oSv9G+JG6yOC7S/Vbt1lxhLCSnTU620FG -W13n0K+W2JtuATq+U9M9JozY4ApkyMVoTnl0LtxFNA/1QlI3WyVXYlLIVAJpnSfN -I1wLjoZsYQ47lEUdO8yWAFAsqih1Km6duGXkEwvvTn5q9mhA4b6giprc6QKBgQCR -knMcd068ziXdxsitJHDoQHkoE8BiZYIpFuIIHcP6dPTPIdQhsusguqy8i7Sh/Pmh -XCaj25KQMBRX52jKY8iROfOSJSIWp6r1yAXnAEqV655rNqdyCvZD/dRW/SIDXz4q -tmDbJkYy5kDys0oJltqJe7A8eV/nn2UrLRIrTBj22QKBgQCFMmXVRqRje9k0Aqfe -KGYYCEPzeFzY4PzufwoOyhsGkLCwKthf43jXjWy53+u82Od1oKiNCjIhQHOtL720 -mTIhl2AzTJ1VMWoqUIHtGxhaIC3zhDjAaTMHZNDXFU78hPOhcBPtKikh3Hj2bfGG -TK1KTG49VMcWHmYJhJXwVevKAg== ------END PRIVATE KEY----- diff --git a/selfservice/strategy/saml/testdata/saml.jsonnet b/selfservice/strategy/saml/testdata/saml.jsonnet index 87103e26bc6b..2b97922447ec 100644 --- a/selfservice/strategy/saml/testdata/saml.jsonnet +++ b/selfservice/strategy/saml/testdata/saml.jsonnet @@ -12,6 +12,7 @@ local claims = { // Therefore we only return the email if it (a) exists and (b) is marked verified // by Discord. [if "email" in claims && claims.email_verified then "email" else null]: claims.email, + [if "groups" in claims then "groups" else null]: claims.groups, }, }, } \ No newline at end of file diff --git a/selfservice/strategy/saml/testdata/saml_response.xml b/selfservice/strategy/saml/testdata/saml_response.xml index 22ea0920a014..05b53a365821 100644 --- a/selfservice/strategy/saml/testdata/saml_response.xml +++ b/selfservice/strategy/saml/testdata/saml_response.xml @@ -1,5 +1,5 @@ https://idp.testshib.org/idp/shibbolethMIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJVUzELMAkGA1UE CAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTEzMTAwMjAwMDg1MVoX DTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28x diff --git a/selfservice/strategy/saml/testdata/samlkratos.crt b/selfservice/strategy/saml/testdata/samlkratos.crt deleted file mode 100755 index 3dfdeb703e1c..000000000000 --- a/selfservice/strategy/saml/testdata/samlkratos.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDazCCAlOgAwIBAgIUVREfiVXf4z/hq8AsbyNnkuWn6i8wDQYJKoZIhvcNAQEL -BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjAyMjExMTA4MjBaFw0yMzAy -MjExMTA4MjBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw -HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQCjvij3wZV+OhbEbwcs7cpc1hGR+uK4Y0y/ItHkAqlV -ddl+D28iDJeHci4LA8XmG0loFMTxdC9PG5t4ewn8G18+EeYRV0K3BMMWfxrO6ibG -z1ElTxQvVSw9tgPpjIgZqL8Qso8UO1ji98yoPhqP77F29pCNqiHrKJI1c52OCPHq -NBCZa76DmCGcXKAwRQaTo+tig6HJ1/3qCLGq57O396mQRFvjB535mceLzKSpFHsh -45beytXiBjTkvOEmNIUGVKIidXxqDtuTHz5QqhHTHMSsFH8cT648sSB9K9jPZ6ai -VCq5z/McyaYFlb/wt7PApJTSRjU0Any4876eBca59ca/AgMBAAGjUzBRMB0GA1Ud -DgQWBBQml5ORluABegdU+rLlpn++esD9fjAfBgNVHSMEGDAWgBQml5ORluABegdU -+rLlpn++esD9fjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCL -X5bpRKtMY7FsPtMsO/KBz5GT7P6aqe8pS0m3uXap6KkQwxa2wyyyH+in6uds8Sxm -bsdsGpSpCfGQCMqmu0yCjhfwI8nFA6q1YxLNgmx7kEIAQQQG2+jZJE7adXzSk2vT -tiNQ55mfiO9Wv+JpaB7ldAX3Q+O2uqVLJG/NlvC3ZAq0FXMyeitddLYSmEE0xrcM -QTB7vb7LpZk7Owa2UJ2VcQyZcxLWMonikIg4u3ALHGR0SvEgMwGhWr354RDGLYSO -Ii5O1foUR1O71jffr7CgELauyz3AXv6PNYLkyOCQP5gNB2NEMLJBRn5U4IhCHKzD -t1/BujsTuZV5r6aj3J9+ ------END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/sp2_cert.pem b/selfservice/strategy/saml/testdata/sp2_cert.pem new file mode 100644 index 000000000000..e74d8fcb8c00 --- /dev/null +++ b/selfservice/strategy/saml/testdata/sp2_cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFbTCCA1WgAwIBAgIUDxKgJwZpe1Fmo4nFUVYTXwvIgOAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzAxMTExMzQ4MTlaGA8yMjk2 +MTAyNTEzNDgxOVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAM6XqltTvOTIqGZeUKnRfWqZdmjiJWU4XtSh4D8v +DUmBRIYXEBbcSqcNKsHjNoHoTXBqa9jkxl5ZIOwE3mCWr5heXTR9T4IhtsWRopWB +Aio8AIdAPuHiQoMxGm0jwZ4KCaZfLsHRIflJloSFEtebungmpDqo4hwsM6IUz5z3 +bKX1rlCCtupHpLihaLMVmgSSBvLDeAPvtHIBtNjNyhhi9TMS8SxFbCI7+PXRTRrR +G2ozUrR8qIBQxh/kA/rQWH4GGLt6phBdsxnmdn2idLhyvTjT8nA09gglvXZZ7gI9 +m8jbmUgpVGzmk38cAw+oENVMufAZWJBdAfrIfK0TR5YqXqhfj6e5BBf4OyM4rMlo +xlJnbQTI30uB2uzPfjry43NSu9jlFc/+r4ufW+ptEH9YIx9HbSz8y+hrpFdofSEQ +8J6w71x3NzN1L/hFKYqhQ8Gv2Flk1kp0iLkZ3tP1UCK2eDFhM9BBD3ZHYyByboWj +sKffst67LUAlufiLW4q0tZkpWwUR5fExAsFF//CciKPIpe8WBKlHxw6FYGPRGva5 +KK10jGZTJoM+KYwcZFpHCaWt8tFQB8ti9bQIP2etmrpsBZuR4CDuZEkMuvSLsJE5 +6SI+rxT3WKwK9zAginomFvEEvaqp43RT2a1zzYAfE3pRETis1tyoNN2I384ywL41 +lofhAgMBAAGjUzBRMB0GA1UdDgQWBBSmECM2phplGyTKV/dGef+JUPVpgjAfBgNV +HSMEGDAWgBSmECM2phplGyTKV/dGef+JUPVpgjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQCxbwfNQvpw68pTmyCIipb5pkuVDnjp65RV0wJbOfDR +qiQHVQsJexY1xmptOADzCvBQIkAAKCeLfJ8tKS6473Xc3BayREyJpN3oQsr1MDep +j6/ae8I1wt6uJ18M93wArWou/nuDHlkBeEKYlwCQYRZPW++9E38v6ZzKK7qHN+6M +vFKXx/Q98WpaNo5Oj0o8ngEYxS5/9Axn7EUBKLpikb8KNIO+icqc6DPs4GTqKb4/ +wC5FPcROoQAau3RsrZ8cAMU+zGt6OeYWU2Sabsnm1lo3bYMx2XuQOEQvLcrTjBLm +041LYk3SbPotBWc4ahVF4SUZWHKZst76+cZtR5RLZt3jjKjTguq76itPnuZxdM0g +JmdhophvFNwyKjxQ3jbJc9W1mpq5ILrtzO0pWTjOrBDWdZ4GF078GmjYrtJJ2e7T +LI0uuXwKB0K5SktluIM+7PVXYqt3ZnPJ6zjMCuYoQT5ua29hs9Qi/zjAf2Mf8JLo +t2MdAmvVZDr8bSVkyx0RrIKwYKLJ6b+KgdSACb618GV6dLpqMbe3mC+yPPa/FKIS +M64SBf/gBlMQpcUWdH4IWvQXu1Lmn+TPr4+BXi7loMGwAcGH7pcbYouOMsZlJ/CG +1cQ5cf3kevKolmJVaxJC+ZEBCqvM3/FySSmNNCmQidXi5QLHP87uYn/9aRaHh1kM +eA== +-----END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/sp2_key.pem b/selfservice/strategy/saml/testdata/sp2_key.pem new file mode 100644 index 000000000000..9bf6aeab4b89 --- /dev/null +++ b/selfservice/strategy/saml/testdata/sp2_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDOl6pbU7zkyKhm +XlCp0X1qmXZo4iVlOF7UoeA/Lw1JgUSGFxAW3EqnDSrB4zaB6E1wamvY5MZeWSDs +BN5glq+YXl00fU+CIbbFkaKVgQIqPACHQD7h4kKDMRptI8GeCgmmXy7B0SH5SZaE +hRLXm7p4JqQ6qOIcLDOiFM+c92yl9a5QgrbqR6S4oWizFZoEkgbyw3gD77RyAbTY +zcoYYvUzEvEsRWwiO/j10U0a0RtqM1K0fKiAUMYf5AP60Fh+Bhi7eqYQXbMZ5nZ9 +onS4cr040/JwNPYIJb12We4CPZvI25lIKVRs5pN/HAMPqBDVTLnwGViQXQH6yHyt +E0eWKl6oX4+nuQQX+DsjOKzJaMZSZ20EyN9Lgdrsz3468uNzUrvY5RXP/q+Ln1vq +bRB/WCMfR20s/Mvoa6RXaH0hEPCesO9cdzczdS/4RSmKoUPBr9hZZNZKdIi5Gd7T +9VAitngxYTPQQQ92R2Mgcm6Fo7Cn37Leuy1AJbn4i1uKtLWZKVsFEeXxMQLBRf/w +nIijyKXvFgSpR8cOhWBj0Rr2uSitdIxmUyaDPimMHGRaRwmlrfLRUAfLYvW0CD9n +rZq6bAWbkeAg7mRJDLr0i7CROekiPq8U91isCvcwIIp6JhbxBL2qqeN0U9mtc82A +HxN6URE4rNbcqDTdiN/OMsC+NZaH4QIDAQABAoICAQCZ1EbOUBDkDiGOcAYCHPIV +EQYhXNrZftrl208d3Qw4wl9itQOO8iNINj6zNltc6bvXy/ZX/ylSEW25MHrhUvKX +MxSVxAUS8cWlYSa9ydzx09HU49qu2YoLI+H4iFpgMjszPcaUHQP+GnRQYsI/9z4m +vyckYqJStfsQYgyhZX7qKIDOhDZtRkF6FP3f82LGqnEwDKpty+wBxBGEKd+kvvKz +QBSCkYLODvf3Gg0evbt7HZIkwHm7aenMzzzDYqWx2RpLZy0GHK8Cxx9Nt0zQFuec +y/zG3jigonFsEdRuqK86JYICQHwTxrDnQdVpsAwwtzvwcv8GJ6sUsHpdaXCxeQUX +ZC0k20JkUzAlNCUT+w+MaN6gfO27NFknmDYvtV46z7dm2gaCE2IGi/Z890xg/wTl +3OelqnNM9+qNRWCl0JLgVPTlcWexceg8DJVLTuE1R7qRND/FIG1IzMJUGQdhGwnn +ehJ3BvoMj+RxHkVnljN2wDYWtlbfJcjbBL/ruUbDHKsUB85j9RCkTWDvetUs6cPG +QB+CJ/SxI4/2Wlw58e/kG22uja+SvZJuwza7M3pOkv1Dxa+6ASj/O6sooEkVJ90y +aoM8A5b+FoS8XGoAJu3D3cV3GwYfE6Yue5nCcp10Wh/MQ+e/HqaWZHmgNjY0zd1I +1vyeQieJyT7oA7XsMDt1UQKCAQEA/mzRWKFf3C7DsgoxHDc7nGF6ojhK+z+ujfxj +qDyUqXZkx7UmY/2hp2/20ndXbla8TqFgkdh1zj4uRqbMlgNStlQnYzIwU6IBtfPo ++NrFPmlnZ4pHqMNkATO+oYegKASOaqF53aD09PV0KhzcfJpkSgxbOOE5Gt5sfj5q +z5AO6aLFQXUaykCZU/mf+WneX5YltXpHLelx6YyIL/H/RQjbR7A8V5ug7dkuy+U/ +RUb07sJ5AmeH+dbMiDpwLyFxfltEKdV21Ex/oGrdVHvPYzuhT/IVHujqdMmkODJ5 +PD+jmHcted0nZ0VH9gxprGkGF3PlsSX+KcuvrGU4uN6yasi9hQKCAQEAz98MXcbs +5akVyq9U4cIGeyn1miGwlNpKPxBOqWEc1lKqUHTCayD+SUrXfd0zSi8R5VSp9MuX +ciiibkb5/vBrLYXZRtZCtaVUPlyH/PvKIlsYP+J4QMqZJGHIFUpPsF9SQjmBmktF +J+N00WT7iGfJiFpHc05aq7yo2/AgiLmjWUsDYXxHwTkDT/q838K4gsIdOCh1sUQr +tT/LhciHEp9yF8hcQ38BPPnp7NHalwx8WndgjNqC6gaQ5T01K0YKH5mtuyPpJj4d +p9RqqcLju8FzNiEld57JQJPSb6MW5cKYu0uSArRLdAMeVYMThYCHHu15Z0zEy3ce +PAlY8G1pfYgxrQKCAQAbdGKizccqW2GCtNbX1J36Igq5tplgw15ys+mNHfxszPnT +ExkxcQ0gpFReIcKthW6MjZ1+H32W497agOVSyskCI9KcQa41WCYXHFrnf7QJKBag +dauF6o/AEXVguOHvb45usz4TTGsig9olMTgZug9YbjzpxmQDIj1S4ilkfIcfbxEa +Hyjk6lOhXC6HG4WDixBGpQtJSQehzChmBBcnu+ztr3bTfVfAUs9Z8UMClsWXfiTQ +vZtOun8XtDam31T/7ZlNaluITTj4do+rrjCS5LxjhBwDWd7y+09dQRUUC0n8CeA+ +Zj76Rd+eDXjZwfuGTFtc4lyq5e/vCn00ddOK8l6BAoIBAQDJPGBHZK2wA4myJxyg +VWpaz5sRdK3y3IRmGs5cEUSOg4aXzwDsHwutPoPxODRQC9NiVR0XfAUIIihlY9bf +JDZN4rceaYw5N22f1YpcshDUQ6XtKrxJ1Rh+bR765W7SCuWicPNzwIyZegx8LiuH +uRoUI3nqOZ9zhHdgPE3yruxhJEqIlH0OpLf9NHqmkGZ5R5xr4ldVne5GUBUiVafV +soAMYA5Z1VkIg9QfTGU2N4MnPUw978gu8N5S3ndbhjmEsAzND43FVPr2n6AG6kH3 +YOa9L0eLTy/7kV92bcdb9JBROW6Hqa0mCWLTW8qJQo0Mts8B3wLhClc9vbrZPsKS +IUgdAoIBADTgbwunp+95gp7eC2VPM42T/cZfCjHmThzF6dNJOWEVgUTsvjTqND7C ++XDyCgTVbyBWiEl8OA3ePl8oDVUX96Bd4TE/7wwfGe4wn85XcchdZmFbIN/2cS7T +eHEq85IY676T8WLU3LdEu/fPn4xYCT9fx32JB7IfZDuJ6liQUrjQFhZrC6eIFQON +He4niCxUTt1VxMb+0dtVGF2sBBW/rfg9BlOW+Jrllhm6RWTzlajCkmZW0BWwl5zi +KTt/KgAF7SkGoW57znDr9soJcLPaPAhjdYkxmuPPqwn94Qw6IX24DL6QLNALVlf5 +d7y+GE7k0jSTLxP851L5BvfqKi2Ay0w= +-----END PRIVATE KEY----- diff --git a/selfservice/strategy/saml/testdata/sp_cert.pem b/selfservice/strategy/saml/testdata/sp_cert.pem new file mode 100644 index 000000000000..cba15632963d --- /dev/null +++ b/selfservice/strategy/saml/testdata/sp_cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0 +MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9 +ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmH +O8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKv +Rsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgk +akpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeT +QLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvn +OwJlNCASPZRH/JmF8tX0hoHuAQ== +-----END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/sp_key.pem b/selfservice/strategy/saml/testdata/sp_key.pem new file mode 100644 index 000000000000..c4530a84babb --- /dev/null +++ b/selfservice/strategy/saml/testdata/sp_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDU8wdiaFmPfTyRYuFlVPi866WrH/2JubkHzp89bBQopDaLXYxi +3PTu3O6Q/KaKxMOFBqrInwqpv/omOGZ4ycQ51O9I+Yc7ybVlW94lTo2gpGf+Y/8E +PsVbnZaFutRctJ4dVIp9aQ2TpLiGT0xX1OzBO/JEgq9GzDRf+B+eqSuglwIDAQAB +AoGBAMuy1eN6cgFiCOgBsB3gVDdTKpww87Qk5ivjqEt28SmXO13A1KNVPS6oQ8SJ +CT5Azc6X/BIAoJCURVL+LHdqebogKljhH/3yIel1kH19vr4E2kTM/tYH+qj8afUS +JEmArUzsmmK8ccuNqBcllqdwCZjxL4CHDUmyRudFcHVX9oyhAkEA/OV1OkjM3CLU +N3sqELdMmHq5QZCUihBmk3/N5OvGdqAFGBlEeewlepEVxkh7JnaNXAXrKHRVu/f/ +fbCQxH+qrwJBANeQERF97b9Sibp9xgolb749UWNlAdqmEpmlvmS202TdcaaT1msU +4rRLiQN3X9O9mq4LZMSVethrQAdX1whawpkCQQDk1yGf7xZpMJ8F4U5sN+F4rLyM +Rq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA +yfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr +vBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6 +hU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA== +-----END RSA PRIVATE KEY----- diff --git a/selfservice/strategy/saml/testdata/token.json b/selfservice/strategy/saml/testdata/token.json new file mode 100644 index 000000000000..c85dbac81d7f --- /dev/null +++ b/selfservice/strategy/saml/testdata/token.json @@ -0,0 +1,46 @@ +{ + "aud": "https://15661444.ngrok.io/", + "iss": "https://15661444.ngrok.io/", + "exp": 1448942229, + "iat": 1448935029, + "nbf": 1448935029, + "sub": "_41bd295976dadd70e1480f318e772841", + "attr": { + "SessionIndex": [ + "_6149230ee8fb88d3635c238509d9a35a" + ], + "cn": [ + "Me Myself And I" + ], + "eduPersonAffiliation": [ + "Member", + "Staff" + ], + "eduPersonEntitlement": [ + "urn:mace:dir:entitlement:common-lib-terms" + ], + "eduPersonPrincipalName": [ + "myself@testshib.org" + ], + "eduPersonScopedAffiliation": [ + "Member@testshib.org", + "Staff@testshib.org" + ], + "eduPersonTargetedID": [ + "" + ], + "givenName": [ + "Me Myself" + ], + "sn": [ + "And I" + ], + "telephoneNumber": [ + "555-5555" + ], + "uid": [ + "myself" + ] + }, + "saml-session": true + } \ No newline at end of file diff --git a/selfservice/strategy/saml/vulnerabilities_helper_test.go b/selfservice/strategy/saml/vulnerabilities_helper_test.go new file mode 100644 index 000000000000..b49f433d81b8 --- /dev/null +++ b/selfservice/strategy/saml/vulnerabilities_helper_test.go @@ -0,0 +1,330 @@ +package saml_test + +import ( + "bytes" + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/xml" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/beevik/etree" + "github.com/crewjam/saml" + "github.com/crewjam/saml/logger" + "github.com/crewjam/saml/samlsp" + "github.com/crewjam/saml/xmlenc" + "github.com/julienschmidt/httprouter" + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + samlhandler "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/kratos/x" + + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt/v4" + dsig "github.com/russellhaering/goxmldsig" + "github.com/stretchr/testify/require" + "gotest.tools/assert" + "gotest.tools/golden" +) + +type MiddlewareTest struct { + AuthnRequest []byte + SamlResponse []byte + Key *rsa.PrivateKey + Certificate *x509.Certificate + IDPMetadata []byte + Middleware *samlsp.Middleware +} + +type IdentityProviderTest struct { + SPKey *rsa.PrivateKey + SPCertificate *x509.Certificate + SP saml.ServiceProvider + + Key crypto.PrivateKey + Certificate *x509.Certificate + IDP saml.IdentityProvider +} + +type mockServiceProviderProvider struct { + GetServiceProviderFunc func(r *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) +} + +func (mspp *mockServiceProviderProvider) GetServiceProvider(r *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) { + return mspp.GetServiceProviderFunc(r, serviceProviderID) +} + +func mustParseURL(s string) url.URL { + rv, err := url.Parse(s) + if err != nil { + panic(err) + } + return *rv +} + +func setSAMLTimeNow(timeStr string) { + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05.999999999 MST 2006", timeStr) + return rv + } + + saml.TimeNow = TimeNow + jwt.TimeFunc = TimeNow + saml.Clock = dsig.NewFakeClockAt(TimeNow()) +} + +func (test *MiddlewareTest) makeTrackedRequest(id string) (string, string) { + uuid, _ := uuid.NewV4() + + codec := test.Middleware.RequestTracker.(samlsp.CookieRequestTracker).Codec + index := uuid.String() + token, err := codec.Encode(samlsp.TrackedRequest{ + Index: index, + SAMLRequestID: id, + URI: "/frob", + }) + if err != nil { + panic(err) + } + return token, index +} + +func NewMiddlewareTest(t *testing.T) (*MiddlewareTest, *samlhandler.Strategy, *httptest.Server) { + middlewareTest := MiddlewareTest{} + + samlhandler.DestroyMiddlewareIfExists("samlProvider") + + middleWare, strategy, ts, err := InitTestMiddlewareWithMetadata(t, "file://testdata/idp_metadata.xml") + if err != nil { + return nil, nil, nil + } + + middlewareTest.Middleware = middleWare + + middlewareTest.Key = middlewareTest.Middleware.ServiceProvider.Key + middlewareTest.Certificate = middlewareTest.Middleware.ServiceProvider.Certificate + middlewareTest.IDPMetadata = golden.Get(t, "idp_metadata.xml") + + var metadata saml.EntityDescriptor + if err := xml.Unmarshal(middlewareTest.IDPMetadata, &metadata); err != nil { + panic(err) + } + + return &middlewareTest, strategy, ts +} + +func NewIdentifyProviderTest(t *testing.T, serviceProvider saml.ServiceProvider, tsURL string) *IdentityProviderTest { + IDPtest := IdentityProviderTest{} + + IDPtest.SP = serviceProvider + IDPtest.SPKey = IDPtest.SP.Key + IDPtest.SPCertificate = IDPtest.SP.Certificate + + IDPtest.Key = mustParsePrivateKey(golden.Get(t, "idp_key.pem")) + IDPtest.Certificate = mustParseCertificate(golden.Get(t, "idp_cert.pem")) + + IDPtest.IDP = saml.IdentityProvider{ + Key: IDPtest.Key, + Certificate: IDPtest.Certificate, + Logger: logger.DefaultLogger, + MetadataURL: mustParseURL("https://idp.example.com/saml/metadata"), + SSOURL: mustParseURL("https://idp.example.com/saml/sso"), + ServiceProviderProvider: &mockServiceProviderProvider{ + GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) { + if serviceProviderID == IDPtest.SP.MetadataURL.String() { + return IDPtest.SP.Metadata(), nil + } + return nil, os.ErrNotExist + }, + }, + } + + IDPtest.SP.IDPMetadata = IDPtest.IDP.Metadata() + + return &IDPtest +} + +func NewIdpAuthnRequest(t *testing.T, idp *saml.IdentityProvider, acsURL string, issuer string, destination string, issueInstant string) (saml.IdpAuthnRequest, string) { + uuid, err := uuid.NewV4() + assert.NilError(t, err) + id := "id-" + strings.Replace(uuid.String(), "-", "", -1) + + authnRequest := saml.IdpAuthnRequest{ + Now: TimeNow(), + IDP: idp, + RequestBuffer: []byte("" + + "" + + " " + issuer + "" + + " urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + + ""), + } + + authnRequest.HTTPRequest, err = http.NewRequest("POST", acsURL, nil) + assert.NilError(t, err) + assert.NilError(t, authnRequest.Validate()) + + return authnRequest, id +} + +func NewTestIdpAuthnRequest(t *testing.T, idp *saml.IdentityProvider, acsURL string, issuer string) (saml.IdpAuthnRequest, string) { + authnRequest, id := NewIdpAuthnRequest(t, idp, acsURL, issuer, "https://idp.example.com/saml/sso", "2014-01-01T01:57:09Z") + return authnRequest, id +} + +func MakeAssertion(t *testing.T, authnRequest *saml.IdpAuthnRequest, userSession *saml.Session) { + err := saml.DefaultAssertionMaker{}.MakeAssertion(authnRequest, userSession) + assert.NilError(t, err) +} + +func prepareTestEnvironment(t *testing.T) (*MiddlewareTest, *samlhandler.Strategy, *IdentityProviderTest, saml.IdpAuthnRequest, string) { + // Set timeNow for SAML Requests and Responses + setSAMLTimeNow("Wed Jan 1 01:57:09.123456789 UTC 2014") + + // Create a SAML SP + testMiddleware, strategy, ts := NewMiddlewareTest(t) + + // Create a SAML IdP + testIDP := NewIdentifyProviderTest(t, testMiddleware.Middleware.ServiceProvider, ts.URL) + + // SP ACS URL + acsURL := ts.URL + "/self-service/methods/saml/acs/samlProvider" + + // Create a SAML AuthnRequest as it would be taken into account by the IdP + // so that it can send the SAML Response back to the SP via the SP ACS + authnRequest, authnRequestID := NewTestIdpAuthnRequest(t, &testIDP.IDP, acsURL, testMiddleware.Middleware.ServiceProvider.EntityID) + + return testMiddleware, strategy, testIDP, authnRequest, authnRequestID +} + +func startContinuity(resp *httptest.ResponseRecorder, r *http.Request, strategy *samlhandler.Strategy) { + conf := strategy.D().Config() + f, _ := login.NewFlow(conf, conf.SelfServiceFlowLoginRequestLifespan(r.Context()), strategy.D().GenerateCSRFToken(r), r, flow.TypeBrowser) + strategy.D().LoginFlowPersister().CreateLoginFlow(r.Context(), f) + state := x.NewUUID().String() + + strategy.D().RelayStateContinuityManager().Pause(r.Context(), resp, r, "ory_kratos_saml_auth_code_session", + continuity.WithPayload(&authCodeContainer{ + State: state, + FlowID: f.ID.String(), + }), + continuity.WithLifespan(time.Minute*30)) +} + +func initRouterParams() httprouter.Params { + ps := httprouter.Params{ + httprouter.Param{ + Key: "provider", + Value: "samlProvider", + }, + } + return ps +} + +func prepareTestEnvironmentTwoServiceProvider(t *testing.T) (*MiddlewareTest, *MiddlewareTest, *samlhandler.Strategy, *IdentityProviderTest, saml.IdpAuthnRequest, string) { + // Set timeNow for SAML Requests and Responses + setSAMLTimeNow("Wed Jan 1 01:57:09.123456789 UTC 2014") + + // Create a SAML SP + testMiddleware, strategy, ts := NewMiddlewareTest(t) + + // Create a SAML IdP + testIDP := NewIdentifyProviderTest(t, testMiddleware.Middleware.ServiceProvider, ts.URL) + + // SP ACS URL + acsURL := ts.URL + "/self-service/methods/saml/acs/samlProvider" + + // Create a SAML AuthnRequest as it would be taken into account by the IdP + // so that it can send the SAML Response back to the SP via the SP ACS + authnRequest, authnRequestID := NewTestIdpAuthnRequest(t, &testIDP.IDP, acsURL, testMiddleware.Middleware.ServiceProvider.EntityID) + + return testMiddleware, nil, strategy, testIDP, authnRequest, authnRequestID +} + +func PrepareTestSAMLResponse(t *testing.T, testMiddleware *MiddlewareTest, authnRequest saml.IdpAuthnRequest, authnRequestID string) saml.IdpAuthnRequest { + // User session + userSession := &saml.Session{ + ID: "f00df00df00d", + UserEmail: "alice@example.com", + } + + return PrepareTestSAMLResponseWithSession(t, testMiddleware, authnRequest, authnRequestID, userSession) +} + +func PrepareTestSAMLResponseWithSession(t *testing.T, testMiddleware *MiddlewareTest, authnRequest saml.IdpAuthnRequest, authnRequestID string, userSession *saml.Session) saml.IdpAuthnRequest { + // Make SAML Assertion + MakeAssertion(t, &authnRequest, userSession) + + // Make SAML Response + authnRequest.MakeResponse() + + return authnRequest +} + +func PrepareTestSAMLResponseHTTPRequest(t *testing.T, testMiddleware *MiddlewareTest, authnRequest saml.IdpAuthnRequest, authnRequestID string, responseStr string) *http.Request { + // Prepare SAMLResponse body attribute + v1 := &url.Values{} + v1.Set("SAMLResponse", base64.StdEncoding.EncodeToString([]byte(responseStr))) + + // Set SAML AuthnRequest HTTP Request body with the SAML Response + req := authnRequest.HTTPRequest + req, err := http.NewRequest(req.Method, req.URL.String(), bytes.NewReader([]byte(v1.Encode()))) + assert.NilError(t, err) + + // Make tracked request and get its index + trackedRequestToken, trackedRequestIndex := testMiddleware.makeTrackedRequest(authnRequestID) + + // Set SAML AuthnRequest HTTP Request headers Content-Type and session cookie + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Cookie", ""+ + "saml_"+trackedRequestIndex+"="+trackedRequestToken) + + return req +} + +func GetAndDecryptAssertionEl(t *testing.T, testMiddleware *MiddlewareTest, responseDoc *etree.Document) *etree.Element { + // Get the Encrypted Assertion Data + spKey := testMiddleware.Middleware.ServiceProvider.Key + encryptedAssertionDataEl := responseDoc.Element.FindElement("//EncryptedAssertion/EncryptedData") + + // Decrypt the Encrypted Assertion + plaintextAssertion, err := xmlenc.Decrypt(spKey, encryptedAssertionDataEl) + require.NoError(t, err) + stringAssertion := string(plaintextAssertion) + newAssertion := etree.NewDocument() + newAssertion.ReadFromString(stringAssertion) + + return newAssertion.Root() +} + +// Replace the Encrypted Assertion by the modified Assertion +func ReplaceResponseAssertion(t *testing.T, responseEl *etree.Element, newAssertionEl *etree.Element) { + encryptedAssertionEl := responseEl.FindElement("//EncryptedAssertion") + encryptedAssertionEl.Parent().RemoveChild(encryptedAssertionEl) + responseEl.AddChild(newAssertionEl) +} + +// Remove the SAML Response signature +func RemoveResponseSignature(responseDoc *etree.Document) { + responseSignatureEl := responseDoc.FindElement("//Signature") + responseSignatureEl.Parent().RemoveChild(responseSignatureEl) +} + +func RemoveAssertionSignature(responseDoc *etree.Document) { + assertionSignatureEl := responseDoc.FindElement("//Assertion/Signature") + assertionSignatureEl.Parent().RemoveChild(assertionSignatureEl) +} diff --git a/selfservice/strategy/saml/vulnerabilities_test.go b/selfservice/strategy/saml/vulnerabilities_test.go new file mode 100644 index 000000000000..fe8f05c447a4 --- /dev/null +++ b/selfservice/strategy/saml/vulnerabilities_test.go @@ -0,0 +1,1336 @@ +package saml_test + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/beevik/etree" + "github.com/crewjam/saml" + + dsig "github.com/russellhaering/goxmldsig" + "gotest.tools/assert" +) + +type authCodeContainer struct { + FlowID string `json:"flow_id"` + State string `json:"state"` + Traits json.RawMessage `json:"traits"` +} + +type ory_kratos_continuity struct{} + +func TestHappyPath(t *testing.T) { + + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + _ = ids + + // This is the Happy Path, the HTTP response code should be 302 (Found status) + assert.Check(t, !strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestAddSAMLResponseAttribute(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + + // Add an attribute to the Response + responseEl.CreateAttr("newAttr", "randomValue") + + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // This is the Happy Path, the HTTP response code should be 302 (Found status) + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestAddSAMLResponseElement(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + + // Add an attribute to the Response + responseEl.CreateElement("newEl") + + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // This is the Happy Path, the HTTP response code should be 302 (Found status) + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestAddSAMLAssertionAttribute(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove the whole Signature element + RemoveResponseSignature(doc) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Add an attribute to the Response + decryptedAssertion.CreateAttr("newAttr", "randomValue") + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, responseEl, decryptedAssertion) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestAddSAMLAssertionElement(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove the whole Signature element + RemoveResponseSignature(doc) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Add an attribute to the Response + decryptedAssertion.CreateElement("newEl") + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, responseEl, decryptedAssertion) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // This is the Happy Path, the HTTP response code should be 302 (Found status) + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestRemoveSAMLResponseSignatureValue(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove SignatureValue element of Signature element + signatureValueEl := doc.FindElement("//Signature/SignatureValue") + signatureValueEl.Parent().RemoveChild(signatureValueEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // This is the Happy Path, the HTTP response code should be 302 (Found status) + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestRemoveSAMLResponseSignature(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove the whole Signature element + signatureEl := doc.FindElement("//Signature") + signatureEl.Parent().RemoveChild(signatureEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // This is the Happy Path, the HTTP response code should be 302 (Found status) + assert.Check(t, !strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestRemoveSAMLAssertionSignatureValue(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove the whole Signature element + RemoveResponseSignature(doc) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Remove the Signature Value from the decrypted assertion + signatureValueEl := decryptedAssertion.FindElement("//Signature/SignatureValue") + signatureValueEl.Parent().RemoveChild(signatureValueEl) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, responseEl, decryptedAssertion) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // This is the Happy Path, the HTTP response code should be 302 (Found status) + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestRemoveSAMLAssertionSignature(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove the whole Signature element + RemoveResponseSignature(doc) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Remove the Signature Value from the decrypted assertion + signatureEl := decryptedAssertion.FindElement("//Signature") + signatureEl.Parent().RemoveChild(signatureEl) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, responseEl, decryptedAssertion) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // The SAML Assertion signature has been removed but the SAML Response is still signed + // The SAML Response has been modified, the SAML Response signature is invalid, so there is an error + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestRemoveBothSAMLResponseSignatureAndSAMLAssertionSignatureValue(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove the whole Signature element + RemoveResponseSignature(doc) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Remove the Signature Value from the decrypted assertion + assertionSignatureEl := decryptedAssertion.FindElement("//Signature") + assertionSignatureEl.Parent().RemoveChild(assertionSignatureEl) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, responseEl, decryptedAssertion) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestAddXMLCommentsInSAMLAttributes(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + groups := []string{"admin@test.ovh", "not-adminc@test.ovh", "regular@test.ovh", "manager@test.ovh"} + commentedGroups := []string{"admin@test.ovh", "not-adminc@test.ovh", "regular@test.ovh", "manager@test.ovh"} + + // User session + userSession := &saml.Session{ + ID: "f00df00df00d", + UserEmail: "alice@example.com", + Groups: commentedGroups, + } + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponseWithSession(t, testMiddleware, authnRequest, authnRequestID, userSession) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Remove the whole Signature element + RemoveResponseSignature(doc) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, responseEl, decryptedAssertion) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // Get all identities + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + traitsMap := make(map[string]interface{}) + json.Unmarshal(ids[0].Traits, &traitsMap) + + // Get the groups of the identity + identityGroups := traitsMap["groups"].([]interface{}) + + // We have to check that either the comments are still there, or that they have been deleted by the canonicalizer but that the parser recovers the whole string + for i := 0; i < len(identityGroups); i++ { + identityGroup := identityGroups[i].(string) + if commentedGroups[i] != identityGroup { + assert.Check(t, groups[i] == identityGroup) + } + } +} + +// More information about the 9 next tests about XSW attacks: +// https://epi052.gitlab.io/notes-to-self/blog/2019-03-13-how-to-test-saml-a-methodology-part-two + +// XSW #1 manipulates SAML Responses. +// It does this by making a copy of the SAML Response and Assertion, +// then inserting the original Signature into the XML as a child element of the copied Response. +// The assumption being that the XML parser finds and uses the copied Response at the top of +// the document after signature validation instead of the original signed Response. +func TestXSW1ResponseWrap1(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + // Remove the whole Signature element of the copied Response Element + RemoveResponseSignature(originalResponseDoc) + + // Get the original Response Signature element + evilResponseDoc.FindElement("//Signature").AddChild(originalResponseEl) + + // Modify the ID attribute of the original Response Element + evilResponseEl.RemoveAttr("ID") + evilResponseEl.CreateAttr("ID", "id-evil") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +// Similar to XSW #1, XSW #2 manipulates SAML Responses. +// The key difference between #1 and #2 is that the type of Signature used is a detached signature where XSW #1 used an enveloping signature. +// The location of the malicious Response remains the same. +func TestXSW2ResponseWrap2(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + // Remove the whole Signature element of the copied Response Element + RemoveResponseSignature(originalResponseDoc) + + // We put the orignal response and its signature on the same level, just under the evil reponse + evilResponseDoc.FindElement("//Response").AddChild(originalResponseEl) + evilResponseDoc.FindElement("//Response").AddChild(evilResponseDoc.FindElement("//Signature")) + + // Modify the ID attribute of the original Response Element + evilResponseEl.RemoveAttr("ID") + evilResponseEl.CreateAttr("ID", "id-evil") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +// XSW #3 is the first example of an XSW that wraps the Assertion element. +// It inserts the copied Assertion as the first child of the root Response element. +// The original Assertion is a sibling of the copied Assertion. +func TestXSW3AssertionWrap1(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, evilResponseDoc) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, evilResponseEl, decryptedAssertion) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + RemoveResponseSignature(evilResponseDoc) + + // We have to delete the signature of the evil assertion + RemoveAssertionSignature(evilResponseDoc) + evilResponseDoc.FindElement("//Assertion").RemoveAttr("ID") + evilResponseDoc.FindElement("//Assertion").CreateAttr("ID", "id-evil") + + evilResponseDoc.FindElement("//Response").AddChild(originalResponseDoc.FindElement("//Assertion")) + + // Change one attribute + evilResponseDoc.FindElement("//Response/Assertion/AttributeStatement/Attribute/AttributeValue").SetText("evil_alice@example.com") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // Get all identities + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + + // We have to check that there is either an error or an identity created without the modified attribute + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) +} + +// XSW #4 is similar to #3, except in this case the original Assertion becomes a child of the copied Assertion. +func TestXSW4AssertionWrap2(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, evilResponseDoc) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, evilResponseEl, decryptedAssertion) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + RemoveResponseSignature(evilResponseDoc) + + // We have to delete the signature of the evil assertion + RemoveAssertionSignature(evilResponseDoc) + evilResponseDoc.FindElement("//Assertion").RemoveAttr("ID") + evilResponseDoc.FindElement("//Assertion").CreateAttr("ID", "id-evil") + + evilResponseDoc.FindElement("//Assertion").AddChild(originalResponseDoc.FindElement("//Assertion")) + + // Change the username + evilResponseDoc.FindElement("//Response/Assertion/AttributeStatement/Attribute/AttributeValue").SetText("evil_alice@example.com") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // Get all identities + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + + // We have to check that there is either an error or an identity created without the modified attribute + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) +} + +// XSW #5 is the first instance of Assertion wrapping we see where the Signature and the original Assertion aren’t in one of the three standard configurations (enveloped/enveloping/detached). +// In this case, the copied Assertion envelopes the Signature. +func TestXSW5AssertionWrap3(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, evilResponseDoc) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, evilResponseEl, decryptedAssertion) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + RemoveResponseSignature(evilResponseDoc) + + evilResponseDoc.FindElement("//Assertion").RemoveAttr("ID") + evilResponseDoc.FindElement("//Assertion").CreateAttr("ID", "id-evil") + + RemoveAssertionSignature(originalResponseDoc) + evilResponseDoc.FindElement("//Response").AddChild(originalResponseDoc.FindElement("//Assertion")) + + // Change the username + evilResponseDoc.FindElement("//Response/Assertion/AttributeStatement/Attribute/AttributeValue").SetText("evil_alice@example.com") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // Get all identities + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + + // We have to check that there is either an error or an identity created without the modified attribute + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) +} + +// XSW #6 inserts its copied Assertion into the same location as #’s 4 and 5. +// The interesting piece here is that the copied Assertion envelopes the Signature, which in turn envelopes the original Assertion. +func TestXSW6AssertionWrap4(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, evilResponseDoc) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, evilResponseEl, decryptedAssertion) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + RemoveResponseSignature(evilResponseDoc) + + evilResponseDoc.FindElement("//Assertion").RemoveAttr("ID") + evilResponseDoc.FindElement("//Assertion").CreateAttr("ID", "id-evil") + + RemoveAssertionSignature(originalResponseDoc) + evilResponseDoc.FindElement("//Assertion").FindElement("//Signature").AddChild(originalResponseDoc.FindElement("//Assertion")) + + // Change the username + evilResponseDoc.FindElement("//Response/Assertion/AttributeStatement/Attribute/AttributeValue").SetText("evil_alice@example.com") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // Get all identities + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + + // We have to check that there is either an error or an identity created without the modified attribute + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) +} + +// XSW #7 inserts an Extensions element and adds the copied Assertion as a child. Extensions is a valid XML element with a less restrictive schema definition. +func TestXSW7AssertionWrap5(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, evilResponseDoc) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, evilResponseEl, decryptedAssertion) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + RemoveResponseSignature(evilResponseDoc) + + // We have to delete the signature of the evil assertion + RemoveAssertionSignature(evilResponseDoc) + + evilResponseDoc.FindElement("//Response").AddChild(etree.NewElement("Extension")) + evilResponseDoc.FindElement("//Response").FindElement("//Extension").AddChild(evilResponseDoc.FindElement("//Assertion")) + evilResponseDoc.FindElement("//Response").AddChild(originalResponseDoc.FindElement("//Assertion")) + + // Change the username + evilResponseDoc.FindElement("//Response/Extension/Assertion/AttributeStatement/Attribute/AttributeValue").SetText("evil_alice@example.com") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // Get all identities + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + + // We have to check that there is either an error or an identity created without the modified attribute + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) +} + +// XSW #8 uses another less restrictive XML element to perform a variation of the attack pattern used in XSW #7. +// This time around the original Assertion is the child of the less restrictive element instead of the copied Assertion. +func TestXSW8AssertionWrap6(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + evilResponseEl := authnRequest.ResponseEl + evilResponseDoc := etree.NewDocument() + evilResponseDoc.SetRoot(evilResponseEl) + + // Get and Decrypt SAML Assertion + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, evilResponseDoc) + + // Replace the SAML crypted Assertion in the SAML Response by SAML decrypted Assertion + ReplaceResponseAssertion(t, evilResponseEl, decryptedAssertion) + + // Copy the Response Element + // This copy will not be changed and contain the original Response content + originalResponseEl := evilResponseEl.Copy() + originalResponseDoc := etree.NewDocument() + originalResponseDoc.SetRoot(originalResponseEl) + + RemoveResponseSignature(evilResponseDoc) + + RemoveAssertionSignature(originalResponseDoc) + evilResponseDoc.FindElement("//Response/Assertion/Signature").AddChild(etree.NewElement("Object")) + evilResponseDoc.FindElement("//Assertion/Signature/Object").AddChild(originalResponseDoc.FindElement("//Assertion")) + + // Change the username + evilResponseDoc.FindElement("//Response/Assertion/AttributeStatement/Attribute/AttributeValue").SetText("evil_alice@example.com") + + // Get Reponse string + responseStr, err := evilResponseDoc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + // Get all identities + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + + // We have to check that there is either an error or an identity created without the modified attribute + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) +} + +// If the response was meant for a different Service Provider, the current Service Provider should notice it and reject the authentication +func TestTokenRecipientConfusion(t *testing.T) { + + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Change the ACS Endpoint location in order to change the recipient in the SAML Assertion + authnRequest.ACSEndpoint.Location = "https://test.com" + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) + +} + +func TestXMLExternalEntity(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Payload XEE + xee := "]>&xxe;" + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, xee+responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestExtensibleStylesheetLanguageTransformation(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Payload XSLT + xslt := "" + xsltDoc := etree.NewDocument() + xsltDoc.ReadFromString(xslt) + xsltElement := xsltDoc.SelectElement("stylesheet") + doc.FindElement("//Transforms").AddChild(xsltElement) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestExpiredSAMLResponse(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // The answer was forged on January 1 and therefore we set the current date to January 2 so that it is expired + TimeNow = func() time.Time { + rv, _ := time.Parse("Mon Jan 2 15:04:05.999999999 MST 2006", "Wed Jan 2 01:57:09.123456789 UTC 2014") + return rv + } + + saml.TimeNow = TimeNow + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestSignSAMLAssertionWithOwnKeypair(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get and Decrypt SAML Assertion in order to encrypt it afterwards + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Sign the SAML assertion with an evil key pair + keyPair, err := tls.LoadX509KeyPair("./testdata/evilcert.crt", "./testdata/evilkey.key") + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + keyStore := dsig.TLSCertKeyStore(keyPair) + + signingContext := dsig.NewDefaultSigningContext(keyStore) + signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("") + signingContext.SetSignatureMethod(dsig.RSASHA256SignatureMethod) + + signedAssertionEl, err := signingContext.SignEnveloped(decryptedAssertion) + + // Replace the SAML crypted Assertion in the SAML Response by the assertion signed by our keys + ReplaceResponseAssertion(t, responseEl, signedAssertionEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestSignSAMLResponseWithOwnKeypair(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + + // Sign the SAML response with an evil key pair + keyPair, err := tls.LoadX509KeyPair("./testdata/evilcert.crt", "./testdata/evilkey.key") + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + keyStore := dsig.TLSCertKeyStore(keyPair) + + signingContext := dsig.NewDefaultSigningContext(keyStore) + signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("") + signingContext.SetSignatureMethod(dsig.RSASHA256SignatureMethod) + + // Sign the whole response + signedResponseEl, err := signingContext.SignEnveloped(responseEl) + doc.SetRoot(signedResponseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +func TestSignBothResponseAndAssertionWithOwnKeypair(t *testing.T) { + // Create the SP, the IdP and the AnthnRequest + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get and Decrypt SAML Assertion in order to encrypt it afterwards + decryptedAssertion := GetAndDecryptAssertionEl(t, testMiddleware, doc) + + // Sign the SAML assertion with an evil key pair + keyPair, err := tls.LoadX509KeyPair("./testdata/evilcert.crt", "./testdata/evilkey.key") + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + keyStore := dsig.TLSCertKeyStore(keyPair) + + signingContext := dsig.NewDefaultSigningContext(keyStore) + signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("") + signingContext.SetSignatureMethod(dsig.RSASHA256SignatureMethod) + + signedAssertionEl, err := signingContext.SignEnveloped(decryptedAssertion) + + // Replace the SAML crypted Assertion in the SAML Response by the assertion signed by our keys + ReplaceResponseAssertion(t, responseEl, signedAssertionEl) + + // Sign the whole response with own keys pairs + signedResponseEl, err := signingContext.SignEnveloped(responseEl) + doc.SetRoot(signedResponseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + + // Send the SAML Response to the SP ACS + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request to Kratos + strategy.HandleCallback(resp, req, ps) + + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} + +// Check if it is possible to send the same SAML Response twice (Replay Attack) +func TestReplayAttack(t *testing.T) { + + testMiddleware, strategy, _, authnRequest, authnRequestID := prepareTestEnvironment(t) + + // Generate the SAML Assertion and the SAML Response + authnRequest = PrepareTestSAMLResponse(t, testMiddleware, authnRequest, authnRequestID) + + // Get Response Element + responseEl := authnRequest.ResponseEl + doc := etree.NewDocument() + doc.SetRoot(responseEl) + + // Get Reponse string + responseStr, err := doc.WriteToString() + assert.NilError(t, err) + + req := PrepareTestSAMLResponseHTTPRequest(t, testMiddleware, authnRequest, authnRequestID, responseStr) + resp := httptest.NewRecorder() + + // Start the continuity + startContinuity(resp, req, strategy) + + // We make sure that continuity is respected + ps := initRouterParams() + + // We send the request once to Kratos, everything is in order so there should be no error. + strategy.HandleCallback(resp, req, ps) + assert.Check(t, !strings.Contains(resp.HeaderMap["Location"][0], "error")) + + // We send the same request a second time to Kratos, it has already been received by Kratos so there must be an error + strategy.HandleCallback(resp, req, ps) + assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error")) +} From 22d6994283ca5cda6f80a0ce548f7f741579271b Mon Sep 17 00:00:00 2001 From: sebferrer Date: Wed, 15 Feb 2023 10:45:04 +0000 Subject: [PATCH 6/8] feat(saml): resolving conflicts with master Signed-off-by: sebferrer Co-authored-by: ThibaultHerard --- continuity/manager_relaystate.go | 10 ++++----- go.mod | 6 ++--- go.sum | 22 ++++++++++--------- selfservice/strategy/saml/strategy.go | 9 +++++++- selfservice/strategy/saml/strategy_login.go | 5 +++-- .../strategy/saml/vulnerabilities_test.go | 18 +++++++-------- 6 files changed, 38 insertions(+), 32 deletions(-) diff --git a/continuity/manager_relaystate.go b/continuity/manager_relaystate.go index 5ce1dc7ffee7..264a54e17064 100644 --- a/continuity/manager_relaystate.go +++ b/continuity/manager_relaystate.go @@ -49,18 +49,16 @@ func (m *ManagerRelayState) Pause(ctx context.Context, w http.ResponseWriter, r } c := NewContainer(name, *o) - // We have to put the continuity value in the cookie to ensure that value are passed between API and UI - // It is also useful to pass the value between SP and IDP with POST method because RelayState will take its value from cookie + if err := m.dr.ContinuityPersister().SaveContinuitySession(r.Context(), c); err != nil { + return errors.WithStack(err) + } + if err = x.SessionPersistValues(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, map[string]interface{}{ name: c.ID.String(), }); err != nil { return err } - if err := m.dr.ContinuityPersister().SaveContinuitySession(r.Context(), c); err != nil { - return errors.WithStack(err) - } - return nil } diff --git a/go.mod b/go.mod index fce674c9b2ce..5df409bc1e06 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,6 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/imdario/mergo v0.3.13 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf - github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65 github.com/jarcoal/httpmock v1.0.5 github.com/jteeuwen/go-bindata v3.0.7+incompatible github.com/julienschmidt/httprouter v1.3.0 @@ -85,6 +84,7 @@ require ( github.com/pquerna/otp v1.4.0 github.com/rs/cors v1.8.2 github.com/russellhaering/goxmldsig v1.1.1 + github.com/samber/lo v1.37.0 github.com/sirupsen/logrus v1.9.0 github.com/slack-go/slack v0.7.4 github.com/spf13/cobra v1.6.1 @@ -103,7 +103,7 @@ require ( golang.org/x/oauth2 v0.4.0 golang.org/x/sync v0.1.0 golang.org/x/tools v0.2.0 - google.golang.org/grpc v1.50.1 + google.golang.org/grpc v1.52.0 gotest.tools v2.2.0+incompatible ) @@ -328,7 +328,7 @@ require ( golang.org/x/time v0.1.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 // indirect + google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index 427d9b4cccdb..0fabab367e28 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -505,8 +507,8 @@ github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/V github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= github.com/gobuffalo/plush/v4 v4.1.18 h1:bnPjdMTEUQHqj9TNX2Ck3mxEXYZa+0nrFMNM07kpX9g= github.com/gobuffalo/plush/v4 v4.1.18/go.mod h1:xi2tJIhFI4UdzIL8sxZtzGYOd2xbBpcFbLZlIPGGZhU= -github.com/gobuffalo/pop/v6 v6.1.2-0.20230124165254-ec9229dbf7d7 h1:lwf/5cRw46IrLrhZnCg8J9NKgskkwMPuVvEOc2Wy72I= -github.com/gobuffalo/pop/v6 v6.1.2-0.20230124165254-ec9229dbf7d7/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI= +github.com/gobuffalo/pop/v6 v6.0.8 h1:9+5ShHYh3x9NDFCITfm/gtKDDRSgOwiY7kA0Hf7N9aQ= +github.com/gobuffalo/pop/v6 v6.0.8/go.mod h1:f4JQ4Zvkffcevz+t+XAwBLStD7IQs19DiIGIDFYw1eA= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= @@ -521,8 +523,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= -github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= +github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -598,6 +600,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v27 v27.0.1 h1:sSMFSShNn4VnqCqs+qhab6TS3uQc+uVR6TD1bW6MavM= @@ -785,8 +788,6 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= -github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65 h1:T25FL3WEzgmKB0m6XCJNZ65nw09/QIp3T1yXr487D+A= -github.com/instana/testify v1.6.2-0.20200721153833-94b1851f4d65/go.mod h1:nYhEREG/B7HUY7P+LKOrqy53TpIqmJ9JyUShcaEKtGw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -1119,8 +1120,8 @@ github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OU github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= -github.com/ory/x v0.0.519 h1:T8/LbbQQqm+3P7bfI838T7eECv6+laXlvIyCp0QB+R8= -github.com/ory/x v0.0.519/go.mod h1:xUtRpoiRARyJNPVk/fcCNKzyp25Foxt9GPlj8pd7egY= +github.com/ory/x v0.0.534 h1:hc49pmcOuHdJ6rbHVGtJJ4/LU88dzDCtEQKfgeo/ecU= +github.com/ory/x v0.0.534/go.mod h1:CQopDsCC9t0tQsddE9UlyRFVEFd2xjKBVcw4nLMMMS0= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -1320,6 +1321,7 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -1950,8 +1952,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= -golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/selfservice/strategy/saml/strategy.go b/selfservice/strategy/saml/strategy.go index fec366df3359..d813bb29bbd0 100644 --- a/selfservice/strategy/saml/strategy.go +++ b/selfservice/strategy/saml/strategy.go @@ -3,7 +3,9 @@ package saml import ( "bytes" "context" + "encoding/base64" "encoding/json" + "fmt" "net/http" "strings" @@ -124,6 +126,11 @@ type authCodeContainer struct { Traits json.RawMessage `json:"traits"` } +func generateState(flowID string) string { + state := x.NewUUID().String() + return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", flowID, state))) +} + func NewStrategy(d registrationStrategyDependencies) *Strategy { return &Strategy{ d: d, @@ -178,7 +185,7 @@ func (s *Strategy) GetAttributesFromAssertion(assertion *saml.Assertion) (map[st } func (s *Strategy) validateFlow(ctx context.Context, r *http.Request, rid uuid.UUID) (flow.Flow, error) { - if x.IsZeroUUID(rid) { + if rid.IsNil() { return nil, errors.WithStack(herodot.ErrBadRequest.WithReason("The session cookie contains invalid values and the flow could not be executed. Please try again.")) } diff --git a/selfservice/strategy/saml/strategy_login.go b/selfservice/strategy/saml/strategy_login.go index b31e6332b0cb..67faa6682b25 100644 --- a/selfservice/strategy/saml/strategy_login.go +++ b/selfservice/strategy/saml/strategy_login.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/gofrs/uuid" "github.com/google/go-jsonnet" "github.com/pkg/errors" "github.com/tidwall/gjson" @@ -74,7 +75,7 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login return nil, nil } -func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, ss *session.Session) (i *identity.Identity, err error) { +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, identityID uuid.UUID) (i *identity.Identity, err error) { if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { return nil, err } @@ -102,7 +103,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return } - state := x.NewUUID().String() + state := generateState(f.ID.String()) if err := s.d.RelayStateContinuityManager().Pause(r.Context(), w, r, sessionName, continuity.WithPayload(&authCodeContainer{ State: state, diff --git a/selfservice/strategy/saml/vulnerabilities_test.go b/selfservice/strategy/saml/vulnerabilities_test.go index fe8f05c447a4..7c0095aef5a8 100644 --- a/selfservice/strategy/saml/vulnerabilities_test.go +++ b/selfservice/strategy/saml/vulnerabilities_test.go @@ -12,6 +12,7 @@ import ( "github.com/beevik/etree" "github.com/crewjam/saml" + "github.com/ory/kratos/identity" dsig "github.com/russellhaering/goxmldsig" "gotest.tools/assert" @@ -56,9 +57,6 @@ func TestHappyPath(t *testing.T) { // We send the request to Kratos strategy.HandleCallback(resp, req, ps) - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) - _ = ids - // This is the Happy Path, the HTTP response code should be 302 (Found status) assert.Check(t, !strings.Contains(resp.HeaderMap["Location"][0], "error")) } @@ -498,7 +496,7 @@ func TestAddXMLCommentsInSAMLAttributes(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) traitsMap := make(map[string]interface{}) json.Unmarshal(ids[0].Traits, &traitsMap) @@ -682,7 +680,7 @@ func TestXSW3AssertionWrap1(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -744,7 +742,7 @@ func TestXSW4AssertionWrap2(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -806,7 +804,7 @@ func TestXSW5AssertionWrap3(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -868,7 +866,7 @@ func TestXSW6AssertionWrap4(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -930,7 +928,7 @@ func TestXSW7AssertionWrap5(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -990,7 +988,7 @@ func TestXSW8AssertionWrap6(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) From 576df75b3eb28ba7fe40603f3b5d65759d1a8f6f Mon Sep 17 00:00:00 2001 From: ThibaultHerard Date: Thu, 16 Feb 2023 15:58:28 +0000 Subject: [PATCH 7/8] feat(saml): update attributes mapping + remove slo Signed-off-by: ThibaultHerard Co-authored-by: sebferrer --- embedx/config.schema.json | 15 +-- selfservice/strategy/saml/config_test.go | 7 -- selfservice/strategy/saml/handler.go | 26 +---- selfservice/strategy/saml/handler_test.go | 6 +- selfservice/strategy/saml/strategy.go | 1 + .../strategy/saml/strategy_helper_test.go | 1 - .../strategy/saml/strategy_registration.go | 105 +++++++----------- selfservice/strategy/saml/strategy_test.go | 20 ---- 8 files changed, 52 insertions(+), 129 deletions(-) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 1e5113d77c3b..3a68c0ba4ce4 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -446,14 +446,6 @@ "https://foo.bar.com/path/to/certificate" ] }, - "idp_logout_url": { - "title": "IDP Logout URL", - "description": "The URL of the Single Log Out (SLO) API of the IDP", - "type": "string", - "examples": [ - "https://path/to/logout" - ] - }, "idp_sso_url": { "title": "IDP SSO URL", "description": "The URL of the SSO Handler at the IDP", @@ -482,9 +474,9 @@ }, "then": { "required": [ - "idp_logout_url", "idp_certificate_path", - "idp_entity_id" + "idp_entity_id", + "idp_sso_url" ] }, "else":{ @@ -492,9 +484,6 @@ "idp_certificate_path": { "const": {} }, - "idp_logout_url": { - "const": {} - }, "idp_entity_id":{ "const":{} }, diff --git a/selfservice/strategy/saml/config_test.go b/selfservice/strategy/saml/config_test.go index 65b394949506..0b509776c026 100644 --- a/selfservice/strategy/saml/config_test.go +++ b/selfservice/strategy/saml/config_test.go @@ -34,7 +34,6 @@ func TestInitSAMLWithoutProvider(t *testing.T) { idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" - idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" // Initiates without service provider ViperSetProviderConfig( @@ -75,7 +74,6 @@ func TestInitSAMLWithoutPoviderID(t *testing.T) { idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" - idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" // Initiates the service provider ViperSetProviderConfig( @@ -125,7 +123,6 @@ func TestInitSAMLWithoutPoviderLabel(t *testing.T) { idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" - idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" // Initiates the service provider ViperSetProviderConfig( @@ -174,7 +171,6 @@ func TestAttributesMapWithoutID(t *testing.T) { idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" - idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" // Initiates the service provider ViperSetProviderConfig( @@ -226,7 +222,6 @@ func TestAttributesMapWithAnExtraField(t *testing.T) { idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" idpInformation["idp_certificate_path"] = "file://testdata/idp_cert.pem" - idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" // Initiates the service provider ViperSetProviderConfig( @@ -319,7 +314,6 @@ func TestInitSAMLWithMissingIDPInformationField(t *testing.T) { idpInformation := make(map[string]string) idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" - idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" // Initiates the service provider ViperSetProviderConfig( @@ -369,7 +363,6 @@ func TestInitSAMLWithExtraIDPInformationField(t *testing.T) { idpInformation["idp_sso_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["idp_entity_id"] = "https://samltest.id/saml/idp" idpInformation["idp_certificate_path"] = "file://testdata/samlkratos.crt" - idpInformation["idp_logout_url"] = "https://samltest.id/idp/profile/SAML2/Redirect/SSO" idpInformation["evil"] = "evil" // Initiates the service provider diff --git a/selfservice/strategy/saml/handler.go b/selfservice/strategy/saml/handler.go index eacb5e742c3f..2e09da96130f 100644 --- a/selfservice/strategy/saml/handler.go +++ b/selfservice/strategy/saml/handler.go @@ -142,13 +142,13 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi } // Key pair to encrypt and sign SAML requests - keyPair, err := tls.LoadX509KeyPair(strings.Replace(providerConfig.PublicCertPath, "file://", "", 1), strings.Replace(providerConfig.PrivateKeyPath, "file://", "", 1)) // TODO : Fetcher + keyPair, err := tls.LoadX509KeyPair(strings.Replace(providerConfig.PublicCertPath, "file://", "", 1), strings.Replace(providerConfig.PrivateKeyPath, "file://", "", 1)) if err != nil { - return herodot.ErrNotFound.WithTrace(err) // TODO : Replace with File not found error + return herodot.ErrInternalServerError.WithReason("An error occurred while retrieving the key pair used by SAML") } keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { - return herodot.ErrNotFound.WithTrace(err) + return herodot.ErrInternalServerError.WithReason("An error occurred while using the certificate associated with SAML") } var idpMetadata *samlidp.EntityDescriptor @@ -187,12 +187,6 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi return herodot.ErrNotFound.WithTrace(err) } - // The IDP Logout URL - IDPlogoutURL, err := url.Parse(providerConfig.IDPInformation["idp_logout_url"]) - if err != nil { - return herodot.ErrNotFound.WithTrace(err) - } - // The certificate of the IDP certificateBuffer, err := fetcher.NewFetcher().Fetch(providerConfig.IDPInformation["idp_certificate_path"]) if err != nil { @@ -212,12 +206,9 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // Because the metadata file is not provided, we need to simulate an IDP to create artificial metadata from the data entered in the conf file tempIDP := samlidp.IdentityProvider{ - Key: nil, Certificate: IDPCertificate, - Logger: nil, MetadataURL: *entityIDURL, SSOURL: *IDPSSOURL, - LogoutURL: *IDPlogoutURL, } // Now we assign our reconstructed metadata to our SP @@ -282,7 +273,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi } // Crewjam library use default route for ACS and metadata but we want to overwrite them - metadata, err := url.Parse(publicUrlString + RouteMetadata) + metadata, err := url.Parse(publicUrlString + strings.Replace(RouteMetadata, ":provider", providerConfig.ID, 1)) if err != nil { return herodot.ErrNotFound.WithTrace(err) } @@ -302,7 +293,7 @@ func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Confi // Return the singleton MiddleWare func GetMiddleware(pid string) (*samlsp.Middleware, error) { if samlMiddlewares[pid] == nil { - return nil, errors.Errorf("An error occurred while retrieving the middeware, it is null") // TODO : Improve error message + return nil, errors.Errorf("An error occurred during the connection with SAML.") } return samlMiddlewares[pid], nil } @@ -342,17 +333,12 @@ func CreateSAMLProviderConfig(config config.Config, ctx context.Context, pid str return nil, ErrInvalidSAMLConfiguration.WithReasonf("Please include your Identity Provider information in the configuration file.").WithTrace(err) } - /** - * SAMLTODO errors - */ - // _, sso_exists := providerConfig.IDPInformation["idp_sso_url"] _, sso_exists := providerConfig.IDPInformation["idp_sso_url"] _, entity_id_exists := providerConfig.IDPInformation["idp_entity_id"] _, certificate_exists := providerConfig.IDPInformation["idp_certificate_path"] - _, logout_url_exists := providerConfig.IDPInformation["idp_logout_url"] _, metadata_exists := providerConfig.IDPInformation["idp_metadata_url"] - if (!metadata_exists && (!sso_exists || !entity_id_exists || !certificate_exists || !logout_url_exists)) || len(providerConfig.IDPInformation) > 4 { + if (!metadata_exists && (!sso_exists || !entity_id_exists || !certificate_exists)) || len(providerConfig.IDPInformation) > 3 { return nil, ErrInvalidSAMLConfiguration.WithReason("Please check your IDP information in the configuration file").WithTrace(err) } diff --git a/selfservice/strategy/saml/handler_test.go b/selfservice/strategy/saml/handler_test.go index cd8b40626438..0f2de69cfc1c 100644 --- a/selfservice/strategy/saml/handler_test.go +++ b/selfservice/strategy/saml/handler_test.go @@ -24,7 +24,7 @@ func TestInitMiddleWareWithMetadata(t *testing.T) { require.NoError(t, err) assert.Check(t, middleWare != nil) assert.Check(t, middleWare.ServiceProvider.IDPMetadata != nil) - assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/:provider") + assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/samlProvider") assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://idp.testshib.org/idp/shibboleth") } @@ -44,7 +44,7 @@ func TestInitMiddleWareWithoutMetadata(t *testing.T) { require.NoError(t, err) assert.Check(t, middleWare != nil) assert.Check(t, middleWare.ServiceProvider.IDPMetadata != nil) - assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/:provider") + assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/samlProvider") assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://samltest.id/saml/idp") } @@ -63,7 +63,7 @@ func TestGetMiddleware(t *testing.T) { require.NoError(t, err) assert.Check(t, middleWare != nil) assert.Check(t, middleWare.ServiceProvider.IDPMetadata != nil) - assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/:provider") + assert.Check(t, middleWare.ServiceProvider.MetadataURL.Path == "/self-service/methods/saml/metadata/samlProvider") assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://idp.testshib.org/idp/shibboleth") } diff --git a/selfservice/strategy/saml/strategy.go b/selfservice/strategy/saml/strategy.go index d813bb29bbd0..4e2c69b91653 100644 --- a/selfservice/strategy/saml/strategy.go +++ b/selfservice/strategy/saml/strategy.go @@ -61,6 +61,7 @@ type registrationStrategyDependencies interface { x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider + x.HTTPClientProvider config.Provider diff --git a/selfservice/strategy/saml/strategy_helper_test.go b/selfservice/strategy/saml/strategy_helper_test.go index bb55a8f8fccb..44a53861d3af 100644 --- a/selfservice/strategy/saml/strategy_helper_test.go +++ b/selfservice/strategy/saml/strategy_helper_test.go @@ -156,7 +156,6 @@ func InitTestMiddlewareWithoutMetadata(t *testing.T, idpSsoUrl string, idpEntity idpInformation["idp_sso_url"] = idpSsoUrl idpInformation["idp_entity_id"] = idpEntityId idpInformation["idp_certificate_path"] = idpCertifiatePath - idpInformation["idp_logout_url"] = idpLogoutUrl return InitTestMiddleware(t, idpInformation) } diff --git a/selfservice/strategy/saml/strategy_registration.go b/selfservice/strategy/saml/strategy_registration.go index e0422ba4c48f..d536be07d629 100644 --- a/selfservice/strategy/saml/strategy_registration.go +++ b/selfservice/strategy/saml/strategy_registration.go @@ -2,14 +2,11 @@ package saml import ( "bytes" - "context" "encoding/json" "net/http" - "github.com/google/go-jsonnet" "github.com/pkg/errors" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/x/decoderx" @@ -17,7 +14,6 @@ import ( "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/text" - "github.com/tidwall/gjson" "github.com/tidwall/sjson" "github.com/ory/kratos/x" @@ -31,87 +27,66 @@ func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) { s.setRoutes(r) } -func (s *Strategy) GetRegistrationIdentity(r *http.Request, ctx context.Context, provider Provider, claims *Claims, logsEnabled bool) (*identity.Identity, error) { - // Fetch fetches the file contents from the mapper file. - jn, err := s.f.Fetch(provider.Config().Mapper) - if err != nil { - return nil, err - } - +func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider) (*identity.Identity, error) { var jsonClaims bytes.Buffer if err := json.NewEncoder(&jsonClaims).Encode(claims); err != nil { - return nil, err + return nil, s.handleError(w, r, a, provider.Config().ID, nil, err) } - // Identity Creation - i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i := identity.NewIdentity(s.d.Config().DefaultIdentityTraitsSchemaID(r.Context())) + if err := s.setTraits(w, r, a, claims, provider, jsonClaims, i); err != nil { + return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + } - vm := jsonnet.MakeVM() - vm.ExtCode("claims", jsonClaims.String()) - evaluated, err := vm.EvaluateAnonymousSnippet(provider.Config().Mapper, jn.String()) + s.d.Logger(). + WithRequest(r). + WithField("saml_provider", provider.Config().ID). + WithSensitiveField("saml_claims", claims). + Debug("SAML Connect completed.") + return i, nil +} + +func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, jsonClaims bytes.Buffer, i *identity.Identity) error { + + traitsMap := make(map[string]interface{}) + json.Unmarshal(jsonClaims.Bytes(), &traitsMap) + delete(traitsMap, "iss") + delete(traitsMap, "email_verified") + delete(traitsMap, "sub") + traits, err := json.Marshal(traitsMap) if err != nil { - return nil, err - } else if traits := gjson.Get(evaluated, "identity.traits"); !traits.IsObject() { - i.Traits = []byte{'{', '}'} - if logsEnabled { - s.d.Logger(). - WithRequest(r). - WithField("Provider", provider.Config().ID). - WithSensitiveField("saml_claims", claims). - WithField("mapper_jsonnet_output", evaluated). - WithField("mapper_jsonnet_url", provider.Config().Mapper). - Error("SAML Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!") - } - } else { - i.Traits = []byte(traits.Raw) + return s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } + i.Traits = identity.Traits(traits) + + s.d.Logger(). + WithRequest(r). + WithField("oidc_provider", provider.Config().ID). + WithSensitiveField("identity_traits", i.Traits). + WithField("mapper_jsonnet_url", provider.Config().Mapper). + Debug("Merged form values and OpenID Connect Jsonnet output.") + return nil +} - if logsEnabled { - s.d.Logger(). - WithRequest(r). - WithField("saml_provider", provider.Config().ID). - WithSensitiveField("saml_claims", claims). - WithSensitiveField("mapper_jsonnet_output", evaluated). - WithField("mapper_jsonnet_url", provider.Config().Mapper). - Debug("SAML Jsonnet mapper completed.") - - s.d.Logger(). - WithRequest(r). - WithField("saml_provider", provider.Config().ID). - WithSensitiveField("identity_traits", i.Traits). - WithSensitiveField("mapper_jsonnet_output", evaluated). - WithField("mapper_jsonnet_url", provider.Config().Mapper). - Debug("Merged form values and SAML Jsonnet output.") +func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a *registration.Flow, provider Provider, claims *Claims) error { + i, err := s.createIdentity(w, r, a, claims, provider) + if err != nil { + return s.handleError(w, r, a, provider.Config().ID, nil, err) } // Verify the identity - if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { - return i, err + if err := s.d.IdentityValidator().Validate(r.Context(), i); err != nil { + return s.handleError(w, r, a, provider.Config().ID, nil, err) } // Create new uniq credentials identifier for user is database creds, err := identity.NewCredentialsSAML(claims.Subject, provider.Config().ID) if err != nil { - return i, err + return s.handleError(w, r, a, provider.Config().ID, nil, err) } // Set the identifiers to the identity i.SetCredentials(s.ID(), *creds) - - return i, nil -} - -func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a *registration.Flow, provider Provider, claims *Claims) error { - - i, err := s.GetRegistrationIdentity(r, r.Context(), provider, claims, true) - if err != nil { - if i == nil { - return s.handleError(w, r, a, provider.Config().ID, nil, err) - } else { - return s.handleError(w, r, a, provider.Config().ID, i.Traits, err) - } - } - if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeSAML, a, i); err != nil { return s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } diff --git a/selfservice/strategy/saml/strategy_test.go b/selfservice/strategy/saml/strategy_test.go index c594d1eb0f09..8717e414398c 100644 --- a/selfservice/strategy/saml/strategy_test.go +++ b/selfservice/strategy/saml/strategy_test.go @@ -171,26 +171,6 @@ func TestCountActiveCredentials(t *testing.T) { gotest.Check(t, count == 1) } -func TestGetRegistrationIdentity(t *testing.T) { - if testing.Short() { - t.Skip() - } - - saml.DestroyMiddlewareIfExists("samlProvider") - - middleware, strategy, _, _ := InitTestMiddlewareWithMetadata(t, - "file://testdata/SP_IDPMetadata.xml") - - provider, _ := strategy.Provider(context.Background(), "samlProvider") - assertion, _ := GetAndDecryptAssertion(t, "./testdata/SP_SamlResponse.xml", middleware.ServiceProvider.Key) - attributes, _ := strategy.GetAttributesFromAssertion(assertion) - claims, _ := provider.Claims(context.Background(), strategy.D().Config(), attributes, "samlProvider") - - i, err := strategy.GetRegistrationIdentity(nil, context.Background(), provider, claims, false) - require.NoError(t, err) - gotest.Check(t, i != nil) -} - func TestCountActiveFirstFactorCredentials(t *testing.T) { _, reg := internal.NewFastRegistryWithMocks(t) strategy := saml.NewStrategy(reg) From 7a827c66db35bc09e601e220bc26ee7e937ce9ce Mon Sep 17 00:00:00 2001 From: sebferrer Date: Wed, 22 Feb 2023 16:55:49 +0000 Subject: [PATCH 8/8] feat(saml): continuity manager relaystate refactoring + test new credentials saml + resolving conflicts with master Signed-off-by: sebferrer Co-authored-by: ThibaultHerard --- continuity/manager.go | 24 +-- continuity/manager_cookie.go | 29 +++- continuity/manager_relaystate.go | 149 ------------------ continuity/manager_relaystate_test.go | 19 ++- driver/registry_default.go | 18 +-- go.sum | 4 +- identity/credentials_saml.go | 9 +- identity/credentials_saml_test.go | 18 +++ selfservice/strategy/saml/config_test.go | 24 +-- selfservice/strategy/saml/provider_config.go | 2 +- selfservice/strategy/saml/provider_saml.go | 4 +- selfservice/strategy/saml/strategy.go | 109 ++++++------- selfservice/strategy/saml/strategy_login.go | 29 +--- .../strategy/saml/strategy_registration.go | 8 +- .../strategy/saml/testdata/sp2_cert.pem | 32 ---- .../strategy/saml/testdata/sp2_key.pem | 52 ------ .../saml/vulnerabilities_helper_test.go | 4 +- .../strategy/saml/vulnerabilities_test.go | 14 +- x/provider.go | 5 - 19 files changed, 167 insertions(+), 386 deletions(-) delete mode 100644 continuity/manager_relaystate.go create mode 100644 identity/credentials_saml_test.go delete mode 100644 selfservice/strategy/saml/testdata/sp2_cert.pem delete mode 100644 selfservice/strategy/saml/testdata/sp2_key.pem diff --git a/continuity/manager.go b/continuity/manager.go index 27541f32b381..e2323bfb7f60 100644 --- a/continuity/manager.go +++ b/continuity/manager.go @@ -20,22 +20,19 @@ type ManagementProvider interface { ContinuityManager() Manager } -type ManagementProviderRelayState interface { - RelayStateContinuityManager() Manager -} - type Manager interface { Pause(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) error Continue(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) (*Container, error) - Abort(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) error + Abort(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) error } type managerOptions struct { - iid uuid.UUID - ttl time.Duration - payload json.RawMessage - payloadRaw interface{} - cleanUp bool + iid uuid.UUID + ttl time.Duration + payload json.RawMessage + payloadRaw interface{} + cleanUp bool + useRelayState bool } type ManagerOption func(*managerOptions) error @@ -87,3 +84,10 @@ func WithPayload(payload interface{}) ManagerOption { return nil } } + +func UseRelayState() ManagerOption { + return func(o *managerOptions) error { + o.useRelayState = true + return nil + } +} diff --git a/continuity/manager_cookie.go b/continuity/manager_cookie.go index 314c33da4a68..8e3d8f76b999 100644 --- a/continuity/manager_cookie.go +++ b/continuity/manager_cookie.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/gofrs/uuid" + "github.com/gorilla/sessions" "github.com/pkg/errors" "github.com/ory/herodot" @@ -64,12 +65,12 @@ func (m *ManagerCookie) Pause(ctx context.Context, w http.ResponseWriter, r *htt } func (m *ManagerCookie) Continue(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) (*Container, error) { - container, err := m.container(ctx, w, r, name) + o, err := newManagerOptions(opts) if err != nil { return nil, err } - o, err := newManagerOptions(opts) + container, err := m.container(ctx, w, r, name, o.useRelayState) if err != nil { return nil, err } @@ -95,9 +96,16 @@ func (m *ManagerCookie) Continue(ctx context.Context, w http.ResponseWriter, r * return container, nil } -func (m *ManagerCookie) sid(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) (uuid.UUID, error) { +func (m *ManagerCookie) sid(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, useRelayState bool) (uuid.UUID, error) { + var getStringFunction func(r *http.Request, s sessions.StoreExact, id string, key interface{}) (string, error) + if useRelayState { + getStringFunction = x.SessionGetStringRelayState + } else { + getStringFunction = x.SessionGetString + } + var sid uuid.UUID - if s, err := x.SessionGetString(r, m.d.ContinuityCookieManager(ctx), CookieName, name); err != nil { + if s, err := getStringFunction(r, m.d.ContinuityCookieManager(ctx), CookieName, name); err != nil { _ = x.SessionUnsetKey(w, r, m.d.ContinuityCookieManager(ctx), CookieName, name) return sid, errors.WithStack(ErrNotResumable.WithDebugf("%+v", err)) } else if sid = x.ParseUUID(s); sid == uuid.Nil { @@ -108,8 +116,8 @@ func (m *ManagerCookie) sid(ctx context.Context, w http.ResponseWriter, r *http. return sid, nil } -func (m *ManagerCookie) container(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) (*Container, error) { - sid, err := m.sid(ctx, w, r, name) +func (m *ManagerCookie) container(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, useRelayState bool) (*Container, error) { + sid, err := m.sid(ctx, w, r, name, useRelayState) if err != nil { return nil, err } @@ -129,8 +137,13 @@ func (m *ManagerCookie) container(ctx context.Context, w http.ResponseWriter, r return container, err } -func (m ManagerCookie) Abort(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) error { - sid, err := m.sid(ctx, w, r, name) +func (m ManagerCookie) Abort(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) error { + o, err := newManagerOptions(opts) + if err != nil { + return err + } + + sid, err := m.sid(ctx, w, r, name, o.useRelayState) if errors.Is(err, &ErrNotResumable) { // We do not care about an error here return nil diff --git a/continuity/manager_relaystate.go b/continuity/manager_relaystate.go deleted file mode 100644 index 264a54e17064..000000000000 --- a/continuity/manager_relaystate.go +++ /dev/null @@ -1,149 +0,0 @@ -package continuity - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - - "github.com/gofrs/uuid" - "github.com/pkg/errors" - - "github.com/ory/herodot" - "github.com/ory/x/sqlcon" - - "github.com/ory/kratos/session" - "github.com/ory/kratos/x" -) - -var _ Manager = new(ManagerRelayState) -var ErrNotResumableRelayState = *herodot.ErrBadRequest.WithError("no resumable session found").WithReason("The browser does not contain the necessary RelayState value to resume the session. This is a security violation and was blocked. Please try again!") - -type ( - managerRelayStateDependencies interface { - PersistenceProvider - x.RelayStateProvider - session.ManagementProvider - } - ManagerRelayState struct { - dr managerRelayStateDependencies - dc managerCookieDependencies - } -) - -// To ensure continuity even after redirection to the IDP, we cannot use cookies because the IDP and the SP are on two different domains. -// So we have to pass the continuity value through the relaystate. -// This value corresponds to the session ID -func NewManagerRelayState(dr managerRelayStateDependencies, dc managerCookieDependencies) *ManagerRelayState { - return &ManagerRelayState{dr: dr, dc: dc} -} - -func (m *ManagerRelayState) Pause(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) error { - if len(name) == 0 { - return errors.Errorf("continuity container name must be set") - } - - o, err := newManagerOptions(opts) - if err != nil { - return err - } - c := NewContainer(name, *o) - - if err := m.dr.ContinuityPersister().SaveContinuitySession(r.Context(), c); err != nil { - return errors.WithStack(err) - } - - if err = x.SessionPersistValues(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, map[string]interface{}{ - name: c.ID.String(), - }); err != nil { - return err - } - - return nil -} - -func (m *ManagerRelayState) Continue(ctx context.Context, w http.ResponseWriter, r *http.Request, name string, opts ...ManagerOption) (*Container, error) { - container, err := m.container(ctx, w, r, name) - if err != nil { - return nil, err - } - - o, err := newManagerOptions(opts) - if err != nil { - return nil, err - } - - if err := container.Valid(o.iid); err != nil { - return nil, err - } - - if o.payloadRaw != nil && container.Payload != nil { - if err := json.NewDecoder(bytes.NewBuffer(container.Payload)).Decode(o.payloadRaw); err != nil { - return nil, errors.WithStack(err) - } - } - - if err := x.SessionUnsetKey(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, name); err != nil { - return nil, err - } - - if err := m.dc.ContinuityPersister().DeleteContinuitySession(ctx, container.ID); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { - return nil, err - } - - return container, nil -} - -func (m *ManagerRelayState) sid(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) (uuid.UUID, error) { - var sid uuid.UUID - if s, err := x.SessionGetStringRelayState(r, m.dc.ContinuityCookieManager(ctx), CookieName, name); err != nil { - return sid, errors.WithStack(ErrNotResumable.WithDebugf("%+v", err)) - - } else if sid = x.ParseUUID(s); sid == uuid.Nil { - return sid, errors.WithStack(ErrNotResumable.WithDebug("session id is not a valid uuid")) - - } - - return sid, nil -} - -func (m *ManagerRelayState) container(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) (*Container, error) { - sid, err := m.sid(ctx, w, r, name) - if err != nil { - return nil, err - } - - container, err := m.dr.ContinuityPersister().GetContinuitySession(ctx, sid) - - if err != nil { - _ = x.SessionUnsetKey(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, name) - } - - if errors.Is(err, sqlcon.ErrNoRows) { - return nil, errors.WithStack(ErrNotResumable.WithDebug("Resumable ID from RelayState could not be found in the datastore")) - } else if err != nil { - return nil, err - } - - return container, err -} - -func (m ManagerRelayState) Abort(ctx context.Context, w http.ResponseWriter, r *http.Request, name string) error { - sid, err := m.sid(ctx, w, r, name) - if errors.Is(err, &ErrNotResumable) { - // We do not care about an error here - return nil - } else if err != nil { - return err - } - - if err := x.SessionUnsetKey(w, r, m.dc.ContinuityCookieManager(ctx), CookieName, name); err != nil { - return err - } - - if err := m.dr.ContinuityPersister().DeleteContinuitySession(ctx, sid); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { - return errors.WithStack(err) - } - - return nil -} diff --git a/continuity/manager_relaystate_test.go b/continuity/manager_relaystate_test.go index c37b4d5b8d45..d8bbfdbe703e 100644 --- a/continuity/manager_relaystate_test.go +++ b/continuity/manager_relaystate_test.go @@ -5,6 +5,7 @@ package continuity_test import ( "context" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -31,7 +32,7 @@ import ( "github.com/ory/kratos/x" ) -func TestManagerRelayState(t *testing.T) { +func TestManagerUseRelayState(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) @@ -57,6 +58,7 @@ func TestManagerRelayState(t *testing.T) { r.PostForm = make(url.Values) r.PostForm.Set("RelayState", relayState) + tc.wo = append(tc.wo, continuity.UseRelayState()) c, err := p.Continue(r.Context(), w, r, ps.ByName("name"), tc.wo...) if err != nil { writer.WriteError(w, r, err) @@ -71,7 +73,7 @@ func TestManagerRelayState(t *testing.T) { r.PostForm = make(url.Values) r.PostForm.Set("RelayState", relayState) - err := p.Abort(r.Context(), w, r, ps.ByName("name")) + err := p.Abort(r.Context(), w, r, ps.ByName("name"), continuity.UseRelayState()) if err != nil { writer.WriteError(w, r, err) return @@ -90,7 +92,7 @@ func TestManagerRelayState(t *testing.T) { return &http.Client{Jar: x.EasyCookieJar(t, nil)} } - p := reg.RelayStateContinuityManager() + p := reg.ContinuityManager() cl := newClient() t.Run("case=continue cookie persists with same http client", func(t *testing.T) { @@ -115,6 +117,7 @@ func TestManagerRelayState(t *testing.T) { t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) + assert.Equal(t, res.StatusCode, 200) require.Len(t, res.Cookies(), 1) assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName) }) @@ -147,7 +150,8 @@ func TestManagerRelayState(t *testing.T) { t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) - require.Len(t, res.Cookies(), 1) + assert.Equal(t, res.StatusCode, 200) + assert.Len(t, res.Cookies(), 1) assert.EqualValues(t, res.Cookies()[0].Name, continuity.CookieName) }) @@ -165,6 +169,7 @@ func TestManagerRelayState(t *testing.T) { for _, c := range res.Cookies() { relayState = c.Value + fmt.Println(relayState) relayState = strings.Replace(relayState, "a", "b", 1) } require.Len(t, res.Cookies(), 1) @@ -180,7 +185,7 @@ func TestManagerRelayState(t *testing.T) { t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) - require.Len(t, res.Cookies(), 0, "the cookie couldn't be reconstructed without a valid relaystate") + assert.True(t, res.StatusCode == 400 || len(res.Cookies()) == 0) }) t.Run("case=continue cookie not delivered without relaystate", func(t *testing.T) { @@ -205,7 +210,7 @@ func TestManagerRelayState(t *testing.T) { t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) - require.Len(t, res.Cookies(), 0, "the cookie couldn't be reconstructed without a valid relaystate") + assert.True(t, res.StatusCode == 400 || len(res.Cookies()) == 0) }) t.Run("case=pause, abort, and continue session with failure", func(t *testing.T) { @@ -236,6 +241,6 @@ func TestManagerRelayState(t *testing.T) { t.Cleanup(func() { require.NoError(t, res.Body.Close()) }) - require.Len(t, res.Cookies(), 0, "the cookie couldn't be reconstructed without a valid relaystate") + assert.True(t, res.StatusCode == 400 || len(res.Cookies()) == 0) }) } diff --git a/driver/registry_default.go b/driver/registry_default.go index 78a5c5b69da0..3c33be26a111 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -110,9 +110,6 @@ type RegistryDefault struct { continuityManager continuity.Manager - x.RelayStateProvider - session.ManagementProvider - schemaHandler *schema.Handler sessionHandler *session.Handler @@ -676,25 +673,12 @@ func (m *RegistryDefault) Courier(ctx context.Context) (courier.Courier, error) } func (m *RegistryDefault) ContinuityManager() continuity.Manager { - // If m.continuityManager is nil or not a continuity.ManagerCookie - switch m.continuityManager.(type) { - case *continuity.ManagerCookie: - default: + if m.continuityManager == nil { m.continuityManager = continuity.NewManagerCookie(m) } return m.continuityManager } -func (m *RegistryDefault) RelayStateContinuityManager() continuity.Manager { - // If m.continuityManager is nil or not a continuity.ManagerRelayState - switch m.continuityManager.(type) { - case *continuity.ManagerRelayState: - default: - m.continuityManager = continuity.NewManagerRelayState(m, m) - } - return m.continuityManager -} - func (m *RegistryDefault) ContinuityPersister() continuity.Persister { return m.persister } diff --git a/go.sum b/go.sum index 0fabab367e28..76bd8bcd396c 100644 --- a/go.sum +++ b/go.sum @@ -1120,8 +1120,8 @@ github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OU github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= -github.com/ory/x v0.0.534 h1:hc49pmcOuHdJ6rbHVGtJJ4/LU88dzDCtEQKfgeo/ecU= -github.com/ory/x v0.0.534/go.mod h1:CQopDsCC9t0tQsddE9UlyRFVEFd2xjKBVcw4nLMMMS0= +github.com/ory/x v0.0.537 h1:FB8Tioza6pihvy/RsVNzX08Qg3/VpIhI9vBnEQ4iFmQ= +github.com/ory/x v0.0.537/go.mod h1:CQopDsCC9t0tQsddE9UlyRFVEFd2xjKBVcw4nLMMMS0= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= diff --git a/identity/credentials_saml.go b/identity/credentials_saml.go index c420be179f1f..f79e6b5d5506 100644 --- a/identity/credentials_saml.go +++ b/identity/credentials_saml.go @@ -22,11 +22,18 @@ type CredentialsSAML struct { // swagger:model identityCredentialsSamlProvider type CredentialsSAMLProvider struct { Subject string `json:"subject"` - Provider string `json:"samlProvider"` + Provider string `json:"saml_provider"` } // Create an uniq identifier for user in database. Its look like "id + the id of the saml provider" func NewCredentialsSAML(subject string, provider string) (*Credentials, error) { + if provider == "" { + return nil, errors.New("received empty provider in saml credentials") + } + + if subject == "" { + return nil, errors.New("received empty provider in saml credentials") + } var b bytes.Buffer if err := json.NewEncoder(&b).Encode(CredentialsSAML{ Providers: []CredentialsSAMLProvider{ diff --git a/identity/credentials_saml_test.go b/identity/credentials_saml_test.go new file mode 100644 index 000000000000..e6c80a0be709 --- /dev/null +++ b/identity/credentials_saml_test.go @@ -0,0 +1,18 @@ +package identity + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewCredentialsSAML(t *testing.T) { + _, err := NewCredentialsSAML("not-empty", "") + require.Error(t, err) + + _, err = NewCredentialsSAML("", "not-empty") + require.Error(t, err) + + _, err = NewCredentialsSAML("not-empty", "not-empty") + require.NoError(t, err) +} diff --git a/selfservice/strategy/saml/config_test.go b/selfservice/strategy/saml/config_test.go index 0b509776c026..b32b882e6bc3 100644 --- a/selfservice/strategy/saml/config_test.go +++ b/selfservice/strategy/saml/config_test.go @@ -82,8 +82,8 @@ func TestInitSAMLWithoutPoviderID(t *testing.T) { saml.Configuration{ ID: "", Label: "samlProviderLabel", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", AttributesMap: attributesMap, IDPInformation: idpInformation, @@ -131,8 +131,8 @@ func TestInitSAMLWithoutPoviderLabel(t *testing.T) { saml.Configuration{ ID: "samlProvider", Label: "", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", AttributesMap: attributesMap, IDPInformation: idpInformation, @@ -179,8 +179,8 @@ func TestAttributesMapWithoutID(t *testing.T) { saml.Configuration{ ID: "samlProvider", Label: "samlProviderLabel", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", AttributesMap: attributesMap, IDPInformation: idpInformation, @@ -275,8 +275,8 @@ func TestInitSAMLWithoutIDPInformation(t *testing.T) { saml.Configuration{ ID: "samlProvider", Label: "samlProviderLabel", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", AttributesMap: attributesMap, }, @@ -322,8 +322,8 @@ func TestInitSAMLWithMissingIDPInformationField(t *testing.T) { saml.Configuration{ ID: "samlProvider", Label: "samlProviderLabel", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", IDPInformation: idpInformation, AttributesMap: attributesMap, @@ -372,8 +372,8 @@ func TestInitSAMLWithExtraIDPInformationField(t *testing.T) { saml.Configuration{ ID: "samlProvider", Label: "samlProviderLabel", - PublicCertPath: "file://testdata/myservice.cert", - PrivateKeyPath: "file://testdata/myservice.key", + PublicCertPath: "file://testdata/sp_cert.pem", + PrivateKeyPath: "file://testdata/sp_key.pem", Mapper: "file://testdata/saml.jsonnet", IDPInformation: idpInformation, AttributesMap: attributesMap, diff --git a/selfservice/strategy/saml/provider_config.go b/selfservice/strategy/saml/provider_config.go index 357c9516f965..096513fa9f56 100644 --- a/selfservice/strategy/saml/provider_config.go +++ b/selfservice/strategy/saml/provider_config.go @@ -36,7 +36,7 @@ type ConfigurationCollection struct { SAMLProviders []Configuration `json:"providers"` } -func (c ConfigurationCollection) Provider(id string, reg registrationStrategyDependencies) (Provider, error) { +func (c ConfigurationCollection) Provider(id string, reg dependencies) (Provider, error) { for k := range c.SAMLProviders { p := c.SAMLProviders[k] if p.ID == id { diff --git a/selfservice/strategy/saml/provider_saml.go b/selfservice/strategy/saml/provider_saml.go index e0917dfd44ea..4b31d814ba55 100644 --- a/selfservice/strategy/saml/provider_saml.go +++ b/selfservice/strategy/saml/provider_saml.go @@ -13,12 +13,12 @@ import ( type ProviderSAML struct { config *Configuration - reg registrationStrategyDependencies + reg dependencies } func NewProviderSAML( config *Configuration, - reg registrationStrategyDependencies, + reg dependencies, ) *ProviderSAML { return &ProviderSAML{ config: config, diff --git a/selfservice/strategy/saml/strategy.go b/selfservice/strategy/saml/strategy.go index 4e2c69b91653..64b49322b7b9 100644 --- a/selfservice/strategy/saml/strategy.go +++ b/selfservice/strategy/saml/strategy.go @@ -17,19 +17,18 @@ import ( "github.com/tidwall/gjson" "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/schema" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" - "github.com/go-playground/validator/v10" - "github.com/ory/x/decoderx" - "github.com/ory/x/fetcher" + "github.com/ory/x/jsonnetsecure" "github.com/ory/x/jsonx" "github.com/ory/kratos/continuity" "github.com/ory/kratos/driver/config" - "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/errorx" "github.com/ory/kratos/selfservice/flow" @@ -56,50 +55,57 @@ const ( var _ identity.ActiveCredentialsCounter = new(Strategy) -type registrationStrategyDependencies interface { - x.LoggingProvider - x.WriterProvider - x.CSRFTokenGeneratorProvider - x.CSRFProvider - x.HTTPClientProvider +type dependencies interface { + errorx.ManagementProvider config.Provider - continuity.ManagementProvider - continuity.ManagementProviderRelayState + x.LoggingProvider + x.CookieProvider + x.CSRFProvider + x.CSRFTokenGeneratorProvider + x.WriterProvider + x.HTTPClientProvider + x.TracingProvider - errorx.ManagementProvider - hash.HashProvider + identity.ValidationProvider + identity.PrivilegedPoolProvider + identity.ActiveCredentialsCounterStrategyProvider + identity.ManagementProvider - registration.HandlerProvider - registration.HooksProvider - registration.ErrorHandlerProvider - registration.HookExecutorProvider - registration.FlowPersistenceProvider + session.ManagementProvider + session.HandlerProvider - login.HooksProvider - login.ErrorHandlerProvider login.HookExecutorProvider login.FlowPersistenceProvider + login.HooksProvider + login.StrategyProvider login.HandlerProvider + login.ErrorHandlerProvider + registration.HookExecutorProvider + registration.FlowPersistenceProvider + registration.HooksProvider + registration.StrategyProvider + registration.HandlerProvider + registration.ErrorHandlerProvider + + settings.ErrorHandlerProvider settings.FlowPersistenceProvider settings.HookExecutorProvider - settings.HooksProvider - settings.ErrorHandlerProvider - identity.PrivilegedPoolProvider - identity.ValidationProvider + continuity.ManagementProvider - session.HandlerProvider - session.ManagementProvider + cipher.Provider + + jsonnetsecure.VMProvider } func (s *Strategy) ID() identity.CredentialsType { return identity.CredentialsTypeSAML } -func (s *Strategy) D() registrationStrategyDependencies { +func (s *Strategy) D() dependencies { return s.d } @@ -115,10 +121,9 @@ func isForced(req interface{}) bool { } type Strategy struct { - d registrationStrategyDependencies - f *fetcher.Fetcher - v *validator.Validate - hd *decoderx.HTTP + d dependencies + validator *schema.Validator + dec *decoderx.HTTP } type authCodeContainer struct { @@ -132,12 +137,10 @@ func generateState(flowID string) string { return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", flowID, state))) } -func NewStrategy(d registrationStrategyDependencies) *Strategy { +func NewStrategy(d dependencies) *Strategy { return &Strategy{ - d: d, - f: fetcher.NewFetcher(), - v: validator.New(), - hd: decoderx.NewHTTP(), + d: d, + validator: schema.NewValidator(), } } @@ -247,7 +250,7 @@ func (s *Strategy) alreadyAuthenticated(w http.ResponseWriter, r *http.Request, func (s *Strategy) validateCallback(w http.ResponseWriter, r *http.Request) (flow.Flow, *authCodeContainer, error) { var cntnr authCodeContainer - if _, err := s.d.RelayStateContinuityManager().Continue(r.Context(), w, r, sessionName, continuity.WithPayload(&cntnr)); err != nil { + if _, err := s.d.ContinuityManager().Continue(r.Context(), w, r, sessionName, continuity.WithPayload(&cntnr), continuity.UseRelayState()); err != nil { return nil, nil, err } @@ -286,7 +289,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt m, err := GetMiddleware(pid) if err != nil { - s.forwardError(w, r, err) + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) return } @@ -296,28 +299,28 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt // We parse the SAML Response to get the SAML Assertion assertion, err := m.ServiceProvider.ParseResponse(r, possibleRequestIDs) if err != nil { - s.forwardError(w, r, err) + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) return } // We get the user's attributes from the SAML Response (assertion) attributes, err := s.GetAttributesFromAssertion(assertion) if err != nil { - s.forwardError(w, r, err) + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) return } // We get the provider information from the config file provider, err := s.Provider(r.Context(), pid) if err != nil { - s.forwardError(w, r, err) + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) return } // We translate SAML Attributes into claims (To create an identity we need these claims) claims, err := provider.Claims(r.Context(), s.d.Config(), attributes, pid) if err != nil { - s.forwardError(w, r, err) + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) return } @@ -326,10 +329,10 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt // Now that we have the claims and the provider, we have to decide if we log or register the user if ff, err := s.processLoginOrRegister(w, r, a, provider, claims); err != nil { if ff != nil { - s.forwardError(w, r, err) + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) return } - s.forwardError(w, r, err) + s.forwardError(w, r, s.handleError(w, r, req, pid, nil, err)) } return } @@ -385,7 +388,7 @@ func (s *Strategy) populateMethod(r *http.Request, c *container.Container, messa func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Flow, provider string, traits []byte, err error) error { switch rf := f.(type) { case *login.Flow: - return ErrAPIFlowNotSupported.WithTrace(err) + return err case *registration.Flow: // Reset all nodes to not confuse users. // This is kinda hacky and will probably need to be updated at some point. @@ -399,24 +402,22 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl if traits != nil { ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) if err != nil { - return ErrInvalidSAMLConfiguration.WithTrace(err) + return err } - traitNodes, err := container.NodesFromJSONSchema(r.Context(), node.SAMLGroup, ds.String(), "", nil) + traitNodes, err := container.NodesFromJSONSchema(r.Context(), node.OpenIDConnectGroup, ds.String(), "", nil) if err != nil { - return herodot.ErrInternalServerError.WithTrace(err) + return err } rf.UI.Nodes = append(rf.UI.Nodes, traitNodes...) - rf.UI.UpdateNodeValuesFromJSON(traits, "traits", node.SAMLGroup) + rf.UI.UpdateNodeValuesFromJSON(traits, "traits", node.OpenIDConnectGroup) } - return herodot.ErrInternalServerError.WithTrace(err) - case *settings.Flow: - return ErrAPIFlowNotSupported.WithTrace(err) + return err } - return herodot.ErrInternalServerError.WithTrace(err) + return err } func (s *Strategy) CountActiveCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { diff --git a/selfservice/strategy/saml/strategy_login.go b/selfservice/strategy/saml/strategy_login.go index 67faa6682b25..f8670b16fe55 100644 --- a/selfservice/strategy/saml/strategy_login.go +++ b/selfservice/strategy/saml/strategy_login.go @@ -7,9 +7,7 @@ import ( "time" "github.com/gofrs/uuid" - "github.com/google/go-jsonnet" "github.com/pkg/errors" - "github.com/tidwall/gjson" "github.com/ory/herodot" "github.com/ory/kratos/continuity" @@ -58,7 +56,7 @@ type SubmitSelfServiceLoginFlowWithSAMLMethodBody struct { // Login and give a session to the user func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login.Flow, provider Provider, c *identity.Credentials, i *identity.Identity, claims *Claims) (*registration.Flow, error) { - s.updateIdentityTraits(i, provider, claims) + s.updateIdentityTraits(w, r, i, provider, claims) var o identity.CredentialsSAML if err := json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o); err != nil { @@ -104,13 +102,13 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } state := generateState(f.ID.String()) - if err := s.d.RelayStateContinuityManager().Pause(r.Context(), w, r, sessionName, + if err := s.d.ContinuityManager().Pause(r.Context(), w, r, sessionName, continuity.WithPayload(&authCodeContainer{ State: state, FlowID: f.ID.String(), Traits: p.Traits, }), - continuity.WithLifespan(time.Minute*30)); err != nil { + continuity.WithLifespan(time.Minute*30), continuity.UseRelayState()); err != nil { return nil, s.handleError(w, r, f, pid, nil, err) } @@ -142,28 +140,17 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au } // In order to do a JustInTimeProvisioning, it is important to update the identity traits at each new SAML connection -func (s *Strategy) updateIdentityTraits(i *identity.Identity, provider Provider, claims *Claims) error { - jn, err := s.f.Fetch(provider.Config().Mapper) - if err != nil { - return nil - } +func (s *Strategy) updateIdentityTraits(w http.ResponseWriter, r *http.Request, i *identity.Identity, provider Provider, claims *Claims) error { var jsonClaims bytes.Buffer if err := json.NewEncoder(&jsonClaims).Encode(claims); err != nil { - return nil + return err } - vm := jsonnet.MakeVM() - vm.ExtCode("claims", jsonClaims.String()) - evaluated, err := vm.EvaluateAnonymousSnippet(provider.Config().Mapper, jn.String()) - if err != nil { + if err := s.setTraits(w, r, claims, provider, jsonClaims, i); err != nil { return err - } else if traits := gjson.Get(evaluated, "identity.traits"); !traits.IsObject() { - i.Traits = []byte{'{', '}'} - return errors.New("SAML Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!") - } else { - i.Traits = []byte(traits.Raw) - return nil } + return nil + } diff --git a/selfservice/strategy/saml/strategy_registration.go b/selfservice/strategy/saml/strategy_registration.go index d536be07d629..208fce11655b 100644 --- a/selfservice/strategy/saml/strategy_registration.go +++ b/selfservice/strategy/saml/strategy_registration.go @@ -34,7 +34,7 @@ func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *reg } i := identity.NewIdentity(s.d.Config().DefaultIdentityTraitsSchemaID(r.Context())) - if err := s.setTraits(w, r, a, claims, provider, jsonClaims, i); err != nil { + if err := s.setTraits(w, r, claims, provider, jsonClaims, i); err != nil { return nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } @@ -46,7 +46,7 @@ func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *reg return i, nil } -func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, jsonClaims bytes.Buffer, i *identity.Identity) error { +func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, claims *Claims, provider Provider, jsonClaims bytes.Buffer, i *identity.Identity) error { traitsMap := make(map[string]interface{}) json.Unmarshal(jsonClaims.Bytes(), &traitsMap) @@ -55,7 +55,7 @@ func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registra delete(traitsMap, "sub") traits, err := json.Marshal(traitsMap) if err != nil { - return s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + return err } i.Traits = identity.Traits(traits) @@ -119,7 +119,7 @@ func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { return errors.WithStack(err) } - if err := s.hd.Decode(r, &p, compiler, + if err := s.dec.Decode(r, &p, compiler, decoderx.HTTPKeepRequestBody(true), decoderx.HTTPDecoderSetValidatePayloads(false), decoderx.HTTPDecoderUseQueryAndBody(), diff --git a/selfservice/strategy/saml/testdata/sp2_cert.pem b/selfservice/strategy/saml/testdata/sp2_cert.pem deleted file mode 100644 index e74d8fcb8c00..000000000000 --- a/selfservice/strategy/saml/testdata/sp2_cert.pem +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFbTCCA1WgAwIBAgIUDxKgJwZpe1Fmo4nFUVYTXwvIgOAwDQYJKoZIhvcNAQEL -BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzAxMTExMzQ4MTlaGA8yMjk2 -MTAyNTEzNDgxOVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx -ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAM6XqltTvOTIqGZeUKnRfWqZdmjiJWU4XtSh4D8v -DUmBRIYXEBbcSqcNKsHjNoHoTXBqa9jkxl5ZIOwE3mCWr5heXTR9T4IhtsWRopWB -Aio8AIdAPuHiQoMxGm0jwZ4KCaZfLsHRIflJloSFEtebungmpDqo4hwsM6IUz5z3 -bKX1rlCCtupHpLihaLMVmgSSBvLDeAPvtHIBtNjNyhhi9TMS8SxFbCI7+PXRTRrR -G2ozUrR8qIBQxh/kA/rQWH4GGLt6phBdsxnmdn2idLhyvTjT8nA09gglvXZZ7gI9 -m8jbmUgpVGzmk38cAw+oENVMufAZWJBdAfrIfK0TR5YqXqhfj6e5BBf4OyM4rMlo -xlJnbQTI30uB2uzPfjry43NSu9jlFc/+r4ufW+ptEH9YIx9HbSz8y+hrpFdofSEQ -8J6w71x3NzN1L/hFKYqhQ8Gv2Flk1kp0iLkZ3tP1UCK2eDFhM9BBD3ZHYyByboWj -sKffst67LUAlufiLW4q0tZkpWwUR5fExAsFF//CciKPIpe8WBKlHxw6FYGPRGva5 -KK10jGZTJoM+KYwcZFpHCaWt8tFQB8ti9bQIP2etmrpsBZuR4CDuZEkMuvSLsJE5 -6SI+rxT3WKwK9zAginomFvEEvaqp43RT2a1zzYAfE3pRETis1tyoNN2I384ywL41 -lofhAgMBAAGjUzBRMB0GA1UdDgQWBBSmECM2phplGyTKV/dGef+JUPVpgjAfBgNV -HSMEGDAWgBSmECM2phplGyTKV/dGef+JUPVpgjAPBgNVHRMBAf8EBTADAQH/MA0G -CSqGSIb3DQEBCwUAA4ICAQCxbwfNQvpw68pTmyCIipb5pkuVDnjp65RV0wJbOfDR -qiQHVQsJexY1xmptOADzCvBQIkAAKCeLfJ8tKS6473Xc3BayREyJpN3oQsr1MDep -j6/ae8I1wt6uJ18M93wArWou/nuDHlkBeEKYlwCQYRZPW++9E38v6ZzKK7qHN+6M -vFKXx/Q98WpaNo5Oj0o8ngEYxS5/9Axn7EUBKLpikb8KNIO+icqc6DPs4GTqKb4/ -wC5FPcROoQAau3RsrZ8cAMU+zGt6OeYWU2Sabsnm1lo3bYMx2XuQOEQvLcrTjBLm -041LYk3SbPotBWc4ahVF4SUZWHKZst76+cZtR5RLZt3jjKjTguq76itPnuZxdM0g -JmdhophvFNwyKjxQ3jbJc9W1mpq5ILrtzO0pWTjOrBDWdZ4GF078GmjYrtJJ2e7T -LI0uuXwKB0K5SktluIM+7PVXYqt3ZnPJ6zjMCuYoQT5ua29hs9Qi/zjAf2Mf8JLo -t2MdAmvVZDr8bSVkyx0RrIKwYKLJ6b+KgdSACb618GV6dLpqMbe3mC+yPPa/FKIS -M64SBf/gBlMQpcUWdH4IWvQXu1Lmn+TPr4+BXi7loMGwAcGH7pcbYouOMsZlJ/CG -1cQ5cf3kevKolmJVaxJC+ZEBCqvM3/FySSmNNCmQidXi5QLHP87uYn/9aRaHh1kM -eA== ------END CERTIFICATE----- diff --git a/selfservice/strategy/saml/testdata/sp2_key.pem b/selfservice/strategy/saml/testdata/sp2_key.pem deleted file mode 100644 index 9bf6aeab4b89..000000000000 --- a/selfservice/strategy/saml/testdata/sp2_key.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -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 -d7y+GE7k0jSTLxP851L5BvfqKi2Ay0w= ------END PRIVATE KEY----- diff --git a/selfservice/strategy/saml/vulnerabilities_helper_test.go b/selfservice/strategy/saml/vulnerabilities_helper_test.go index b49f433d81b8..9592902f0f61 100644 --- a/selfservice/strategy/saml/vulnerabilities_helper_test.go +++ b/selfservice/strategy/saml/vulnerabilities_helper_test.go @@ -217,12 +217,12 @@ func startContinuity(resp *httptest.ResponseRecorder, r *http.Request, strategy strategy.D().LoginFlowPersister().CreateLoginFlow(r.Context(), f) state := x.NewUUID().String() - strategy.D().RelayStateContinuityManager().Pause(r.Context(), resp, r, "ory_kratos_saml_auth_code_session", + strategy.D().ContinuityManager().Pause(r.Context(), resp, r, "ory_kratos_saml_auth_code_session", continuity.WithPayload(&authCodeContainer{ State: state, FlowID: f.ID.String(), }), - continuity.WithLifespan(time.Minute*30)) + continuity.WithLifespan(time.Minute*30), continuity.UseRelayState()) } func initRouterParams() httprouter.Params { diff --git a/selfservice/strategy/saml/vulnerabilities_test.go b/selfservice/strategy/saml/vulnerabilities_test.go index 7c0095aef5a8..8af41cc7ee5a 100644 --- a/selfservice/strategy/saml/vulnerabilities_test.go +++ b/selfservice/strategy/saml/vulnerabilities_test.go @@ -496,7 +496,7 @@ func TestAddXMLCommentsInSAMLAttributes(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) traitsMap := make(map[string]interface{}) json.Unmarshal(ids[0].Traits, &traitsMap) @@ -680,7 +680,7 @@ func TestXSW3AssertionWrap1(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -742,7 +742,7 @@ func TestXSW4AssertionWrap2(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -804,7 +804,7 @@ func TestXSW5AssertionWrap3(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -866,7 +866,7 @@ func TestXSW6AssertionWrap4(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -928,7 +928,7 @@ func TestXSW7AssertionWrap5(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) @@ -988,7 +988,7 @@ func TestXSW8AssertionWrap6(t *testing.T) { strategy.HandleCallback(resp, req, ps) // Get all identities - ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ExpandEverything, 0, 1000) + ids, _ := strategy.D().PrivilegedIdentityPool().ListIdentities(context.Background(), identity.ListIdentityParameters{Expand: identity.ExpandEverything, Page: 0, PerPage: 1000}) // We have to check that there is either an error or an identity created without the modified attribute assert.Check(t, strings.Contains(resp.HeaderMap["Location"][0], "error") || strings.Contains(string(ids[0].Traits), "alice@example.com")) diff --git a/x/provider.go b/x/provider.go index ec2ae5736ce5..f87f3f0fdc95 100644 --- a/x/provider.go +++ b/x/provider.go @@ -29,11 +29,6 @@ type CookieProvider interface { ContinuityCookieManager(ctx context.Context) sessions.StoreExact } -type RelayStateProvider interface { - RelayStateManager(ctx context.Context) sessions.StoreExact - ContinuityRelayStateManager(ctx context.Context) sessions.StoreExact -} - type TracingProvider interface { Tracer(ctx context.Context) *otelx.Tracer }