From 6cf07784831527d596e37239c00f9e3fb395d2b3 Mon Sep 17 00:00:00 2001 From: ThibaultHerard Date: Tue, 30 Aug 2022 15:12:58 +0000 Subject: [PATCH] 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 ++++++ driver/registry_default.go | 24 +- driver/registry_default_saml.go | 11 + driver/registry_default_test.go | 4 +- embedx/config.schema.json | 239 +++++++++ go.mod | 7 + go.sum | 12 + identity/credentials.go | 1 + internal/httpclient/.openapi-generator/FILES | 2 + internal/httpclient/README.md | 2 + internal/httpclient/api/openapi.yaml | 45 ++ .../httpclient/docs/SelfServiceSamlUrl.md | 72 +++ internal/httpclient/docs/V0alpha2Api.md | 62 +++ .../httpclient/model_self_service_saml_url.go | 138 +++++ ...0_identity_credentials_types_saml.down.sql | 1 + ...000_identity_credentials_types_saml.up.sql | 1 + selfservice/flow/saml/handler.go | 336 +++++++++++++ .../flow/saml/helpertest/helpertest.go | 203 ++++++++ selfservice/flow/saml/test/handler_test.go | 92 ++++ selfservice/flow/saml/test/metadata_test.go | 127 +++++ selfservice/flow/saml/test/testdata/cert.pem | 13 + .../saml/test/testdata/expected_metadata.xml | 25 + .../saml/test/testdata/idp_saml_metadata.xml | 118 +++++ selfservice/flow/saml/test/testdata/key.pem | 15 + .../flow/saml/test/testdata/myservice.cert | 19 + .../flow/saml/test/testdata/myservice.key | 28 ++ .../test/testdata/registration.schema.json | 16 + .../flow/saml/test/testdata/saml.jsonnet | 17 + .../flow/saml/test/testdata/samlkratos.crt | 21 + selfservice/strategy/saml/error.go | 16 + selfservice/strategy/saml/provider.go | 43 ++ selfservice/strategy/saml/provider_config.go | 33 ++ selfservice/strategy/saml/provider_saml.go | 67 +++ .../saml/strategy/.schema/link.schema.json | 17 + .../strategy/.schema/settings.schema.json | 19 + selfservice/strategy/saml/strategy/const.go | 5 + selfservice/strategy/saml/strategy/schema.go | 8 + .../strategy/saml/strategy/strategy.go | 471 ++++++++++++++++++ .../strategy/saml/strategy/strategy_auth.go | 66 +++ .../strategy/saml/strategy/strategy_login.go | 140 ++++++ .../saml/strategy/strategy_registration.go | 164 ++++++ .../saml/strategy/test/strategy_test.go | 191 +++++++ .../strategy/test/testdata/SP_IDPMetadata.xml | 118 +++++ .../test/testdata/SP_SamlResponse.xml | 38 ++ .../TestSPCanHandleOneloginResponse_response | 1 + .../saml/strategy/test/testdata/cert.pem | 13 + .../test/testdata/idp_saml_metadata.xml | 118 +++++ .../saml/strategy/test/testdata/key.pem | 15 + .../strategy/test/testdata/myservice.cert | 19 + .../saml/strategy/test/testdata/myservice.key | 28 ++ .../test/testdata/registration.schema.json | 16 + .../saml/strategy/test/testdata/saml.jsonnet | 17 + .../strategy/test/testdata/saml_response.xml | 11 + .../strategy/test/testdata/samlkratos.crt | 21 + selfservice/strategy/saml/strategy/types.go | 63 +++ spec/api.json | 59 +++ spec/swagger.json | 51 ++ ui/node/node.go | 1 + x/provider.go | 5 + x/relaystate.go | 51 ++ 63 files changed, 3838 insertions(+), 3 deletions(-) create mode 100644 continuity/manager_relaystate.go create mode 100644 driver/registry_default_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 selfservice/flow/saml/handler.go create mode 100644 selfservice/flow/saml/helpertest/helpertest.go create mode 100644 selfservice/flow/saml/test/handler_test.go create mode 100644 selfservice/flow/saml/test/metadata_test.go create mode 100644 selfservice/flow/saml/test/testdata/cert.pem create mode 100644 selfservice/flow/saml/test/testdata/expected_metadata.xml create mode 100644 selfservice/flow/saml/test/testdata/idp_saml_metadata.xml create mode 100644 selfservice/flow/saml/test/testdata/key.pem create mode 100755 selfservice/flow/saml/test/testdata/myservice.cert create mode 100755 selfservice/flow/saml/test/testdata/myservice.key create mode 100644 selfservice/flow/saml/test/testdata/registration.schema.json create mode 100644 selfservice/flow/saml/test/testdata/saml.jsonnet create mode 100755 selfservice/flow/saml/test/testdata/samlkratos.crt create mode 100644 selfservice/strategy/saml/error.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/strategy/.schema/link.schema.json create mode 100644 selfservice/strategy/saml/strategy/.schema/settings.schema.json create mode 100644 selfservice/strategy/saml/strategy/const.go create mode 100644 selfservice/strategy/saml/strategy/schema.go create mode 100644 selfservice/strategy/saml/strategy/strategy.go create mode 100644 selfservice/strategy/saml/strategy/strategy_auth.go create mode 100644 selfservice/strategy/saml/strategy/strategy_login.go create mode 100644 selfservice/strategy/saml/strategy/strategy_registration.go create mode 100644 selfservice/strategy/saml/strategy/test/strategy_test.go create mode 100644 selfservice/strategy/saml/strategy/test/testdata/SP_IDPMetadata.xml create mode 100644 selfservice/strategy/saml/strategy/test/testdata/SP_SamlResponse.xml create mode 100644 selfservice/strategy/saml/strategy/test/testdata/TestSPCanHandleOneloginResponse_response create mode 100644 selfservice/strategy/saml/strategy/test/testdata/cert.pem create mode 100644 selfservice/strategy/saml/strategy/test/testdata/idp_saml_metadata.xml create mode 100644 selfservice/strategy/saml/strategy/test/testdata/key.pem create mode 100755 selfservice/strategy/saml/strategy/test/testdata/myservice.cert create mode 100755 selfservice/strategy/saml/strategy/test/testdata/myservice.key create mode 100644 selfservice/strategy/saml/strategy/test/testdata/registration.schema.json create mode 100644 selfservice/strategy/saml/strategy/test/testdata/saml.jsonnet create mode 100644 selfservice/strategy/saml/strategy/test/testdata/saml_response.xml create mode 100755 selfservice/strategy/saml/strategy/test/testdata/samlkratos.crt create mode 100644 selfservice/strategy/saml/strategy/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 c3f50a4798ef..a2b2b92e88ac 100644 --- a/continuity/manager.go +++ b/continuity/manager.go @@ -17,6 +17,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..4d6d1204118e --- /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.WithDebugf("Resumable ID from RelayState could not be found in the datastore: %+v", err)) + } 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/driver/registry_default.go b/driver/registry_default.go index 631bb4422fc4..ebec168b664a 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -37,11 +37,13 @@ import ( "github.com/ory/kratos/hash" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/saml" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/kratos/selfservice/strategy/profile" + samlstrategy "github.com/ory/kratos/selfservice/strategy/saml/strategy" "github.com/ory/kratos/x" "github.com/cenkalti/backoff" @@ -101,6 +103,9 @@ type RegistryDefault struct { continuityManager continuity.Manager + x.RelayStateProvider + session.ManagementProvider + schemaHandler *schema.Handler sessionHandler *session.Handler @@ -123,6 +128,8 @@ type RegistryDefault struct { selfserviceLoginHandler *login.Handler selfserviceLoginRequestErrorHandler *login.ErrorHandler + selfserviceSAMLHandler *saml.Handler + selfserviceSettingsHandler *settings.Handler selfserviceSettingsErrorHandler *settings.ErrorHandler selfserviceSettingsExecutor *settings.HookExecutor @@ -155,6 +162,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) @@ -284,6 +292,7 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} { m.selfserviceStrategies = []interface{}{ password2.NewStrategy(m), oidc.NewStrategy(m), + samlstrategy.NewStrategy(m), profile.NewStrategy(m), link.NewStrategy(m), totp.NewStrategy(m), @@ -616,12 +625,25 @@ func (m *RegistryDefault) Courier(ctx context.Context) courier.Courier { } 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..c201047fc21d --- /dev/null +++ b/driver/registry_default_saml.go @@ -0,0 +1,11 @@ +package driver + +import "github.com/ory/kratos/selfservice/flow/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 96ac14a7944f..427d8a5803d4 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -687,7 +687,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 { @@ -696,7 +696,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 f2c4ef9696ab..ffbba5f75a52 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -347,6 +347,213 @@ } } }, + "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" + ] + }, + "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": { @@ -806,6 +1013,9 @@ "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "saml": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "hooks": { "$ref": "#/definitions/selfServiceHooks" } @@ -1430,6 +1640,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 c2517839d518..3b9a17cce94a 100644 --- a/go.mod +++ b/go.mod @@ -23,12 +23,14 @@ require ( github.com/Masterminds/sprig/v3 v3.0.0 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.6.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.0 @@ -81,6 +83,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 github.com/rs/cors v1.8.2 + github.com/russellhaering/goxmldsig v1.1.1 github.com/sirupsen/logrus v1.8.1 github.com/slack-go/slack v0.7.4 github.com/spf13/cobra v1.5.0 @@ -98,6 +101,7 @@ require ( golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f golang.org/x/tools v0.1.11 + gotest.tools v2.2.0+incompatible ) require ( @@ -136,6 +140,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.14+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker v20.10.9+incompatible // indirect @@ -184,6 +189,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.7 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/css v1.0.0 // indirect @@ -227,6 +233,7 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/markbates/hmax v1.0.0 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect diff --git a/go.sum b/go.sum index 9739f4f0dc30..1f9b8f540f64 100644 --- a/go.sum +++ b/go.sum @@ -231,6 +231,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/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-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 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= @@ -454,6 +456,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.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= @@ -468,6 +474,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/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= @@ -1271,6 +1278,8 @@ github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCn github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +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= @@ -1624,6 +1633,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 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= 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= @@ -1851,6 +1862,7 @@ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= 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= diff --git a/identity/credentials.go b/identity/credentials.go index b650d7cc1e69..546e1593985f 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -66,6 +66,7 @@ const ( CredentialsTypeTOTP CredentialsType = "totp" CredentialsTypeLookup CredentialsType = "lookup_secret" CredentialsTypeWebAuthn CredentialsType = "webauthn" + CredentialsTypeSAML CredentialsType = "saml" ) const ( diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index 53993154b1db..816cbf306f91 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -48,6 +48,7 @@ docs/SelfServiceRecoveryFlow.md docs/SelfServiceRecoveryFlowState.md docs/SelfServiceRecoveryLink.md docs/SelfServiceRegistrationFlow.md +docs/SelfServiceSamlUrl.md docs/SelfServiceSettingsFlow.md docs/SelfServiceSettingsFlowState.md docs/SelfServiceVerificationFlow.md @@ -139,6 +140,7 @@ 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 diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 5c929d3c48e7..954abdffa952 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -111,6 +111,7 @@ Class | Method | HTTP request | Description *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 @@ -171,6 +172,7 @@ Class | Method | HTTP request | Description - [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) diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml index b6231e0d4415..e7dbf255560b 100644 --- a/internal/httpclient/api/openapi.yaml +++ b/internal/httpclient/api/openapi.yaml @@ -1267,6 +1267,33 @@ paths: summary: Create a Logout URL for Browsers tags: - v0alpha2 + /self-service/methods/saml/auth: + get: + description: |- + Initiates and performs the SAML authentication request to the identity provider + 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: |- @@ -4071,6 +4098,24 @@ components: - type - ui type: object + selfServiceSamlUrl: + properties: + saml_acs_url: + description: |- + SamlAcsURL is a post endpoint to handle SAML Response + + format: uri + type: string + saml_metadata_url: + description: |- + SamlMetadataURL is a get endpoint to get the metadata + + format: uri + type: string + required: + - saml_acs_url + - saml_metadata_url + type: object selfServiceSettingsFlow: description: |- This flow is used when an identity wants to update settings 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/docs/V0alpha2Api.md b/internal/httpclient/docs/V0alpha2Api.md index 2f918b756a1c..68864a50211c 100644 --- a/internal/httpclient/docs/V0alpha2Api.md +++ b/internal/httpclient/docs/V0alpha2Api.md @@ -29,6 +29,7 @@ Method | HTTP request | Description [**InitializeSelfServiceRecoveryFlowWithoutBrowser**](V0alpha2Api.md#InitializeSelfServiceRecoveryFlowWithoutBrowser) | **Get** /self-service/recovery/api | Initialize Recovery Flow for APIs, Services, Apps, ... [**InitializeSelfServiceRegistrationFlowForBrowsers**](V0alpha2Api.md#InitializeSelfServiceRegistrationFlowForBrowsers) | **Get** /self-service/registration/browser | Initialize Registration Flow for Browsers [**InitializeSelfServiceRegistrationFlowWithoutBrowser**](V0alpha2Api.md#InitializeSelfServiceRegistrationFlowWithoutBrowser) | **Get** /self-service/registration/api | Initialize Registration Flow for APIs, Services, Apps, ... +[**InitializeSelfServiceSamlFlowForBrowsers**](V0alpha2Api.md#InitializeSelfServiceSamlFlowForBrowsers) | **Get** /self-service/methods/saml/auth | Initiates and performs the SAML authentication request to the identity provider [**InitializeSelfServiceSettingsFlowForBrowsers**](V0alpha2Api.md#InitializeSelfServiceSettingsFlowForBrowsers) | **Get** /self-service/settings/browser | Initialize Settings Flow for Browsers [**InitializeSelfServiceSettingsFlowWithoutBrowser**](V0alpha2Api.md#InitializeSelfServiceSettingsFlowWithoutBrowser) | **Get** /self-service/settings/api | Initialize Settings Flow for APIs, Services, Apps, ... [**InitializeSelfServiceVerificationFlowForBrowsers**](V0alpha2Api.md#InitializeSelfServiceVerificationFlowForBrowsers) | **Get** /self-service/verification/browser | Initialize Verification Flow for Browser Clients @@ -1745,6 +1746,67 @@ No authorization required [[Back to README]](../README.md) +## InitializeSelfServiceSamlFlowForBrowsers + +> SelfServiceRegistrationFlow InitializeSelfServiceSamlFlowForBrowsers(ctx).Execute() + +Initialize Registration Flow for APIs, Services, Apps, ... + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.V0alpha2Api.InitializeSelfServiceSamlFlowForBrowsers(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `V0alpha2Api.InitializeSelfServiceSamlFlowForBrowsers``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `InitializeSelfServiceSamlFlowForBrowsers`: SelfServiceRegistrationFlow + fmt.Fprintf(os.Stdout, "Response from `V0alpha2Api.InitializeSelfServiceSamlFlowForBrowsers`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiInitializeSelfServiceSamlFlowForBrowsersRequest struct via the builder pattern + + +### Return type + +[**SelfServiceRegistrationFlow**](SelfServiceRegistrationFlow.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## InitializeSelfServiceSettingsFlowForBrowsers > SelfServiceSettingsFlow InitializeSelfServiceSettingsFlowForBrowsers(ctx).ReturnTo(returnTo).Execute() 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/selfservice/flow/saml/handler.go b/selfservice/flow/saml/handler.go new file mode 100644 index 000000000000..9ce5b65cd1e1 --- /dev/null +++ b/selfservice/flow/saml/handler.go @@ -0,0 +1,336 @@ +package saml + +import ( + "bytes" + "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "net/http" + "net/url" + "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" + + samlstrategy "github.com/ory/kratos/selfservice/strategy/saml" + + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "github.com/ory/x/jsonx" +) + +const ( + RouteSamlMetadata = "/self-service/methods/saml/metadata" + RouteSamlLoginInit = "/self-service/methods/saml/auth" // Redirect to the IDP + RouteSamlAcs = "/self-service/methods/saml/acs" +) + +var ErrNoSession = errors.New("saml: session not present") +var samlMiddleware *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().IgnorePath(RouteSamlLoginInit) + h.d.CSRFHandler().IgnorePath(RouteSamlAcs) + + router.GET(RouteSamlMetadata, h.serveMetadata) + router.GET(RouteSamlLoginInit, h.loginWithIdp) +} + +// Handle /selfservice/methods/saml/metadata +func (h *Handler) serveMetadata(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + config := h.d.Config() + if samlMiddleware == nil { + if err := h.instantiateMiddleware(r.Context(), *config); err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } + } + + samlMiddleware.ServeMetadata(w, r) +} + +// 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 + if samlMiddleware == nil { + config := h.d.Config() + if err := h.instantiateMiddleware(r.Context(), *config); err != nil { + h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + } + } + + conf := h.d.Config() + + // 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 + samlMiddleware.HandleStartAuthFlow(w, r) + } else { + // A session already exist, we redirect to the main page + http.Redirect(w, r, conf.SelfServiceBrowserDefaultReturnTo(r.Context()).Path, http.StatusTemporaryRedirect) + } +} + +func DestroyMiddlewareIfExists() { + if samlMiddleware != nil { + samlMiddleware = nil + } +} + +func (h *Handler) instantiateMiddleware(ctx context.Context, config config.Config) error { + // Create a SAMLProvider object from the config file + var c samlstrategy.ConfigurationCollection + conf := config.SelfServiceStrategy(ctx, "saml").Config + if err := jsonx. + NewStrictDecoder(bytes.NewBuffer(conf)). + Decode(&c); err != nil { + return errors.Wrapf(err, "Unable to decode config %v", string(conf)) + } + + // Key pair to encrypt and sign SAML requests + keyPair, err := tls.LoadX509KeyPair(strings.Replace(c.SAMLProviders[len(c.SAMLProviders)-1].PublicCertPath, "file://", "", 1), strings.Replace(c.SAMLProviders[len(c.SAMLProviders)-1].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 c.SAMLProviders[len(c.SAMLProviders)-1].IDPInformation["idp_metadata_url"] != "" { + + metadataURL := c.SAMLProviders[len(c.SAMLProviders)-1].IDPInformation["idp_metadata_url"] + // The metadata file is provided + if strings.HasPrefix(metadataURL, "file://") { + metadataURL = strings.Replace(metadataURL, "file://", "", 1) + + 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(c.SAMLProviders[len(c.SAMLProviders)-1].IDPInformation["idp_entity_id"]) + if err != nil { + return err + } + + // The IDP SSO URL + IDPSSOURL, err := url.Parse(c.SAMLProviders[len(c.SAMLProviders)-1].IDPInformation["idp_sso_url"]) + if err != nil { + return err + } + + // The IDP Logout URL + IDPlogoutURL, err := url.Parse(c.SAMLProviders[len(c.SAMLProviders)-1].IDPInformation["idp_logout_url"]) + if err != nil { + return err + } + + // The certificate of the IDP + certificate, err := ioutil.ReadFile(strings.Replace(c.SAMLProviders[len(c.SAMLProviders)-1].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 metadat but we want to overwrite them + RouteSamlAcsWithSlash := RouteSamlAcs + 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 + RouteSamlMetadata) + 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 + + samlMiddleware = samlMiddleWare + + return nil +} + +func GetMiddleware() (*samlsp.Middleware, error) { + if samlMiddleware == nil { + return nil, errors.Errorf("The MiddleWare for SAML is null (Probably due to a backward step)") + } + return samlMiddleware, 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 +} diff --git a/selfservice/flow/saml/helpertest/helpertest.go b/selfservice/flow/saml/helpertest/helpertest.go new file mode 100644 index 000000000000..21ad51f92b42 --- /dev/null +++ b/selfservice/flow/saml/helpertest/helpertest.go @@ -0,0 +1,203 @@ +package helpertest + +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" + samlhandler "github.com/ory/kratos/selfservice/flow/saml" + samlstrategy "github.com/ory/kratos/selfservice/strategy/saml" + samlstrat "github.com/ory/kratos/selfservice/strategy/saml/strategy" + "github.com/ory/kratos/x" +) + +var TimeNow = func() time.Time { return time.Now().UTC() } +var RandReader = rand.Reader + +func NewSAMLProvider( + t *testing.T, + kratos *httptest.Server, + id, label string, +) samlstrategy.Configuration { + + return samlstrategy.Configuration{ + ID: id, + Label: label, + PublicCertPath: "secret", + PrivateKeyPath: "/", + Mapper: "file://./stub/oidc.hydra.jsonnet", + //IDPMetadataURL: "", + //IDPSSOURL: "", + } +} + +func ViperSetProviderConfig(t *testing.T, conf *config.Config, SAMLProvider ...samlstrategy.Configuration) { + conf.MustSet(context.Background(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeSAML)+".config", &samlstrategy.ConfigurationCollection{SAMLProviders: SAMLProvider}) + conf.MustSet(context.Background(), config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeSAML)+".enabled", true) +} + +func NewClient(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 InitMiddleware(t *testing.T, idpInformation map[string]string) (*samlsp.Middleware, *samlstrat.Strategy, *httptest.Server, error) { + conf, reg := internal.NewFastRegistryWithMocks(t) + + strategy := samlstrat.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, + NewSAMLProvider(t, ts, "samlProviderTestID", "samlProviderTestLabel"), + samlstrategy.Configuration{ + ID: "samlProviderTestID", + Label: "samlProviderTestLabel", + 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) + + // Instantiates the MiddleWare + _, err := NewClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata") + require.NoError(t, err) + middleware, err := samlhandler.GetMiddleware() + 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 InitMiddlewareWithMetadata(t *testing.T, metadataURL string) (*samlsp.Middleware, *samlstrat.Strategy, *httptest.Server, error) { + idpInformation := make(map[string]string) + idpInformation["idp_metadata_url"] = metadataURL + + return InitMiddleware(t, idpInformation) +} + +func InitMiddlewareWithoutMetadata(t *testing.T, idpSsoUrl string, idpEntityId string, + idpCertifiatePath string, idpLogoutUrl string) (*samlsp.Middleware, *samlstrat.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 InitMiddleware(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/flow/saml/test/handler_test.go b/selfservice/flow/saml/test/handler_test.go new file mode 100644 index 000000000000..0af00023e473 --- /dev/null +++ b/selfservice/flow/saml/test/handler_test.go @@ -0,0 +1,92 @@ +package saml_test + +import ( + "io/ioutil" + "testing" + + samlhandler "github.com/ory/kratos/selfservice/flow/saml" + "github.com/stretchr/testify/require" + + helpertest "github.com/ory/kratos/selfservice/flow/saml/helpertest" + "gotest.tools/assert" +) + +func TestInitMiddleWareWithMetadata(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + middleWare, _, _, err := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.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") + assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://idp.testshib.org/idp/shibboleth") +} + +func TestInitMiddleWareWithoutMetadata(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + middleWare, _, _, err := helpertest.InitMiddlewareWithoutMetadata(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") + assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://samltest.id/saml/idp") +} + +func TestGetMiddleware(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + middleWare, err := samlhandler.GetMiddleware() + + 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") + assert.Check(t, middleWare.ServiceProvider.IDPMetadata.EntityID == "https://idp.testshib.org/idp/shibboleth") +} + +func TestMustParseCertificate(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + certificate, err := ioutil.ReadFile("testdata/samlkratos.crt") + require.NoError(t, err) + + cert, err := samlhandler.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/flow/saml/test/metadata_test.go b/selfservice/flow/saml/test/metadata_test.go new file mode 100644 index 000000000000..7545034d7157 --- /dev/null +++ b/selfservice/flow/saml/test/metadata_test.go @@ -0,0 +1,127 @@ +package saml_test + +import ( + "encoding/xml" + "io" + "io/ioutil" + "net/http" + "reflect" + "testing" + + samlhandler "github.com/ory/kratos/selfservice/flow/saml" + helpertest "github.com/ory/kratos/selfservice/flow/saml/helpertest" + + samltesthelpers "github.com/ory/kratos/selfservice/flow/saml/helpertest" + + "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() + } + + samlhandler.DestroyMiddlewareIfExists() + + _, _, ts, err := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + assert.NilError(t, err) + res, _ := samltesthelpers.NewClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata") + + assert.Check(t, is.Equal(http.StatusOK, res.StatusCode)) + assert.Check(t, is.Equal("application/samlmetadata+xml", + res.Header.Get("Content-type"))) +} + +func TestXmlMetadataValues(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + _, _, ts, err := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + res, _ := samltesthelpers.NewClient(t, nil).Get(ts.URL + "/self-service/methods/saml/metadata") + body, _ := io.ReadAll(res.Body) + + assert.Check(t, is.Equal(http.StatusOK, res.StatusCode)) + assert.Check(t, is.Equal("application/samlmetadata+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/flow/saml/test/testdata/cert.pem b/selfservice/flow/saml/test/testdata/cert.pem new file mode 100644 index 000000000000..52667ef39ff2 --- /dev/null +++ b/selfservice/flow/saml/test/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/flow/saml/test/testdata/expected_metadata.xml b/selfservice/flow/saml/test/testdata/expected_metadata.xml new file mode 100644 index 000000000000..05039766f75f --- /dev/null +++ b/selfservice/flow/saml/test/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/flow/saml/test/testdata/idp_saml_metadata.xml b/selfservice/flow/saml/test/testdata/idp_saml_metadata.xml new file mode 100644 index 000000000000..dcaf7f051dae --- /dev/null +++ b/selfservice/flow/saml/test/testdata/idp_saml_metadata.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/flow/saml/test/testdata/key.pem b/selfservice/flow/saml/test/testdata/key.pem new file mode 100644 index 000000000000..48284dac33a1 --- /dev/null +++ b/selfservice/flow/saml/test/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/flow/saml/test/testdata/myservice.cert b/selfservice/flow/saml/test/testdata/myservice.cert new file mode 100755 index 000000000000..a815f8f44742 --- /dev/null +++ b/selfservice/flow/saml/test/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/flow/saml/test/testdata/myservice.key b/selfservice/flow/saml/test/testdata/myservice.key new file mode 100755 index 000000000000..e7b461f2f228 --- /dev/null +++ b/selfservice/flow/saml/test/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/flow/saml/test/testdata/registration.schema.json b/selfservice/flow/saml/test/testdata/registration.schema.json new file mode 100644 index 000000000000..c7005d87ce8d --- /dev/null +++ b/selfservice/flow/saml/test/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/flow/saml/test/testdata/saml.jsonnet b/selfservice/flow/saml/test/testdata/saml.jsonnet new file mode 100644 index 000000000000..87103e26bc6b --- /dev/null +++ b/selfservice/flow/saml/test/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/flow/saml/test/testdata/samlkratos.crt b/selfservice/flow/saml/test/testdata/samlkratos.crt new file mode 100755 index 000000000000..3dfdeb703e1c --- /dev/null +++ b/selfservice/flow/saml/test/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/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/provider.go b/selfservice/strategy/saml/provider.go new file mode 100644 index 000000000000..8be02aa28cb7 --- /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) (*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..6020ccef2b15 --- /dev/null +++ b/selfservice/strategy/saml/provider_config.go @@ -0,0 +1,33 @@ +package saml + +type Configuration struct { + // ID is the provider's ID + ID string `json:"id"` + + // 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, label string) (Provider, error) { + return NewProviderSAML(id, label, &c.SAMLProviders[len(c.SAMLProviders)-1]), nil +} diff --git a/selfservice/strategy/saml/provider_saml.go b/selfservice/strategy/saml/provider_saml.go new file mode 100644 index 000000000000..43f7e0cd9e4a --- /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 { + id string + label string + config *Configuration +} + +func NewProviderSAML( + id string, + label string, + config *Configuration, +) *ProviderSAML { + return &ProviderSAML{ + id: id, + label: label, + config: config, + } +} + +// Translate attributes from saml asseryion into kratos claims +func (d *ProviderSAML) Claims(ctx context.Context, config *config.Config, attributeSAML samlsp.Attributes) (*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)) + } + + providerSAML := c.SAMLProviders[len(c.SAMLProviders)-1] + + claims := &Claims{ + Issuer: "saml", + Subject: attributeSAML.Get(providerSAML.AttributesMap["id"]), + Name: attributeSAML.Get(providerSAML.AttributesMap["firstname"]), + LastName: attributeSAML.Get(providerSAML.AttributesMap["lastname"]), + Nickname: attributeSAML.Get(providerSAML.AttributesMap["nickname"]), + Gender: attributeSAML.Get(providerSAML.AttributesMap["gender"]), + Birthdate: attributeSAML.Get(providerSAML.AttributesMap["birthdate"]), + Picture: attributeSAML.Get(providerSAML.AttributesMap["picture"]), + Email: attributeSAML.Get(providerSAML.AttributesMap["email"]), + Roles: attributeSAML[providerSAML.AttributesMap["roles"]], + Groups: attributeSAML[providerSAML.AttributesMap["groups"]], + PhoneNumber: attributeSAML.Get(providerSAML.AttributesMap["phone_number"]), + EmailVerified: true, + } + + return claims, nil +} + +func (d *ProviderSAML) Config() *Configuration { + return d.config +} diff --git a/selfservice/strategy/saml/strategy/.schema/link.schema.json b/selfservice/strategy/saml/strategy/.schema/link.schema.json new file mode 100644 index 000000000000..95c72b83ed18 --- /dev/null +++ b/selfservice/strategy/saml/strategy/.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/strategy/.schema/settings.schema.json b/selfservice/strategy/saml/strategy/.schema/settings.schema.json new file mode 100644 index 000000000000..3cac10cea831 --- /dev/null +++ b/selfservice/strategy/saml/strategy/.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/strategy/const.go b/selfservice/strategy/saml/strategy/const.go new file mode 100644 index 000000000000..e578913b1427 --- /dev/null +++ b/selfservice/strategy/saml/strategy/const.go @@ -0,0 +1,5 @@ +package strategy + +const ( + sessionName = "ory_kratos_saml_auth_code_session" +) diff --git a/selfservice/strategy/saml/strategy/schema.go b/selfservice/strategy/saml/strategy/schema.go new file mode 100644 index 000000000000..92ac527c5d00 --- /dev/null +++ b/selfservice/strategy/saml/strategy/schema.go @@ -0,0 +1,8 @@ +package strategy + +import ( + _ "embed" +) + +//go:embed .schema/link.schema.json +var linkSchema []byte diff --git a/selfservice/strategy/saml/strategy/strategy.go b/selfservice/strategy/saml/strategy/strategy.go new file mode 100644 index 000000000000..937d5af9168a --- /dev/null +++ b/selfservice/strategy/saml/strategy/strategy.go @@ -0,0 +1,471 @@ +package strategy + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "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" + samlflow "github.com/ory/kratos/selfservice/flow/saml" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/strategy" + samlstrategy "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/kratos/session" + "github.com/ory/kratos/x" +) + +const ( + RouteBase = "/self-service/methods/saml" + + RouteAcs = RouteBase + "/acs" + RouteAuth = RouteBase + "/auth" +) + +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 uid(provider, subject string) string { + return fmt.Sprintf("%s:%s", provider, subject) +} + +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, samlstrategy.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, samlstrategy.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, samlstrategy.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 +} + +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 _, ok := req.(*settings.Flow); ok { + // ignore this if it's a settings flow + } else 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 | Receive SAML response, parse the attributes and start auth flow +func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + + 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)) + // TODO add flow param to s.handleError + // s.forwardError(w, r, req, 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 := samlflow.GetMiddleware() + 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()) + 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) + 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 +func (s *Strategy) Provider(ctx context.Context) (samlstrategy.Provider, error) { + c, err := s.Config(ctx) + if err != nil { + return nil, err + } + + provider, err := c.Provider(c.SAMLProviders[len(c.SAMLProviders)-1].ID, c.SAMLProviders[len(c.SAMLProviders)-1].Label) + 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) (*samlstrategy.ConfigurationCollection, error) { + var c samlstrategy.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 CredentialsConfig + 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.CredentialsOIDC + 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/strategy_auth.go b/selfservice/strategy/saml/strategy/strategy_auth.go new file mode 100644 index 000000000000..b04bea371291 --- /dev/null +++ b/selfservice/strategy/saml/strategy/strategy_auth.go @@ -0,0 +1,66 @@ +package strategy + +import ( + "errors" + "net/http" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + samlsp "github.com/ory/kratos/selfservice/strategy/saml" + "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 samlsp.Provider, claims *samlsp.Claims) (*flow.Flow, error) { + + // This is a check to see if the user exists in the database + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeSAML, uid(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/strategy_login.go b/selfservice/strategy/saml/strategy/strategy_login.go new file mode 100644 index 000000000000..c8d7941a7123 --- /dev/null +++ b/selfservice/strategy/saml/strategy/strategy_login.go @@ -0,0 +1,140 @@ +package strategy + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/pkg/errors" + + "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" + handler "github.com/ory/kratos/selfservice/flow/saml" + samlsp "github.com/ory/kratos/selfservice/strategy/saml" + "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 samlsp.Provider, c *identity.Credentials, i *identity.Identity, claims *samlsp.Claims) (*registration.Flow, error) { + + var o CredentialsConfig + 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(handler.RouteSamlLoginInit)) + } else { + + http.Redirect(w, r, handler.RouteSamlLoginInit, 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) +} diff --git a/selfservice/strategy/saml/strategy/strategy_registration.go b/selfservice/strategy/saml/strategy/strategy_registration.go new file mode 100644 index 000000000000..5a3b52ceea00 --- /dev/null +++ b/selfservice/strategy/saml/strategy/strategy_registration.go @@ -0,0 +1,164 @@ +package strategy + +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" + samlsp "github.com/ory/kratos/selfservice/strategy/saml" + "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 samlsp.Provider, claims *samlsp.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 := NewCredentialsForSAML(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 samlsp.Provider, claims *samlsp.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/strategy_test.go b/selfservice/strategy/saml/strategy/test/strategy_test.go new file mode 100644 index 000000000000..cd1ee0128875 --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/strategy_test.go @@ -0,0 +1,191 @@ +package strategy_test + +import ( + "bytes" + "context" + "encoding/json" + "regexp" + "testing" + + "github.com/ory/kratos/identity" + samlhandler "github.com/ory/kratos/selfservice/flow/saml" + helpertest "github.com/ory/kratos/selfservice/flow/saml/helpertest" + samlstrategy "github.com/ory/kratos/selfservice/strategy/saml/strategy" + "github.com/stretchr/testify/require" + + "gotest.tools/assert" + gotest "gotest.tools/assert" + is "gotest.tools/assert/cmp" +) + +func TestGetAndDecryptAssertion(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + middleware, _, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + assertion, err := helpertest.GetAndDecryptAssertion(t, "./testdata/SP_SamlResponse.xml", middleware.ServiceProvider.Key) + + require.NoError(t, err) + assert.Check(t, assertion != nil) +} + +func TestGetAttributesFromAssertion(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + middleware, strategy, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + assertion, _ := helpertest.GetAndDecryptAssertion(t, "./testdata/SP_SamlResponse.xml", middleware.ServiceProvider.Key) + + mapAttributes, err := strategy.GetAttributesFromAssertion(assertion) + + require.NoError(t, err) + assert.Check(t, mapAttributes["urn:oid:0.9.2342.19200300.100.1.1"][0] == "myself") + assert.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.1"][0] == "Member") + assert.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.1"][1] == "Staff") + assert.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.6"][0] == "myself@testshib.org") + assert.Check(t, mapAttributes["urn:oid:2.5.4.4"][0] == "And I") + assert.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.9"][0] == "Member@testshib.org") + assert.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.9"][1] == "Staff@testshib.org") + assert.Check(t, mapAttributes["urn:oid:2.5.4.42"][0] == "Me Myself") + assert.Check(t, mapAttributes["urn:oid:1.3.6.1.4.1.5923.1.1.1.7"][0] == "urn:mace:dir:entitlement:common-lib-terms") + assert.Check(t, mapAttributes["urn:oid:2.5.4.3"][0] == "Me Myself And I") + assert.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() + } + + samlhandler.DestroyMiddlewareIfExists() + + middleware, _, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.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() + } + + samlhandler.DestroyMiddlewareIfExists() + + _, strategy, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + provider, err := strategy.Provider(context.Background()) + require.NoError(t, err) + gotest.Check(t, provider != nil) + gotest.Check(t, provider.Config().ID == "samlProviderTestID") + gotest.Check(t, provider.Config().Label == "samlProviderTestLabel") +} + +func TestConfig(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + _, strategy, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + config, err := strategy.Config(context.Background()) + require.NoError(t, err) + gotest.Check(t, config != nil) + gotest.Check(t, len(config.SAMLProviders) == 2) + gotest.Check(t, config.SAMLProviders[0].ID == "samlProviderTestID") + gotest.Check(t, config.SAMLProviders[0].Label == "samlProviderTestLabel") +} + +func TestID(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + _, strategy, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + id := strategy.ID() + gotest.Check(t, id == "saml") +} + +func TestCountActiveCredentials(t *testing.T) { + if testing.Short() { + t.Skip() + } + + samlhandler.DestroyMiddlewareIfExists() + + _, strategy, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + mapCredentials := make(map[identity.CredentialsType]identity.Credentials) + + var b bytes.Buffer + err := json.NewEncoder(&b).Encode(samlstrategy.CredentialsConfig{ + Providers: []samlstrategy.ProviderCredentialsConfig{ + { + 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() + } + + samlhandler.DestroyMiddlewareIfExists() + + middleware, strategy, _, _ := helpertest.InitMiddlewareWithMetadata(t, + "file://testdata/idp_saml_metadata.xml") + + provider, _ := strategy.Provider(context.Background()) + assertion, _ := helpertest.GetAndDecryptAssertion(t, "./testdata/SP_SamlResponse.xml", middleware.ServiceProvider.Key) + attributes, _ := strategy.GetAttributesFromAssertion(assertion) + claims, _ := provider.Claims(context.Background(), strategy.D().Config(), attributes) + + i, err := strategy.GetRegistrationIdentity(nil, context.Background(), provider, claims, false) + require.NoError(t, err) + gotest.Check(t, i != nil) +} diff --git a/selfservice/strategy/saml/strategy/test/testdata/SP_IDPMetadata.xml b/selfservice/strategy/saml/strategy/test/testdata/SP_IDPMetadata.xml new file mode 100644 index 000000000000..dcaf7f051dae --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/SP_SamlResponse.xml b/selfservice/strategy/saml/strategy/test/testdata/SP_SamlResponse.xml new file mode 100644 index 000000000000..f5dbc10766ab --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/TestSPCanHandleOneloginResponse_response b/selfservice/strategy/saml/strategy/test/testdata/TestSPCanHandleOneloginResponse_response new file mode 100644 index 000000000000..1031d30218df --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/cert.pem b/selfservice/strategy/saml/strategy/test/testdata/cert.pem new file mode 100644 index 000000000000..52667ef39ff2 --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/idp_saml_metadata.xml b/selfservice/strategy/saml/strategy/test/testdata/idp_saml_metadata.xml new file mode 100644 index 000000000000..dcaf7f051dae --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/testdata/idp_saml_metadata.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/strategy/test/testdata/key.pem b/selfservice/strategy/saml/strategy/test/testdata/key.pem new file mode 100644 index 000000000000..48284dac33a1 --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/myservice.cert b/selfservice/strategy/saml/strategy/test/testdata/myservice.cert new file mode 100755 index 000000000000..a815f8f44742 --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/myservice.key b/selfservice/strategy/saml/strategy/test/testdata/myservice.key new file mode 100755 index 000000000000..e7b461f2f228 --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/registration.schema.json b/selfservice/strategy/saml/strategy/test/testdata/registration.schema.json new file mode 100644 index 000000000000..c7005d87ce8d --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/saml.jsonnet b/selfservice/strategy/saml/strategy/test/testdata/saml.jsonnet new file mode 100644 index 000000000000..87103e26bc6b --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/test/testdata/saml_response.xml b/selfservice/strategy/saml/strategy/test/testdata/saml_response.xml new file mode 100644 index 000000000000..22ea0920a014 --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/testdata/saml_response.xml @@ -0,0 +1,11 @@ +https://idp.testshib.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 \ No newline at end of file diff --git a/selfservice/strategy/saml/strategy/test/testdata/samlkratos.crt b/selfservice/strategy/saml/strategy/test/testdata/samlkratos.crt new file mode 100755 index 000000000000..3dfdeb703e1c --- /dev/null +++ b/selfservice/strategy/saml/strategy/test/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/strategy/types.go b/selfservice/strategy/saml/strategy/types.go new file mode 100644 index 000000000000..60db543da1a8 --- /dev/null +++ b/selfservice/strategy/saml/strategy/types.go @@ -0,0 +1,63 @@ +package strategy + +import ( + "bytes" + "encoding/json" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/strategy/saml" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/stringsx" + + "github.com/pkg/errors" +) + +type CredentialsConfig struct { + Providers []ProviderCredentialsConfig `json:"providers"` +} + +// Create an uniq identifier for user in database. Its look like "id + the id of the saml provider" +func NewCredentialsForSAML(subject string, provider string) (*identity.Credentials, error) { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(CredentialsConfig{ + Providers: []ProviderCredentialsConfig{ + { + Subject: subject, + Provider: provider, + }}, + }); err != nil { + return nil, errors.WithStack(x.PseudoPanic. + WithDebugf("Unable to encode password options to JSON: %s", err)) + } + + return &identity.Credentials{ + Type: identity.CredentialsTypeSAML, + Identifiers: []string{uid(provider, subject)}, + Config: b.Bytes(), + }, nil +} + +func AddProviders(c *container.Container, providers []saml.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), + ) +} + +type ProviderCredentialsConfig struct { + Subject string `json:"subject"` + Provider string `json:"samlProvider"` +} + +type FlowMethod struct { + *container.Container +} diff --git a/spec/api.json b/spec/api.json index 857e8714c219..958fd8402dcc 100755 --- a/spec/api.json +++ b/spec/api.json @@ -1036,6 +1036,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" + }, "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)", "properties": { @@ -3810,6 +3827,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 d0f5cdc3275e..13e31871e4d6 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1265,6 +1265,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).", @@ -3463,6 +3497,23 @@ } } }, + "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", diff --git a/ui/node/node.go b/ui/node/node.go index 4856f6d8e9e7..36b87fb0119b 100644 --- a/ui/node/node.go +++ b/ui/node/node.go @@ -39,6 +39,7 @@ const ( DefaultGroup UiNodeGroup = "default" PasswordGroup UiNodeGroup = "password" OpenIDConnectGroup UiNodeGroup = "oidc" + SAMLGroup UiNodeGroup = "saml" ProfileGroup UiNodeGroup = "profile" LinkGroup UiNodeGroup = "link" TOTPGroup UiNodeGroup = "totp" diff --git a/x/provider.go b/x/provider.go index d6311c54adf3..dd54485e9ebf 100644 --- a/x/provider.go +++ b/x/provider.go @@ -26,6 +26,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) +}