Skip to content

Commit

Permalink
feat(saml): saml 2.0 implementation
Browse files Browse the repository at this point in the history
Signed-off-by: ThibaultHerard <[email protected]>

Co-authored-by: sebferrer <[email protected]>
Co-authored-by: psauvage <[email protected]>
Co-authored-by: alexGNX <[email protected]>
Co-authored-by: Stoakes <[email protected]>
  • Loading branch information
5 people committed Aug 12, 2022
1 parent c589520 commit 8f81269
Show file tree
Hide file tree
Showing 67 changed files with 3,853 additions and 5 deletions.
75 changes: 75 additions & 0 deletions .schema/api.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down
75 changes: 75 additions & 0 deletions .schema/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down
4 changes: 4 additions & 0 deletions continuity/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
151 changes: 151 additions & 0 deletions continuity/manager_relaystate.go
Original file line number Diff line number Diff line change
@@ -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").WithReasonf("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
}
2 changes: 1 addition & 1 deletion contrib/quickstart/kratos/email-password/identity.schema.json
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@
"additionalProperties": false
}
}
}
}
2 changes: 1 addition & 1 deletion contrib/quickstart/kratos/email-password/kratos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ identity:

courier:
smtp:
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true
11 changes: 11 additions & 0 deletions driver/registery_default_saml.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 8f81269

Please sign in to comment.