mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 16:27:23 +00:00
feat: add saml request to link to sessions (#9001)
# Which Problems Are Solved It is currently not possible to use SAML with the Session API. # How the Problems Are Solved Add SAML service, to get and resolve SAML requests. Add SAML session and SAML request aggregate, which can be linked to the Session to get back a SAMLResponse from the API directly. # Additional Changes Update of dependency zitadel/saml to provide all functionality for handling of SAML requests and responses. # Additional Context Closes #6053 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
parent
50d2b26a28
commit
c3b97a91a2
@ -563,6 +563,7 @@ OIDC:
|
|||||||
DefaultBackChannelLogoutLifetime: 15m # ZITADEL_OIDC_DEFAULTBACKCHANNELLOGOUTLIFETIME
|
DefaultBackChannelLogoutLifetime: 15m # ZITADEL_OIDC_DEFAULTBACKCHANNELLOGOUTLIFETIME
|
||||||
|
|
||||||
SAML:
|
SAML:
|
||||||
|
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
|
||||||
ProviderConfig:
|
ProviderConfig:
|
||||||
MetadataConfig:
|
MetadataConfig:
|
||||||
Path: "/metadata" # ZITADEL_SAML_PROVIDERCONFIG_METADATACONFIG_PATH
|
Path: "/metadata" # ZITADEL_SAML_PROVIDERCONFIG_METADATACONFIG_PATH
|
||||||
|
@ -49,6 +49,7 @@ import (
|
|||||||
user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha"
|
user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha"
|
||||||
userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha"
|
userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3"
|
"github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3"
|
||||||
|
saml_v2 "github.com/zitadel/zitadel/internal/api/grpc/saml/v2"
|
||||||
session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
||||||
session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
|
session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
|
||||||
settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
||||||
@ -530,7 +531,7 @@ func startAPIs(
|
|||||||
store,
|
store,
|
||||||
consolePath,
|
consolePath,
|
||||||
oidcServer.AuthCallbackURL(),
|
oidcServer.AuthCallbackURL(),
|
||||||
provider.AuthCallbackURL(samlProvider),
|
samlProvider.AuthCallbackURL(),
|
||||||
config.ExternalSecure,
|
config.ExternalSecure,
|
||||||
userAgentInterceptor,
|
userAgentInterceptor,
|
||||||
op.NewIssuerInterceptor(oidcServer.IssuerFromRequest).Handler,
|
op.NewIssuerInterceptor(oidcServer.IssuerFromRequest).Handler,
|
||||||
@ -555,6 +556,10 @@ func startAPIs(
|
|||||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// After SAML provider so that the callback endpoint can be used
|
||||||
|
if err := apis.RegisterService(ctx, saml_v2.CreateServer(commands, queries, samlProvider, config.ExternalSecure)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes
|
// handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes
|
||||||
apis.RouteGRPC()
|
apis.RouteGRPC()
|
||||||
return apis, nil
|
return apis, nil
|
||||||
|
@ -142,12 +142,27 @@ curl --request POST \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
<!--
|
|
||||||
### SAML session
|
### SAML session
|
||||||
|
|
||||||
TODO: https://github.com/zitadel/zitadel/issues/6053
|
The following example shows you how you could use the events search to get all events where a user has authenticated using SAML.
|
||||||
|
|
||||||
-->
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url $CUSTOM-DOMAIN/admin/v1/events/_search \
|
||||||
|
--header "Authorization: Bearer $TOKEN" \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"asc": true,
|
||||||
|
"limit": 1000,
|
||||||
|
"eventTypes": [
|
||||||
|
"saml_session.added",
|
||||||
|
"saml_session.saml_response.added"
|
||||||
|
],
|
||||||
|
"aggregateTypes": [
|
||||||
|
"saml_session"
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Example: Get failed login attempt
|
## Example: Get failed login attempt
|
||||||
|
2
go.mod
2
go.mod
@ -67,7 +67,7 @@ require (
|
|||||||
github.com/zitadel/logging v0.6.1
|
github.com/zitadel/logging v0.6.1
|
||||||
github.com/zitadel/oidc/v3 v3.32.0
|
github.com/zitadel/oidc/v3 v3.32.0
|
||||||
github.com/zitadel/passwap v0.6.0
|
github.com/zitadel/passwap v0.6.0
|
||||||
github.com/zitadel/saml v0.2.0
|
github.com/zitadel/saml v0.3.3
|
||||||
github.com/zitadel/schema v1.3.0
|
github.com/zitadel/schema v1.3.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0
|
||||||
|
4
go.sum
4
go.sum
@ -743,8 +743,8 @@ github.com/zitadel/oidc/v3 v3.32.0 h1:Mw0EPZRC6h+OXAuT0Uk2BZIjJQNHLqUpaJCm6c3IBy
|
|||||||
github.com/zitadel/oidc/v3 v3.32.0/go.mod h1:DyE/XClysRK/ozFaZSqlYamKVnTh4l6Ln25ihSNI03w=
|
github.com/zitadel/oidc/v3 v3.32.0/go.mod h1:DyE/XClysRK/ozFaZSqlYamKVnTh4l6Ln25ihSNI03w=
|
||||||
github.com/zitadel/passwap v0.6.0 h1:m9F3epFC0VkBXu25rihSLGyHvWiNlCzU5kk8RoI+SXQ=
|
github.com/zitadel/passwap v0.6.0 h1:m9F3epFC0VkBXu25rihSLGyHvWiNlCzU5kk8RoI+SXQ=
|
||||||
github.com/zitadel/passwap v0.6.0/go.mod h1:kqAiJ4I4eZvm3Y6oAk6hlEqlZZOkjMHraGXF90GG7LI=
|
github.com/zitadel/passwap v0.6.0/go.mod h1:kqAiJ4I4eZvm3Y6oAk6hlEqlZZOkjMHraGXF90GG7LI=
|
||||||
github.com/zitadel/saml v0.2.0 h1:vv7r+Xz43eAPCb+fImMaospD+TWRZQDkb78AbSJRcL4=
|
github.com/zitadel/saml v0.3.3 h1:Cn+1ZNeWlzMM7wxUxJfgNjXSW+Yu6UD4zWbpySA5GQM=
|
||||||
github.com/zitadel/saml v0.2.0/go.mod h1:QqKcguOt7mMVI6tkEfpkyzwnYRdlmn3kYQj3VTPUw1g=
|
github.com/zitadel/saml v0.3.3/go.mod h1:QqKcguOt7mMVI6tkEfpkyzwnYRdlmn3kYQj3VTPUw1g=
|
||||||
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
|
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
|
||||||
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
|
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
367
internal/api/grpc/saml/v2/integration/saml_test.go
Normal file
367
internal/api/grpc/saml/v2/integration/saml_test.go
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package saml_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/crewjam/saml"
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
|
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||||
|
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CTX context.Context
|
||||||
|
Instance *integration.Instance
|
||||||
|
Client saml_pb.SAMLServiceClient
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
os.Exit(func() int {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
Instance = integration.NewInstance(ctx)
|
||||||
|
Client = Instance.Client.SAMLv2
|
||||||
|
|
||||||
|
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
|
||||||
|
return m.Run()
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_GetAuthRequest(t *testing.T) {
|
||||||
|
rootURL := "https://sp.example.com"
|
||||||
|
idpMetadata, err := Instance.GetSAMLIDPMetadata()
|
||||||
|
require.NoError(t, err)
|
||||||
|
spMiddlewareRedirect, err := integration.CreateSAMLSP(rootURL, idpMetadata, saml.HTTPRedirectBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
spMiddlewarePost, err := integration.CreateSAMLSP(rootURL, idpMetadata, saml.HTTPPostBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0]
|
||||||
|
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
|
||||||
|
|
||||||
|
project, err := Instance.CreateProject(CTX)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewareRedirect)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewarePost)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dep func() (string, error)
|
||||||
|
want *oidc_pb.GetAuthRequestResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Not found",
|
||||||
|
dep: func() (string, error) {
|
||||||
|
return "123", nil
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success, redirect binding",
|
||||||
|
dep: func() (string, error) {
|
||||||
|
return Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success, post binding",
|
||||||
|
dep: func() (string, error) {
|
||||||
|
return Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
authRequestID, err := tt.dep()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||||
|
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||||
|
got, err := Client.GetSAMLRequest(CTX, &saml_pb.GetSAMLRequestRequest{
|
||||||
|
SamlRequestId: authRequestID,
|
||||||
|
})
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(ttt, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(ttt, err)
|
||||||
|
authRequest := got.GetSamlRequest()
|
||||||
|
assert.NotNil(ttt, authRequest)
|
||||||
|
assert.Equal(ttt, authRequestID, authRequest.GetId())
|
||||||
|
assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second))
|
||||||
|
}, retryDuration, tick, "timeout waiting for expected saml request result")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_CreateResponse(t *testing.T) {
|
||||||
|
idpMetadata, err := Instance.GetSAMLIDPMetadata()
|
||||||
|
require.NoError(t, err)
|
||||||
|
rootURLRedirect := "spredirect.example.com"
|
||||||
|
spMiddlewareRedirect, err := integration.CreateSAMLSP("https://"+rootURLRedirect, idpMetadata, saml.HTTPRedirectBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rootURLPost := "sppost.example.com"
|
||||||
|
spMiddlewarePost, err := integration.CreateSAMLSP("https://"+rootURLPost, idpMetadata, saml.HTTPPostBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0]
|
||||||
|
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
|
||||||
|
|
||||||
|
project, err := Instance.CreateProject(CTX)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewareRedirect)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewarePost)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sessionResp, err := Instance.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
|
||||||
|
Checks: &session.Checks{
|
||||||
|
User: &session.CheckUser{
|
||||||
|
Search: &session.CheckUser_UserId{
|
||||||
|
UserId: Instance.Users[integration.UserTypeOrgOwner].ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req *saml_pb.CreateResponseRequest
|
||||||
|
AuthError string
|
||||||
|
want *saml_pb.CreateResponseResponse
|
||||||
|
wantURL *url.URL
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Not found",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: "123",
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||||
|
Session: &saml_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "session not found",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||||
|
Session: &saml_pb.Session{
|
||||||
|
SessionId: "foo",
|
||||||
|
SessionToken: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "session token invalid",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||||
|
Session: &saml_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail callback, post",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Error{
|
||||||
|
Error: &saml_pb.AuthorizationError{
|
||||||
|
Error: saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED,
|
||||||
|
ErrorDescription: gu.Ptr("nope"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &saml_pb.CreateResponseResponse{
|
||||||
|
Url: regexp.QuoteMeta(`https://` + rootURLPost + `/saml/acs`),
|
||||||
|
Binding: &saml_pb.CreateResponseResponse_Post{Post: &saml_pb.PostResponse{
|
||||||
|
RelayState: "notempty",
|
||||||
|
SamlResponse: "notempty",
|
||||||
|
}},
|
||||||
|
Details: &object.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail callback, post, already failed",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
Instance.FailSAMLAuthRequest(CTX, authRequestID, saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Error{
|
||||||
|
Error: &saml_pb.AuthorizationError{
|
||||||
|
Error: saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED,
|
||||||
|
ErrorDescription: gu.Ptr("nope"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fail callback, redirect",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Error{
|
||||||
|
Error: &saml_pb.AuthorizationError{
|
||||||
|
Error: saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED,
|
||||||
|
ErrorDescription: gu.Ptr("nope"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &saml_pb.CreateResponseResponse{
|
||||||
|
Url: `https:\/\/` + rootURLRedirect + `\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)`,
|
||||||
|
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
|
||||||
|
Details: &object.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "callback, redirect",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||||
|
Session: &saml_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &saml_pb.CreateResponseResponse{
|
||||||
|
Url: `https:\/\/` + rootURLRedirect + `\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
|
||||||
|
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
|
||||||
|
Details: &object.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "callback, post",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||||
|
Session: &saml_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &saml_pb.CreateResponseResponse{
|
||||||
|
Url: regexp.QuoteMeta(`https://` + rootURLPost + `/saml/acs`),
|
||||||
|
Binding: &saml_pb.CreateResponseResponse_Post{Post: &saml_pb.PostResponse{
|
||||||
|
RelayState: "notempty",
|
||||||
|
SamlResponse: "notempty",
|
||||||
|
}},
|
||||||
|
Details: &object.Details{
|
||||||
|
ChangeDate: timestamppb.Now(),
|
||||||
|
ResourceOwner: Instance.ID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "callback, post",
|
||||||
|
req: &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: func() string {
|
||||||
|
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||||
|
require.NoError(t, err)
|
||||||
|
Instance.SuccessfulSAMLAuthRequest(CTX, Instance.Users[integration.UserTypeOrgOwner].ID, authRequestID)
|
||||||
|
return authRequestID
|
||||||
|
}(),
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||||
|
Session: &saml_pb.Session{
|
||||||
|
SessionId: sessionResp.GetSessionId(),
|
||||||
|
SessionToken: sessionResp.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Client.CreateResponse(CTX, tt.req)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
integration.AssertDetails(t, tt.want, got)
|
||||||
|
if tt.want != nil {
|
||||||
|
assert.Regexp(t, regexp.MustCompile(tt.want.Url), got.GetUrl())
|
||||||
|
if tt.want.GetPost() != nil {
|
||||||
|
assert.NotEmpty(t, got.GetPost().GetRelayState())
|
||||||
|
assert.NotEmpty(t, got.GetPost().GetSamlResponse())
|
||||||
|
}
|
||||||
|
if tt.want.GetRedirect() != nil {
|
||||||
|
assert.NotNil(t, got.GetRedirect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
112
internal/api/grpc/saml/v2/saml.go
Normal file
112
internal/api/grpc/saml/v2/saml.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/saml"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) GetAuthRequest(ctx context.Context, req *saml_pb.GetSAMLRequestRequest) (*saml_pb.GetSAMLRequestResponse, error) {
|
||||||
|
authRequest, err := s.query.SamlRequestByID(ctx, true, req.GetSamlRequestId(), true)
|
||||||
|
if err != nil {
|
||||||
|
logging.WithError(err).Error("query samlRequest by ID")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &saml_pb.GetSAMLRequestResponse{
|
||||||
|
SamlRequest: samlRequestToPb(authRequest),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func samlRequestToPb(a *query.SamlRequest) *saml_pb.SAMLRequest {
|
||||||
|
return &saml_pb.SAMLRequest{
|
||||||
|
Id: a.ID,
|
||||||
|
CreationDate: timestamppb.New(a.CreationDate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) CreateResponse(ctx context.Context, req *saml_pb.CreateResponseRequest) (*saml_pb.CreateResponseResponse, error) {
|
||||||
|
switch v := req.GetResponseKind().(type) {
|
||||||
|
case *saml_pb.CreateResponseRequest_Error:
|
||||||
|
return s.failSAMLRequest(ctx, req.GetSamlRequestId(), v.Error)
|
||||||
|
case *saml_pb.CreateResponseRequest_Session:
|
||||||
|
return s.linkSessionToSAMLRequest(ctx, req.GetSamlRequestId(), v.Session)
|
||||||
|
default:
|
||||||
|
return nil, zerrors.ThrowUnimplementedf(nil, "SAMLv2-0Tfak3fBS0", "verification oneOf %T in method CreateResponse not implemented", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) failSAMLRequest(ctx context.Context, samlRequestID string, ae *saml_pb.AuthorizationError) (*saml_pb.CreateResponseResponse, error) {
|
||||||
|
details, aar, err := s.command.FailSAMLRequest(ctx, samlRequestID, errorReasonToDomain(ae.GetError()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar}
|
||||||
|
url, body, err := s.idp.CreateErrorResponse(authReq, errorReasonToDomain(ae.GetError()), ae.GetErrorDescription())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID string, session *saml_pb.Session) (*saml_pb.CreateResponseResponse, error) {
|
||||||
|
details, aar, err := s.command.LinkSessionToSAMLRequest(ctx, samlRequestID, session.GetSessionId(), session.GetSessionToken(), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar}
|
||||||
|
url, body, err := s.idp.CreateResponse(ctx, authReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCallbackResponseFromBinding(details *domain.ObjectDetails, url string, body string, relayState string) *saml_pb.CreateResponseResponse {
|
||||||
|
resp := &saml_pb.CreateResponseResponse{
|
||||||
|
Details: object.DomainToDetailsPb(details),
|
||||||
|
Url: url,
|
||||||
|
}
|
||||||
|
|
||||||
|
if body != "" {
|
||||||
|
resp.Binding = &saml_pb.CreateResponseResponse_Post{
|
||||||
|
Post: &saml_pb.PostResponse{
|
||||||
|
RelayState: relayState,
|
||||||
|
SamlResponse: body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp.Binding = &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}}
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorReasonToDomain(errorReason saml_pb.ErrorReason) domain.SAMLErrorReason {
|
||||||
|
switch errorReason {
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_UNSPECIFIED:
|
||||||
|
return domain.SAMLErrorReasonUnspecified
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_VERSION_MISSMATCH:
|
||||||
|
return domain.SAMLErrorReasonVersionMissmatch
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED:
|
||||||
|
return domain.SAMLErrorReasonAuthNFailed
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_INVALID_ATTR_NAME_OR_VALUE:
|
||||||
|
return domain.SAMLErrorReasonInvalidAttrNameOrValue
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_INVALID_NAMEID_POLICY:
|
||||||
|
return domain.SAMLErrorReasonInvalidNameIDPolicy
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED:
|
||||||
|
return domain.SAMLErrorReasonRequestDenied
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_REQUEST_UNSUPPORTED:
|
||||||
|
return domain.SAMLErrorReasonRequestUnsupported
|
||||||
|
case saml_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_BINDING:
|
||||||
|
return domain.SAMLErrorReasonUnsupportedBinding
|
||||||
|
default:
|
||||||
|
return domain.SAMLErrorReasonUnspecified
|
||||||
|
}
|
||||||
|
}
|
59
internal/api/grpc/saml/v2/server.go
Normal file
59
internal/api/grpc/saml/v2/server.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/saml"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ saml_pb.SAMLServiceServer = (*Server)(nil)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
saml_pb.UnimplementedSAMLServiceServer
|
||||||
|
command *command.Commands
|
||||||
|
query *query.Queries
|
||||||
|
|
||||||
|
idp *saml.Provider
|
||||||
|
externalSecure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct{}
|
||||||
|
|
||||||
|
func CreateServer(
|
||||||
|
command *command.Commands,
|
||||||
|
query *query.Queries,
|
||||||
|
idp *saml.Provider,
|
||||||
|
externalSecure bool,
|
||||||
|
) *Server {
|
||||||
|
return &Server{
|
||||||
|
command: command,
|
||||||
|
query: query,
|
||||||
|
idp: idp,
|
||||||
|
externalSecure: externalSecure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||||
|
saml_pb.RegisterSAMLServiceServer(grpcServer, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) AppName() string {
|
||||||
|
return saml_pb.SAMLService_ServiceDesc.ServiceName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) MethodPrefix() string {
|
||||||
|
return saml_pb.SAMLService_ServiceDesc.ServiceName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||||
|
return saml_pb.SAMLService_AuthMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||||
|
return saml_pb.RegisterSAMLServiceHandler
|
||||||
|
}
|
99
internal/api/saml/auth_request.go
Normal file
99
internal/api/saml/auth_request.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/zitadel/saml/pkg/provider"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/models"
|
||||||
|
"github.com/zitadel/saml/pkg/provider/xml"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Provider) CreateErrorResponse(authReq models.AuthRequestInt, reason domain.SAMLErrorReason, description string) (string, string, error) {
|
||||||
|
resp := &provider.Response{
|
||||||
|
ProtocolBinding: authReq.GetBindingType(),
|
||||||
|
RelayState: authReq.GetRelayState(),
|
||||||
|
AcsUrl: authReq.GetAccessConsumerServiceURL(),
|
||||||
|
RequestID: authReq.GetAuthRequestID(),
|
||||||
|
Issuer: authReq.GetDestination(),
|
||||||
|
Audience: authReq.GetIssuer(),
|
||||||
|
}
|
||||||
|
return createResponse(p.AuthCallbackErrorResponse(resp, domain.SAMLErrorReasonToString(reason), description), authReq.GetBindingType(), authReq.GetAccessConsumerServiceURL(), resp.RelayState, resp.SigAlg, resp.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) CreateResponse(ctx context.Context, authReq models.AuthRequestInt) (string, string, error) {
|
||||||
|
resp := &provider.Response{
|
||||||
|
ProtocolBinding: authReq.GetBindingType(),
|
||||||
|
RelayState: authReq.GetRelayState(),
|
||||||
|
AcsUrl: authReq.GetAccessConsumerServiceURL(),
|
||||||
|
RequestID: authReq.GetAuthRequestID(),
|
||||||
|
Issuer: authReq.GetDestination(),
|
||||||
|
Audience: authReq.GetIssuer(),
|
||||||
|
}
|
||||||
|
samlResponse, err := p.AuthCallbackResponse(ctx, authReq, resp)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.command.CreateSAMLSessionFromSAMLRequest(
|
||||||
|
setContextUserSystem(ctx),
|
||||||
|
authReq.GetID(),
|
||||||
|
samlComplianceChecker(),
|
||||||
|
samlResponse.Id,
|
||||||
|
p.Expiration(),
|
||||||
|
); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResponse(samlResponse, authReq.GetBindingType(), authReq.GetAccessConsumerServiceURL(), resp.RelayState, resp.SigAlg, resp.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createResponse(samlResponse interface{}, binding, acs, relayState, sigAlg, sig string) (string, string, error) {
|
||||||
|
respData, err := xml.Marshal(samlResponse)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch binding {
|
||||||
|
case provider.PostBinding:
|
||||||
|
return acs, base64.StdEncoding.EncodeToString(respData), nil
|
||||||
|
case provider.RedirectBinding:
|
||||||
|
respData, err := xml.DeflateAndBase64(respData)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(acs)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
values := parsed.Query()
|
||||||
|
values.Add("SAMLResponse", string(respData))
|
||||||
|
values.Add("RelayState", relayState)
|
||||||
|
values.Add("SigAlg", sigAlg)
|
||||||
|
values.Add("Signature", sig)
|
||||||
|
parsed.RawQuery = values.Encode()
|
||||||
|
return parsed.String(), "", nil
|
||||||
|
}
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setContextUserSystem(ctx context.Context) context.Context {
|
||||||
|
data := authz.CtxData{
|
||||||
|
UserID: "SYSTEM",
|
||||||
|
}
|
||||||
|
return authz.SetCtxData(ctx, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func samlComplianceChecker() command.SAMLRequestComplianceChecker {
|
||||||
|
return func(_ context.Context, samlReq *command.SAMLRequestWriteModel) error {
|
||||||
|
if err := samlReq.CheckAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
45
internal/api/saml/auth_request_converter_v2.go
Normal file
45
internal/api/saml/auth_request_converter_v2.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/saml/pkg/provider/models"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ models.AuthRequestInt = &AuthRequestV2{}
|
||||||
|
|
||||||
|
type AuthRequestV2 struct {
|
||||||
|
*command.CurrentSAMLRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequestV2) GetApplicationID() string {
|
||||||
|
return a.ApplicationID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequestV2) GetID() string {
|
||||||
|
return a.ID
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) GetRelayState() string {
|
||||||
|
return a.RelayState
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) GetAccessConsumerServiceURL() string {
|
||||||
|
return a.ACSURL
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) GetAuthRequestID() string {
|
||||||
|
return a.RequestID
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) GetBindingType() string {
|
||||||
|
return a.Binding
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) GetIssuer() string {
|
||||||
|
return a.Issuer
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) GetDestination() string {
|
||||||
|
return a.Destination
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) GetUserID() string {
|
||||||
|
return a.UserID
|
||||||
|
}
|
||||||
|
func (a *AuthRequestV2) Done() bool {
|
||||||
|
return a.UserID != "" && a.SessionID != ""
|
||||||
|
}
|
@ -24,7 +24,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ProviderConfig *provider.Config
|
ProviderConfig *provider.Config
|
||||||
|
DefaultLoginURLV2 string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
*provider.Provider
|
||||||
|
command *command.Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProvider(
|
func NewProvider(
|
||||||
@ -40,7 +46,7 @@ func NewProvider(
|
|||||||
instanceHandler,
|
instanceHandler,
|
||||||
userAgentCookie func(http.Handler) http.Handler,
|
userAgentCookie func(http.Handler) http.Handler,
|
||||||
accessHandler *middleware.AccessInterceptor,
|
accessHandler *middleware.AccessInterceptor,
|
||||||
) (*provider.Provider, error) {
|
) (*Provider, error) {
|
||||||
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||||
|
|
||||||
provStorage, err := newStorage(
|
provStorage, err := newStorage(
|
||||||
@ -51,6 +57,8 @@ func NewProvider(
|
|||||||
certEncAlg,
|
certEncAlg,
|
||||||
es,
|
es,
|
||||||
projections,
|
projections,
|
||||||
|
fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
||||||
|
conf.DefaultLoginURLV2,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -73,12 +81,19 @@ func NewProvider(
|
|||||||
options = append(options, provider.WithAllowInsecure())
|
options = append(options, provider.WithAllowInsecure())
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider.NewProvider(
|
p, err := provider.NewProvider(
|
||||||
provStorage,
|
provStorage,
|
||||||
HandlerPrefix,
|
HandlerPrefix,
|
||||||
conf.ProviderConfig,
|
conf.ProviderConfig,
|
||||||
options...,
|
options...,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Provider{
|
||||||
|
p,
|
||||||
|
command,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorage(
|
func newStorage(
|
||||||
@ -89,16 +104,19 @@ func newStorage(
|
|||||||
certEncAlg crypto.EncryptionAlgorithm,
|
certEncAlg crypto.EncryptionAlgorithm,
|
||||||
es *eventstore.Eventstore,
|
es *eventstore.Eventstore,
|
||||||
db *database.DB,
|
db *database.DB,
|
||||||
|
defaultLoginURL string,
|
||||||
|
defaultLoginURLV2 string,
|
||||||
) (*Storage, error) {
|
) (*Storage, error) {
|
||||||
return &Storage{
|
return &Storage{
|
||||||
encAlg: encAlg,
|
encAlg: encAlg,
|
||||||
certEncAlg: certEncAlg,
|
certEncAlg: certEncAlg,
|
||||||
locker: crdb.NewLocker(db.DB, locksTable, signingKey),
|
locker: crdb.NewLocker(db.DB, locksTable, signingKey),
|
||||||
eventstore: es,
|
eventstore: es,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
command: command,
|
command: command,
|
||||||
query: query,
|
query: query,
|
||||||
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
defaultLoginURL: defaultLoginURL,
|
||||||
|
defaultLoginURLv2: defaultLoginURLV2,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package saml
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
@ -16,6 +17,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/actions"
|
"github.com/zitadel/zitadel/internal/actions"
|
||||||
"github.com/zitadel/zitadel/internal/actions/object"
|
"github.com/zitadel/zitadel/internal/actions/object"
|
||||||
"github.com/zitadel/zitadel/internal/activity"
|
"github.com/zitadel/zitadel/internal/activity"
|
||||||
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||||
"github.com/zitadel/zitadel/internal/auth/repository"
|
"github.com/zitadel/zitadel/internal/auth/repository"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
@ -33,6 +35,10 @@ var _ provider.IdentityProviderStorage = &Storage{}
|
|||||||
var _ provider.AuthStorage = &Storage{}
|
var _ provider.AuthStorage = &Storage{}
|
||||||
var _ provider.UserStorage = &Storage{}
|
var _ provider.UserStorage = &Storage{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoginClientHeader = "x-zitadel-login-client"
|
||||||
|
)
|
||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
certChan <-chan interface{}
|
certChan <-chan interface{}
|
||||||
defaultCertificateLifetime time.Duration
|
defaultCertificateLifetime time.Duration
|
||||||
@ -51,7 +57,8 @@ type Storage struct {
|
|||||||
command *command.Commands
|
command *command.Commands
|
||||||
query *query.Queries
|
query *query.Queries
|
||||||
|
|
||||||
defaultLoginURL string
|
defaultLoginURL string
|
||||||
|
defaultLoginURLv2 string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) {
|
func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) {
|
||||||
@ -64,7 +71,12 @@ func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*servicep
|
|||||||
&serviceprovider.Config{
|
&serviceprovider.Config{
|
||||||
Metadata: app.SAMLConfig.Metadata,
|
Metadata: app.SAMLConfig.Metadata,
|
||||||
},
|
},
|
||||||
p.defaultLoginURL,
|
func(id string) string {
|
||||||
|
if strings.HasPrefix(id, command.IDPrefixV2) {
|
||||||
|
return p.defaultLoginURLv2 + id
|
||||||
|
}
|
||||||
|
return p.defaultLoginURL + id
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +107,38 @@ func (p *Storage) GetResponseSigningKey(ctx context.Context) (*key.CertificateAn
|
|||||||
func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) {
|
func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
headers, _ := http_utils.HeadersFromCtx(ctx)
|
||||||
|
if loginClient := headers.Get(LoginClientHeader); loginClient != "" {
|
||||||
|
return p.createAuthRequestLoginClient(ctx, req, acsUrl, protocolBinding, relayState, applicationID, loginClient)
|
||||||
|
}
|
||||||
|
return p.createAuthRequest(ctx, req, acsUrl, protocolBinding, relayState, applicationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) createAuthRequestLoginClient(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID, loginClient string) (_ models.AuthRequestInt, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
samlRequest := &command.SAMLRequest{
|
||||||
|
ApplicationID: applicationID,
|
||||||
|
ACSURL: acsUrl,
|
||||||
|
RelayState: relayState,
|
||||||
|
RequestID: req.Id,
|
||||||
|
Binding: protocolBinding,
|
||||||
|
Issuer: req.Issuer.Text,
|
||||||
|
Destination: req.Destination,
|
||||||
|
LoginClient: loginClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
aar, err := p.command.AddSAMLRequest(ctx, samlRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &AuthRequestV2{aar}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Storage) createAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-sd436", "no user agent id")
|
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-sd436", "no user agent id")
|
||||||
@ -113,6 +157,15 @@ func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequest
|
|||||||
func (p *Storage) AuthRequestByID(ctx context.Context, id string) (_ models.AuthRequestInt, err error) {
|
func (p *Storage) AuthRequestByID(ctx context.Context, id string) (_ models.AuthRequestInt, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
if strings.HasPrefix(id, command.IDPrefixV2) {
|
||||||
|
req, err := p.command.GetCurrentSAMLRequest(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &AuthRequestV2{req}, nil
|
||||||
|
}
|
||||||
|
|
||||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-D3g21", "no user agent id")
|
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-D3g21", "no user agent id")
|
||||||
|
161
internal/command/saml_request.go
Normal file
161
internal/command/saml_request.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SAMLRequest struct {
|
||||||
|
ID string
|
||||||
|
LoginClient string
|
||||||
|
|
||||||
|
ApplicationID string
|
||||||
|
ACSURL string
|
||||||
|
RelayState string
|
||||||
|
RequestID string
|
||||||
|
Binding string
|
||||||
|
Issuer string
|
||||||
|
Destination string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CurrentSAMLRequest struct {
|
||||||
|
*SAMLRequest
|
||||||
|
SessionID string
|
||||||
|
UserID string
|
||||||
|
AuthMethods []domain.UserAuthMethodType
|
||||||
|
AuthTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest) (_ *CurrentSAMLRequest, err error) {
|
||||||
|
id, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
samlRequest.ID = IDPrefixV2 + id
|
||||||
|
writeModel, err := c.getSAMLRequestWriteModel(ctx, samlRequest.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if writeModel.SAMLRequestState != domain.SAMLRequestStateUnspecified {
|
||||||
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-MO3vmsMLUt", "Errors.SAMLRequest.AlreadyExisting")
|
||||||
|
}
|
||||||
|
err = c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewAddedEvent(
|
||||||
|
ctx,
|
||||||
|
&samlrequest.NewAggregate(samlRequest.ID, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||||
|
samlRequest.LoginClient,
|
||||||
|
samlRequest.ApplicationID,
|
||||||
|
samlRequest.ACSURL,
|
||||||
|
samlRequest.RelayState,
|
||||||
|
samlRequest.RequestID,
|
||||||
|
samlRequest.Binding,
|
||||||
|
samlRequest.Issuer,
|
||||||
|
samlRequest.Destination,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool) (*domain.ObjectDetails, *CurrentSAMLRequest, error) {
|
||||||
|
writeModel, err := c.getSAMLRequestWriteModel(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if writeModel.SAMLRequestState == domain.SAMLRequestStateUnspecified {
|
||||||
|
return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-GH3PVLSfXC", "Errors.SAMLRequest.NotExisting")
|
||||||
|
}
|
||||||
|
if writeModel.SAMLRequestState != domain.SAMLRequestStateAdded {
|
||||||
|
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-ttPKNdAIFT", "Errors.SAMLRequest.AlreadyHandled")
|
||||||
|
}
|
||||||
|
if checkLoginClient && authz.GetCtxData(ctx).UserID != writeModel.LoginClient {
|
||||||
|
return nil, nil, zerrors.ThrowPermissionDenied(nil, "COMMAND-KCd48Rxt7x", "Errors.SAMLRequest.WrongLoginClient")
|
||||||
|
}
|
||||||
|
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
|
||||||
|
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err = sessionWriteModel.CheckIsActive(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewSessionLinkedEvent(
|
||||||
|
ctx, &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||||
|
sessionID,
|
||||||
|
sessionWriteModel.UserID,
|
||||||
|
sessionWriteModel.AuthenticationTime(),
|
||||||
|
sessionWriteModel.AuthMethodTypes(),
|
||||||
|
)); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return writeModelToObjectDetails(&writeModel.WriteModel), samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) FailSAMLRequest(ctx context.Context, id string, reason domain.SAMLErrorReason) (*domain.ObjectDetails, *CurrentSAMLRequest, error) {
|
||||||
|
writeModel, err := c.getSAMLRequestWriteModel(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if writeModel.SAMLRequestState != domain.SAMLRequestStateAdded {
|
||||||
|
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-32lGj1Fhjt", "Errors.SAMLRequest.AlreadyHandled")
|
||||||
|
}
|
||||||
|
err = c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewFailedEvent(
|
||||||
|
ctx,
|
||||||
|
&samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||||
|
reason,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return writeModelToObjectDetails(&writeModel.WriteModel), samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func samlRequestWriteModelToCurrentSAMLRequest(writeModel *SAMLRequestWriteModel) (_ *CurrentSAMLRequest) {
|
||||||
|
return &CurrentSAMLRequest{
|
||||||
|
SAMLRequest: &SAMLRequest{
|
||||||
|
ID: writeModel.AggregateID,
|
||||||
|
LoginClient: writeModel.LoginClient,
|
||||||
|
ApplicationID: writeModel.ApplicationID,
|
||||||
|
ACSURL: writeModel.ACSURL,
|
||||||
|
RelayState: writeModel.RelayState,
|
||||||
|
RequestID: writeModel.RequestID,
|
||||||
|
Binding: writeModel.Binding,
|
||||||
|
Issuer: writeModel.Issuer,
|
||||||
|
Destination: writeModel.Destination,
|
||||||
|
},
|
||||||
|
SessionID: writeModel.SessionID,
|
||||||
|
UserID: writeModel.UserID,
|
||||||
|
AuthMethods: writeModel.AuthMethods,
|
||||||
|
AuthTime: writeModel.AuthTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) GetCurrentSAMLRequest(ctx context.Context, id string) (_ *CurrentSAMLRequest, err error) {
|
||||||
|
wm, err := c.getSAMLRequestWriteModel(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return samlRequestWriteModelToCurrentSAMLRequest(wm), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) getSAMLRequestWriteModel(ctx context.Context, id string) (writeModel *SAMLRequestWriteModel, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
writeModel = NewSAMLRequestWriteModel(ctx, id)
|
||||||
|
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writeModel, nil
|
||||||
|
}
|
88
internal/command/saml_request_model.go
Normal file
88
internal/command/saml_request_model.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SAMLRequestWriteModel struct {
|
||||||
|
eventstore.WriteModel
|
||||||
|
aggregate *eventstore.Aggregate
|
||||||
|
|
||||||
|
LoginClient string
|
||||||
|
ApplicationID string
|
||||||
|
ACSURL string
|
||||||
|
RelayState string
|
||||||
|
RequestID string
|
||||||
|
Binding string
|
||||||
|
Issuer string
|
||||||
|
Destination string
|
||||||
|
|
||||||
|
SessionID string
|
||||||
|
UserID string
|
||||||
|
AuthTime time.Time
|
||||||
|
AuthMethods []domain.UserAuthMethodType
|
||||||
|
SAMLRequestState domain.SAMLRequestState
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSAMLRequestWriteModel(ctx context.Context, id string) *SAMLRequestWriteModel {
|
||||||
|
return &SAMLRequestWriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: id,
|
||||||
|
},
|
||||||
|
aggregate: &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SAMLRequestWriteModel) Reduce() error {
|
||||||
|
for _, event := range m.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *samlrequest.AddedEvent:
|
||||||
|
m.LoginClient = e.LoginClient
|
||||||
|
m.ApplicationID = e.ApplicationID
|
||||||
|
m.ACSURL = e.ACSURL
|
||||||
|
m.RelayState = e.RelayState
|
||||||
|
m.RequestID = e.RequestID
|
||||||
|
m.Binding = e.Binding
|
||||||
|
m.Issuer = e.Issuer
|
||||||
|
m.Destination = e.Destination
|
||||||
|
m.SAMLRequestState = domain.SAMLRequestStateAdded
|
||||||
|
case *samlrequest.SessionLinkedEvent:
|
||||||
|
m.SessionID = e.SessionID
|
||||||
|
m.UserID = e.UserID
|
||||||
|
m.AuthTime = e.AuthTime
|
||||||
|
m.AuthMethods = e.AuthMethods
|
||||||
|
case *samlrequest.FailedEvent:
|
||||||
|
m.SAMLRequestState = domain.SAMLRequestStateFailed
|
||||||
|
case *samlrequest.SucceededEvent:
|
||||||
|
m.SAMLRequestState = domain.SAMLRequestStateSucceeded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.WriteModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SAMLRequestWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(samlrequest.AggregateType).
|
||||||
|
AggregateIDs(m.AggregateID).
|
||||||
|
Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAuthenticated checks that the auth request exists, a session must have been linked
|
||||||
|
func (m *SAMLRequestWriteModel) CheckAuthenticated() error {
|
||||||
|
if m.SessionID == "" {
|
||||||
|
return zerrors.ThrowPreconditionFailed(nil, "AUTHR-3dNRNwSYeC", "Errors.SAMLRequest.NotAuthenticated")
|
||||||
|
}
|
||||||
|
// check that the requests exists, but has not succeeded yet
|
||||||
|
if m.SAMLRequestState == domain.SAMLRequestStateAdded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return zerrors.ThrowPreconditionFailed(nil, "AUTHR-krQV50AlnJ", "Errors.SAMLRequest.NotAuthenticated")
|
||||||
|
}
|
676
internal/command/saml_request_test.go
Normal file
676
internal/command/saml_request_test.go
Normal file
@ -0,0 +1,676 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
|
"github.com/zitadel/zitadel/internal/id/mock"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/session"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommands_AddSAMLRequest(t *testing.T) {
|
||||||
|
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
idGenerator id.Generator
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
request *SAMLRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *CurrentSAMLRequest
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"already exists error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
request: &SAMLRequest{},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
zerrors.ThrowPreconditionFailed(nil, "COMMAND-MO3vmsMLUt", "Errors.SAMLRequest.AlreadyExisting"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"added",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
request: &SAMLRequest{
|
||||||
|
LoginClient: "login",
|
||||||
|
ApplicationID: "application",
|
||||||
|
ACSURL: "acs",
|
||||||
|
RelayState: "relaystate",
|
||||||
|
RequestID: "request",
|
||||||
|
Binding: "binding",
|
||||||
|
Issuer: "issuer",
|
||||||
|
Destination: "destination",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&CurrentSAMLRequest{
|
||||||
|
SAMLRequest: &SAMLRequest{
|
||||||
|
ID: "V2_id",
|
||||||
|
LoginClient: "login",
|
||||||
|
ApplicationID: "application",
|
||||||
|
ACSURL: "acs",
|
||||||
|
RelayState: "relaystate",
|
||||||
|
RequestID: "request",
|
||||||
|
Binding: "binding",
|
||||||
|
Issuer: "issuer",
|
||||||
|
Destination: "destination",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
}
|
||||||
|
got, err := c.AddSAMLRequest(tt.args.ctx, tt.args.request)
|
||||||
|
require.ErrorIs(t, tt.wantErr, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_LinkSessionToSAMLRequest(t *testing.T) {
|
||||||
|
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
id string
|
||||||
|
sessionID string
|
||||||
|
sessionToken string
|
||||||
|
checkLoginClient bool
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
details *domain.ObjectDetails
|
||||||
|
authReq *CurrentSAMLRequest
|
||||||
|
wantErr error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"samlRequest not found",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
tokenVerifier: newMockTokenVerifierValid(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-GH3PVLSfXC", "Errors.SAMLRequest.NotExisting"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"samlRequest not existing",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("id", "instanceID").Aggregate,
|
||||||
|
domain.SAMLErrorReasonUnspecified,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenVerifier: newMockTokenVerifierValid(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-ttPKNdAIFT", "Errors.SAMLRequest.AlreadyHandled"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wrong login client",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenVerifier: newMockTokenVerifierValid(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "orgID", "wrongLoginClient"),
|
||||||
|
id: "id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
sessionToken: "token",
|
||||||
|
checkLoginClient: true,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-KCd48Rxt7x", "Errors.SAMLRequest.WrongLoginClient"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"session not existing",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
tokenVerifier: newMockTokenVerifierValid(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "V2_id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"session expired",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewAddedEvent(mockCtx,
|
||||||
|
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
&domain.UserAgent{
|
||||||
|
FingerprintID: gu.Ptr("fp1"),
|
||||||
|
IP: net.ParseIP("1.2.3.4"),
|
||||||
|
Description: gu.Ptr("firefox"),
|
||||||
|
Header: http.Header{"foo": []string{"bar"}},
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
"userID", "org1", testNow.Add(-5*time.Minute), &language.Afrikaans),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
testNow.Add(-5*time.Minute)),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
2*time.Minute),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "V2_id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
sessionToken: "token",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Hkl3d", "Errors.Session.Expired"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid session token",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewAddedEvent(mockCtx,
|
||||||
|
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
&domain.UserAgent{
|
||||||
|
FingerprintID: gu.Ptr("fp1"),
|
||||||
|
IP: net.ParseIP("1.2.3.4"),
|
||||||
|
Description: gu.Ptr("firefox"),
|
||||||
|
Header: http.Header{"foo": []string{"bar"}},
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenVerifier: newMockTokenVerifierInvalid(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "V2_id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
sessionToken: "invalid",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"linked",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewAddedEvent(mockCtx,
|
||||||
|
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
&domain.UserAgent{
|
||||||
|
FingerprintID: gu.Ptr("fp1"),
|
||||||
|
IP: net.ParseIP("1.2.3.4"),
|
||||||
|
Description: gu.Ptr("firefox"),
|
||||||
|
Header: http.Header{"foo": []string{"bar"}},
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
"userID", "org1", testNow, &language.Afrikaans),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
testNow),
|
||||||
|
),
|
||||||
|
eventFromEventPusherWithCreationDateNow(
|
||||||
|
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
2*time.Minute),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
samlrequest.NewSessionLinkedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"sessionID",
|
||||||
|
"userID",
|
||||||
|
testNow,
|
||||||
|
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenVerifier: newMockTokenVerifierValid(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "V2_id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
sessionToken: "token",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
|
||||||
|
authReq: &CurrentSAMLRequest{
|
||||||
|
SAMLRequest: &SAMLRequest{
|
||||||
|
ID: "V2_id",
|
||||||
|
LoginClient: "login",
|
||||||
|
ApplicationID: "application",
|
||||||
|
ACSURL: "acs",
|
||||||
|
RelayState: "relaystate",
|
||||||
|
RequestID: "request",
|
||||||
|
Binding: "binding",
|
||||||
|
Issuer: "issuer",
|
||||||
|
Destination: "destination",
|
||||||
|
},
|
||||||
|
SessionID: "sessionID",
|
||||||
|
UserID: "userID",
|
||||||
|
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"linked with login client check",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"loginClient",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewAddedEvent(mockCtx,
|
||||||
|
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
&domain.UserAgent{
|
||||||
|
FingerprintID: gu.Ptr("fp1"),
|
||||||
|
IP: net.ParseIP("1.2.3.4"),
|
||||||
|
Description: gu.Ptr("firefox"),
|
||||||
|
Header: http.Header{"foo": []string{"bar"}},
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
"userID", "org1", testNow, &language.Afrikaans),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
testNow),
|
||||||
|
),
|
||||||
|
eventFromEventPusherWithCreationDateNow(
|
||||||
|
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
2*time.Minute),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
samlrequest.NewSessionLinkedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"sessionID",
|
||||||
|
"userID",
|
||||||
|
testNow,
|
||||||
|
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenVerifier: newMockTokenVerifierValid(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
|
||||||
|
id: "V2_id",
|
||||||
|
sessionID: "sessionID",
|
||||||
|
sessionToken: "token",
|
||||||
|
checkLoginClient: true,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
|
||||||
|
authReq: &CurrentSAMLRequest{
|
||||||
|
SAMLRequest: &SAMLRequest{
|
||||||
|
ID: "V2_id",
|
||||||
|
LoginClient: "loginClient",
|
||||||
|
ApplicationID: "application",
|
||||||
|
ACSURL: "acs",
|
||||||
|
RelayState: "relaystate",
|
||||||
|
RequestID: "request",
|
||||||
|
Binding: "binding",
|
||||||
|
Issuer: "issuer",
|
||||||
|
Destination: "destination",
|
||||||
|
},
|
||||||
|
SessionID: "sessionID",
|
||||||
|
UserID: "userID",
|
||||||
|
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
sessionTokenVerifier: tt.fields.tokenVerifier,
|
||||||
|
}
|
||||||
|
details, got, err := c.LinkSessionToSAMLRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient)
|
||||||
|
require.ErrorIs(t, err, tt.res.wantErr)
|
||||||
|
assertObjectDetails(t, tt.res.details, details)
|
||||||
|
if err == nil {
|
||||||
|
assert.WithinRange(t, got.AuthTime, testNow, testNow)
|
||||||
|
got.AuthTime = time.Time{}
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.res.authReq, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_FailSAMLRequest(t *testing.T) {
|
||||||
|
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
id string
|
||||||
|
reason domain.SAMLErrorReason
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
details *domain.ObjectDetails
|
||||||
|
samlReq *CurrentSAMLRequest
|
||||||
|
wantErr error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"authRequest not existing",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "foo",
|
||||||
|
reason: domain.SAMLErrorReasonAuthNFailed,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-32lGj1Fhjt", "Errors.SAMLRequest.AlreadyHandled"),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
"already failed",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
domain.SAMLErrorReasonAuthNFailed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "V2_id",
|
||||||
|
reason: domain.SAMLErrorReasonAuthNFailed,
|
||||||
|
description: "desc",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-32lGj1Fhjt", "Errors.SAMLRequest.AlreadyHandled"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"failed",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
"login",
|
||||||
|
"application",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||||
|
domain.SAMLErrorReasonAuthNFailed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: mockCtx,
|
||||||
|
id: "V2_id",
|
||||||
|
reason: domain.SAMLErrorReasonAuthNFailed,
|
||||||
|
description: "desc",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
|
||||||
|
samlReq: &CurrentSAMLRequest{
|
||||||
|
SAMLRequest: &SAMLRequest{
|
||||||
|
ID: "V2_id",
|
||||||
|
LoginClient: "login",
|
||||||
|
ApplicationID: "application",
|
||||||
|
ACSURL: "acs",
|
||||||
|
RelayState: "relaystate",
|
||||||
|
RequestID: "request",
|
||||||
|
Binding: "binding",
|
||||||
|
Issuer: "issuer",
|
||||||
|
Destination: "destination",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
}
|
||||||
|
details, got, err := c.FailSAMLRequest(tt.args.ctx, tt.args.id, tt.args.reason)
|
||||||
|
require.ErrorIs(t, err, tt.res.wantErr)
|
||||||
|
assertObjectDetails(t, tt.res.details, details)
|
||||||
|
assert.Equal(t, tt.res.samlReq, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
186
internal/command/saml_session.go
Normal file
186
internal/command/saml_session.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/activity"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlsession"
|
||||||
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SAMLSession struct {
|
||||||
|
SessionID string
|
||||||
|
SAMLResponseID string
|
||||||
|
EntityID string
|
||||||
|
UserID string
|
||||||
|
Audience []string
|
||||||
|
Expiration time.Time
|
||||||
|
AuthMethods []domain.UserAuthMethodType
|
||||||
|
AuthTime time.Time
|
||||||
|
PreferredLanguage *language.Tag
|
||||||
|
UserAgent *domain.UserAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
type SAMLRequestComplianceChecker func(context.Context, *SAMLRequestWriteModel) error
|
||||||
|
|
||||||
|
func (c *Commands) CreateSAMLSessionFromSAMLRequest(ctx context.Context, samlReqId string, complianceCheck SAMLRequestComplianceChecker, samlResponseID string, samlResponseLifetime time.Duration) (err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
if samlReqId == "" {
|
||||||
|
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-0LxK6O31wH", "Errors.SAMLRequest.InvalidCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
samlReqModel, err := c.getSAMLRequestWriteModel(ctx, samlReqId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||||
|
sessionModel := NewSessionWriteModel(samlReqModel.SessionID, instanceID)
|
||||||
|
err = c.eventstore.FilterToQueryReducer(ctx, sessionModel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = sessionModel.CheckIsActive(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := c.newSAMLSessionAddEvents(ctx, sessionModel.UserID, sessionModel.UserResourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = complianceCheck(ctx, samlReqModel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddSession(ctx,
|
||||||
|
sessionModel.UserID,
|
||||||
|
sessionModel.UserResourceOwner,
|
||||||
|
sessionModel.AggregateID,
|
||||||
|
samlReqModel.Issuer,
|
||||||
|
[]string{samlReqModel.Issuer},
|
||||||
|
samlReqModel.AuthMethods,
|
||||||
|
samlReqModel.AuthTime,
|
||||||
|
sessionModel.PreferredLanguage,
|
||||||
|
sessionModel.UserAgent,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = cmd.AddSAMLResponse(ctx, samlResponseID, samlResponseLifetime); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.SetSAMLRequestSuccessful(ctx, samlReqModel.aggregate)
|
||||||
|
_, err = cmd.PushEvents(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) newSAMLSessionAddEvents(ctx context.Context, userID, resourceOwner string, pending ...eventstore.Command) (*SAMLSessionEvents, error) {
|
||||||
|
userStateModel, err := c.userStateWriteModel(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !userStateModel.UserState.IsEnabled() {
|
||||||
|
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-1768ZQpmcP", "Errors.User.NotActive")
|
||||||
|
}
|
||||||
|
sessionID, err := c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sessionID = IDPrefixV2 + sessionID
|
||||||
|
return &SAMLSessionEvents{
|
||||||
|
commands: c,
|
||||||
|
idGenerator: c.idGenerator,
|
||||||
|
encryptionAlg: c.keyAlgorithm,
|
||||||
|
events: pending,
|
||||||
|
samlSessionWriteModel: NewSAMLSessionWriteModel(sessionID, resourceOwner),
|
||||||
|
userStateModel: userStateModel,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SAMLSessionEvents struct {
|
||||||
|
commands *Commands
|
||||||
|
idGenerator id.Generator
|
||||||
|
encryptionAlg crypto.EncryptionAlgorithm
|
||||||
|
events []eventstore.Command
|
||||||
|
samlSessionWriteModel *SAMLSessionWriteModel
|
||||||
|
userStateModel *UserV2WriteModel
|
||||||
|
|
||||||
|
// samlResponseID is set by the command
|
||||||
|
samlResponseID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SAMLSessionEvents) AddSession(
|
||||||
|
ctx context.Context,
|
||||||
|
userID,
|
||||||
|
userResourceOwner,
|
||||||
|
sessionID,
|
||||||
|
entityID string,
|
||||||
|
audience []string,
|
||||||
|
authMethods []domain.UserAuthMethodType,
|
||||||
|
authTime time.Time,
|
||||||
|
preferredLanguage *language.Tag,
|
||||||
|
userAgent *domain.UserAgent,
|
||||||
|
) {
|
||||||
|
c.events = append(c.events, samlsession.NewAddedEvent(
|
||||||
|
ctx,
|
||||||
|
c.samlSessionWriteModel.aggregate,
|
||||||
|
userID,
|
||||||
|
userResourceOwner,
|
||||||
|
sessionID,
|
||||||
|
entityID,
|
||||||
|
audience,
|
||||||
|
authMethods,
|
||||||
|
authTime,
|
||||||
|
preferredLanguage,
|
||||||
|
userAgent,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SAMLSessionEvents) SetSAMLRequestSuccessful(ctx context.Context, samlRequestAggregate *eventstore.Aggregate) {
|
||||||
|
c.events = append(c.events, samlrequest.NewSucceededEvent(ctx, samlRequestAggregate))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SAMLSessionEvents) SetSAMLRequestFailed(ctx context.Context, samlRequestAggregate *eventstore.Aggregate, err domain.SAMLErrorReason) {
|
||||||
|
c.events = append(c.events, samlrequest.NewFailedEvent(ctx, samlRequestAggregate, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SAMLSessionEvents) AddSAMLResponse(ctx context.Context, id string, lifetime time.Duration) error {
|
||||||
|
c.samlResponseID = id
|
||||||
|
c.events = append(c.events, samlsession.NewSAMLResponseAddedEvent(ctx, c.samlSessionWriteModel.aggregate, id, lifetime))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SAMLSessionEvents) PushEvents(ctx context.Context) (*SAMLSession, error) {
|
||||||
|
pushedEvents, err := c.commands.eventstore.Push(ctx, c.events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(c.samlSessionWriteModel, pushedEvents...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
session := &SAMLSession{
|
||||||
|
SessionID: c.samlSessionWriteModel.SessionID,
|
||||||
|
EntityID: c.samlSessionWriteModel.EntityID,
|
||||||
|
UserID: c.samlSessionWriteModel.UserID,
|
||||||
|
Audience: c.samlSessionWriteModel.Audience,
|
||||||
|
Expiration: c.samlSessionWriteModel.SAMLResponseExpiration,
|
||||||
|
AuthMethods: c.samlSessionWriteModel.AuthMethods,
|
||||||
|
AuthTime: c.samlSessionWriteModel.AuthTime,
|
||||||
|
PreferredLanguage: c.samlSessionWriteModel.PreferredLanguage,
|
||||||
|
UserAgent: c.samlSessionWriteModel.UserAgent,
|
||||||
|
SAMLResponseID: c.samlSessionWriteModel.SAMLResponseID,
|
||||||
|
}
|
||||||
|
activity.Trigger(ctx, c.samlSessionWriteModel.UserResourceOwner, c.samlSessionWriteModel.UserID, activity.SAMLResponse, c.commands.eventstore.FilterToQueryReducer)
|
||||||
|
return session, nil
|
||||||
|
}
|
102
internal/command/saml_session_model.go
Normal file
102
internal/command/saml_session_model.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlsession"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SAMLSessionWriteModel struct {
|
||||||
|
eventstore.WriteModel
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
UserResourceOwner string
|
||||||
|
PreferredLanguage *language.Tag
|
||||||
|
SessionID string
|
||||||
|
EntityID string
|
||||||
|
Audience []string
|
||||||
|
AuthMethods []domain.UserAuthMethodType
|
||||||
|
AuthTime time.Time
|
||||||
|
UserAgent *domain.UserAgent
|
||||||
|
State domain.SAMLSessionState
|
||||||
|
SAMLResponseID string
|
||||||
|
SAMLResponseCreation time.Time
|
||||||
|
SAMLResponseExpiration time.Time
|
||||||
|
|
||||||
|
aggregate *eventstore.Aggregate
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSAMLSessionWriteModel(id string, resourceOwner string) *SAMLSessionWriteModel {
|
||||||
|
return &SAMLSessionWriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: id,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
aggregate: &samlsession.NewAggregate(id, resourceOwner).Aggregate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLSessionWriteModel) Reduce() error {
|
||||||
|
for _, event := range wm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *samlsession.AddedEvent:
|
||||||
|
wm.reduceAdded(e)
|
||||||
|
case *samlsession.SAMLResponseAddedEvent:
|
||||||
|
wm.reduceSAMLResponseAdded(e)
|
||||||
|
case *samlsession.SAMLResponseRevokedEvent:
|
||||||
|
wm.reduceSAMLResponseRevoked(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wm.WriteModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLSessionWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(samlsession.AggregateType).
|
||||||
|
AggregateIDs(wm.AggregateID).
|
||||||
|
EventTypes(
|
||||||
|
samlsession.AddedType,
|
||||||
|
samlsession.SAMLResponseAddedType,
|
||||||
|
samlsession.SAMLResponseRevokedType,
|
||||||
|
).
|
||||||
|
Builder()
|
||||||
|
|
||||||
|
if wm.ResourceOwner != "" {
|
||||||
|
query.ResourceOwner(wm.ResourceOwner)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLSessionWriteModel) reduceAdded(e *samlsession.AddedEvent) {
|
||||||
|
wm.UserID = e.UserID
|
||||||
|
wm.UserResourceOwner = e.UserResourceOwner
|
||||||
|
wm.SessionID = e.SessionID
|
||||||
|
wm.EntityID = e.EntityID
|
||||||
|
wm.Audience = e.Audience
|
||||||
|
wm.AuthMethods = e.AuthMethods
|
||||||
|
wm.AuthTime = e.AuthTime
|
||||||
|
wm.PreferredLanguage = e.PreferredLanguage
|
||||||
|
wm.UserAgent = e.UserAgent
|
||||||
|
wm.State = domain.SAMLSessionStateActive
|
||||||
|
// the write model might be initialized without resource owner,
|
||||||
|
// so update the aggregate
|
||||||
|
if wm.ResourceOwner == "" {
|
||||||
|
wm.aggregate = &samlsession.NewAggregate(wm.AggregateID, e.Aggregate().ResourceOwner).Aggregate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLSessionWriteModel) reduceSAMLResponseAdded(e *samlsession.SAMLResponseAddedEvent) {
|
||||||
|
wm.SAMLResponseID = e.ID
|
||||||
|
wm.SAMLResponseCreation = e.CreationDate()
|
||||||
|
wm.SAMLResponseExpiration = e.CreationDate().Add(e.Lifetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *SAMLSessionWriteModel) reduceSAMLResponseRevoked(e *samlsession.SAMLResponseRevokedEvent) {
|
||||||
|
wm.SAMLResponseID = ""
|
||||||
|
wm.SAMLResponseExpiration = e.CreationDate()
|
||||||
|
}
|
337
internal/command/saml_session_test.go
Normal file
337
internal/command/saml_session_test.go
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
|
"github.com/zitadel/zitadel/internal/id/mock"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlsession"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/session"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockSAMLRequestComplianceChecker(returnErr error) SAMLRequestComplianceChecker {
|
||||||
|
return func(context.Context, *SAMLRequestWriteModel) error {
|
||||||
|
return returnErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(*testing.T) *eventstore.Eventstore
|
||||||
|
idGenerator id.Generator
|
||||||
|
keyAlgorithm crypto.EncryptionAlgorithm
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
samlRequestID string
|
||||||
|
samlResponseID string
|
||||||
|
complianceCheck SAMLRequestComplianceChecker
|
||||||
|
samlResponseLifetime time.Duration
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"missing code",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||||
|
samlRequestID: "",
|
||||||
|
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-0LxK6O31wH", "Errors.SAMLRequest.InvalidCode"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filter error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilterError(io.ErrClosedPipe),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||||
|
samlRequestID: "V2_samlRequestID",
|
||||||
|
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"session filter error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||||
|
"loginClient",
|
||||||
|
"applicationId",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilterError(io.ErrClosedPipe),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||||
|
samlRequestID: "V2_samlRequestID",
|
||||||
|
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inactive session error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||||
|
"loginClient",
|
||||||
|
"applicationId",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewSessionLinkedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||||
|
"sessionID",
|
||||||
|
"userID",
|
||||||
|
testNow,
|
||||||
|
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(), // inactive session
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||||
|
samlRequestID: "V2_samlRequestID",
|
||||||
|
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user not active",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||||
|
"loginClient",
|
||||||
|
"applicationId",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewSessionLinkedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||||
|
"sessionID",
|
||||||
|
"userID",
|
||||||
|
testNow,
|
||||||
|
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewAddedEvent(context.Background(),
|
||||||
|
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
&domain.UserAgent{
|
||||||
|
FingerprintID: gu.Ptr("fp1"),
|
||||||
|
IP: net.ParseIP("1.2.3.4"),
|
||||||
|
Description: gu.Ptr("firefox"),
|
||||||
|
Header: http.Header{"foo": []string{"bar"}},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||||
|
"userID", "org1", testNow, &language.Afrikaans),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||||
|
testNow),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
user.NewHumanAddedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&user.NewAggregate("userID", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.Afrikaans,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
user.NewUserDeactivatedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&user.NewAggregate("userID", "org1").Aggregate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: mock.NewIDGeneratorExpectIDs(t),
|
||||||
|
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||||
|
samlRequestID: "V2_samlRequestID",
|
||||||
|
samlResponseID: "samlResponseID",
|
||||||
|
samlResponseLifetime: time.Minute * 5,
|
||||||
|
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: zerrors.ThrowPreconditionFailed(nil, "SAML-1768ZQpmcP", "Errors.User.NotActive"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"add successful",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||||
|
"loginClient",
|
||||||
|
"applicationId",
|
||||||
|
"acs",
|
||||||
|
"relaystate",
|
||||||
|
"request",
|
||||||
|
"binding",
|
||||||
|
"issuer",
|
||||||
|
"destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
samlrequest.NewSessionLinkedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||||
|
"sessionID",
|
||||||
|
"userID",
|
||||||
|
testNow,
|
||||||
|
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewAddedEvent(context.Background(),
|
||||||
|
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||||
|
&domain.UserAgent{
|
||||||
|
FingerprintID: gu.Ptr("fp1"),
|
||||||
|
IP: net.ParseIP("1.2.3.4"),
|
||||||
|
Description: gu.Ptr("firefox"),
|
||||||
|
Header: http.Header{"foo": []string{"bar"}},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||||
|
"userID", "org1", testNow, &language.Afrikaans),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||||
|
testNow),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
user.NewHumanAddedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&user.NewAggregate("userID", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.Afrikaans,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
samlsession.NewAddedEvent(context.Background(), &samlsession.NewAggregate("V2_samlSessionID", "org1").Aggregate,
|
||||||
|
"userID", "org1", "sessionID", "issuer", []string{"issuer"},
|
||||||
|
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, &language.Afrikaans,
|
||||||
|
&domain.UserAgent{
|
||||||
|
FingerprintID: gu.Ptr("fp1"),
|
||||||
|
IP: net.ParseIP("1.2.3.4"),
|
||||||
|
Description: gu.Ptr("firefox"),
|
||||||
|
Header: http.Header{"foo": []string{"bar"}},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
samlsession.NewSAMLResponseAddedEvent(context.Background(), &samlsession.NewAggregate("V2_samlSessionID", "org1").Aggregate, "samlResponseID", time.Minute*5),
|
||||||
|
samlrequest.NewSucceededEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: mock.NewIDGeneratorExpectIDs(t, "samlSessionID"),
|
||||||
|
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||||
|
samlRequestID: "V2_samlRequestID",
|
||||||
|
samlResponseID: "samlResponseID",
|
||||||
|
samlResponseLifetime: time.Minute * 5,
|
||||||
|
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||||
|
},
|
||||||
|
res{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
keyAlgorithm: tt.fields.keyAlgorithm,
|
||||||
|
}
|
||||||
|
err := c.CreateSAMLSessionFromSAMLRequest(tt.args.ctx, tt.args.samlRequestID, tt.args.complianceCheck, tt.args.samlResponseID, tt.args.samlResponseLifetime)
|
||||||
|
require.ErrorIs(t, err, tt.res.err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
41
internal/domain/saml_error_reason.go
Normal file
41
internal/domain/saml_error_reason.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/saml/pkg/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SAMLErrorReason int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
SAMLErrorReasonUnspecified SAMLErrorReason = iota
|
||||||
|
SAMLErrorReasonVersionMissmatch
|
||||||
|
SAMLErrorReasonAuthNFailed
|
||||||
|
SAMLErrorReasonInvalidAttrNameOrValue
|
||||||
|
SAMLErrorReasonInvalidNameIDPolicy
|
||||||
|
SAMLErrorReasonRequestDenied
|
||||||
|
SAMLErrorReasonRequestUnsupported
|
||||||
|
SAMLErrorReasonUnsupportedBinding
|
||||||
|
)
|
||||||
|
|
||||||
|
func SAMLErrorReasonToString(reason SAMLErrorReason) string {
|
||||||
|
switch reason {
|
||||||
|
case SAMLErrorReasonUnspecified:
|
||||||
|
return "unspecified error"
|
||||||
|
case SAMLErrorReasonVersionMissmatch:
|
||||||
|
return provider.StatusCodeVersionMissmatch
|
||||||
|
case SAMLErrorReasonAuthNFailed:
|
||||||
|
return provider.StatusCodeAuthNFailed
|
||||||
|
case SAMLErrorReasonInvalidAttrNameOrValue:
|
||||||
|
return provider.StatusCodeInvalidAttrNameOrValue
|
||||||
|
case SAMLErrorReasonInvalidNameIDPolicy:
|
||||||
|
return provider.StatusCodeInvalidNameIDPolicy
|
||||||
|
case SAMLErrorReasonRequestDenied:
|
||||||
|
return provider.StatusCodeRequestDenied
|
||||||
|
case SAMLErrorReasonRequestUnsupported:
|
||||||
|
return provider.StatusCodeRequestUnsupported
|
||||||
|
case SAMLErrorReasonUnsupportedBinding:
|
||||||
|
return provider.StatusCodeUnsupportedBinding
|
||||||
|
default:
|
||||||
|
return "unspecified error"
|
||||||
|
}
|
||||||
|
}
|
10
internal/domain/saml_request.go
Normal file
10
internal/domain/saml_request.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type SAMLRequestState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SAMLRequestStateUnspecified SAMLRequestState = iota
|
||||||
|
SAMLRequestStateAdded
|
||||||
|
SAMLRequestStateFailed
|
||||||
|
SAMLRequestStateSucceeded
|
||||||
|
)
|
9
internal/domain/saml_session.go
Normal file
9
internal/domain/saml_session.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type SAMLSessionState int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
SAMLSessionStateUnspecified SAMLSessionState = iota
|
||||||
|
SAMLSessionStateActive
|
||||||
|
SAMLSessionStateTerminated
|
||||||
|
)
|
@ -34,6 +34,7 @@ import (
|
|||||||
user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
|
user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
|
||||||
userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
|
userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
|
||||||
webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
|
||||||
|
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||||
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||||
@ -65,6 +66,7 @@ type Client struct {
|
|||||||
WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient
|
WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient
|
||||||
IDPv2 idp_pb.IdentityProviderServiceClient
|
IDPv2 idp_pb.IdentityProviderServiceClient
|
||||||
UserV3Alpha user_v3alpha.ZITADELUsersClient
|
UserV3Alpha user_v3alpha.ZITADELUsersClient
|
||||||
|
SAMLv2 saml_pb.SAMLServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClient(ctx context.Context, target string) (*Client, error) {
|
func newClient(ctx context.Context, target string) (*Client, error) {
|
||||||
@ -96,6 +98,7 @@ func newClient(ctx context.Context, target string) (*Client, error) {
|
|||||||
WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc),
|
WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc),
|
||||||
IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
|
IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
|
||||||
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
|
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
|
||||||
|
SAMLv2: saml_pb.NewSAMLServiceClient(cc),
|
||||||
}
|
}
|
||||||
return client, client.pollHealth(ctx)
|
return client, client.pollHealth(ctx)
|
||||||
}
|
}
|
||||||
|
223
internal/integration/saml.go
Normal file
223
internal/integration/saml.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/crewjam/saml"
|
||||||
|
"github.com/crewjam/saml/samlsp"
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||||
|
oidc_internal "github.com/zitadel/zitadel/internal/api/oidc"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||||
|
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||||
|
session_pb "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const spCertificate = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDITCCAgmgAwIBAgIUUo5urYkuUHAe7LQ9sZSL+xXAqBwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIwNDEz
|
||||||
|
MTE1MFoXDTI1MDEwMzEzMTE1MFowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w
|
||||||
|
bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoACwbGIh8udK
|
||||||
|
Um1r+yQoPtfswEX6Cb6Y1KwR6WZDYgzHdMyUC5Sy8Bg1H2puUZZukDLuyu6Pqvum
|
||||||
|
8kfnzhjUR6nNCoUlidwE+yz020w5oOBofRKgJK/FVUuWD3k6kjdP9CrBFLG0PQQ3
|
||||||
|
N2e4wilP4czCxizKero2a0e7Eq8OjHAPf8gjM+GWFZgVAbV8uf2Mjt1O2Vfbx5PZ
|
||||||
|
sLuBZtl5jokx3NiC7my/yj81MbGEDPcQo0emeVBz3J3nVG6Yr4kdCKkvv2dhJ26C
|
||||||
|
5cL7NIIUY4IQomJNwYC2NaYgSpQOxJHL/HsOPusO4Ia2WtUTXEZUFkxn1u0YuoSx
|
||||||
|
CkGehF/1OwIDAQABo1MwUTAdBgNVHQ4EFgQUr6S0wA2l3MdfnvfveWDueQtaoJMw
|
||||||
|
HwYDVR0jBBgwFoAUr6S0wA2l3MdfnvfveWDueQtaoJMwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zANBgkqhkiG9w0BAQsFAAOCAQEAH3Q9obyWJaMKFuGJDkIp1RFot79RWTVcAcwA
|
||||||
|
XTJNfCseLONRIs4MkRxOn6GQBwV2IEqs1+hFG80dcd/c6yYyJ8bziKEyNMtPWrl6
|
||||||
|
fdVD+1WnWcD1ZYrS8hgdz0FxXxl/+GjA8Pu6icmnhKgUDTYWns6Rj/gtQtZS8ZoA
|
||||||
|
JY+T/1mGze2+Xx6pjuArZ7+hnH6EWwo+ckcmXAKyhnkhX7xIo1UFvNY2VWaGl2wU
|
||||||
|
K2yyJA4Lu/NNmqPnpAcRDsnGP6r4frMhjnPq/ifC3B+6FT3p8dubV9PA0y86bAy5
|
||||||
|
0yIgNje4DyWLy/DM9EpdPfJmvUAL6hOtyb8Aa9hR+a8stu7h6g==
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
const spKey = `-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCgALBsYiHy50pS
|
||||||
|
bWv7JCg+1+zARfoJvpjUrBHpZkNiDMd0zJQLlLLwGDUfam5Rlm6QMu7K7o+q+6by
|
||||||
|
R+fOGNRHqc0KhSWJ3AT7LPTbTDmg4Gh9EqAkr8VVS5YPeTqSN0/0KsEUsbQ9BDc3
|
||||||
|
Z7jCKU/hzMLGLMp6ujZrR7sSrw6McA9/yCMz4ZYVmBUBtXy5/YyO3U7ZV9vHk9mw
|
||||||
|
u4Fm2XmOiTHc2ILubL/KPzUxsYQM9xCjR6Z5UHPcnedUbpiviR0IqS+/Z2EnboLl
|
||||||
|
wvs0ghRjghCiYk3BgLY1piBKlA7Ekcv8ew4+6w7ghrZa1RNcRlQWTGfW7Ri6hLEK
|
||||||
|
QZ6EX/U7AgMBAAECggEAD1aRkwpDO+BdORKhP9WDACc93F647fc1+mk2XFv/yKX1
|
||||||
|
9uXnqUaLcsW3TfgrdCnKFouzZYPCBP+TzPUErTanHumRrNj/tLwBRDzWijE/8wKg
|
||||||
|
MaE39dxdu+P/kiMqcLrZsMvqb3vrjc/aJTcNuJsyO7Cf2VSQ4nv4XIdnUQ60A9VR
|
||||||
|
OmUp//VULZxImnPx/R304/p5VfOhyXfzBeoxUPogBurjtzkyXVG0EG2enJMMiTix
|
||||||
|
900fTDez0TQ8V6O59vM04fhtPXvH51OkMTW/HU1QQvlnAJuX06I7k4CaBpF3xPII
|
||||||
|
QpEbFILq5y6yAQJWELRGWzeoxK6kn6bNfI8S0+oKqQKBgQDg2UM7ruMASpY7B4zj
|
||||||
|
XNztGDOx9BCdYyHH1O05r+ILmltBC7jFImwIYrHbaX+dg52l0PPImZuBz852IqrC
|
||||||
|
VAEF30yBn2gWyVzIdo7W3mw9Jgqc4LrhStaJxOuXVoT2/PAuDBF8TJMNH9oLNqiD
|
||||||
|
aPAI0cVn9BRV7AziEsrMlDLLiQKBgQC2K4Z/caAvwx/AescsN6lp+/m7MeLUpZzQ
|
||||||
|
myZt44bnR5LouUo3vCYl+Bk8wu6PTd41LUYW/SW26HDDFTKgkBb1zVHfk5QRApaB
|
||||||
|
VPwZnhcUvNapPOnDp75Qoq238wpfayQlKF1xCawS3N5AWkDaEdfzuH7umFJxVss2
|
||||||
|
1tfDsn01owKBgAYWG3nMHBzv5+0lIS0uYFSSqSOSBbkc69cq7lj3Z9kEjp/OH2xG
|
||||||
|
qEH52fKkgm3TGDta0p6Fee4jn+UWvySPfY+ZIcsIc5raTIaonuk2EBv/oZ3pf2WF
|
||||||
|
zxTfnbj1AJhm9GFqtjZ1JC3gxNg03I7iEk1K0FsmAj7pKtgbxh2PjWhxAoGBAKBx
|
||||||
|
BSwJbwOh3r0vZWvUOilV+0SbUyPmGI7Blr8BvTbFGuZNCsi7tP2L3O5e4Kzl7+b1
|
||||||
|
0N0+Z5EIdwfaC5TOUup5wroeyDGTDesqZj5JthpVltnHBDuF6WArZsS0EVaojlUL
|
||||||
|
kACWfC7AyB31X1iwjnng7CpHjZS01JWf8rgw44XxAoGAQ5YYd4WmGYZoJJak7zhb
|
||||||
|
xnYG7hU7nS7pBPGob1FvjYMw1x/htuJCjxLh08dlzJGM6SFlDn7HVM9ou99w5n+d
|
||||||
|
xtqmbthw2E9VjSk3zSYb4uFc6mv0C/kRPTDUFH+9CpQTBBx/O016hmcatxlBS6JL
|
||||||
|
VAV6oE8sEJYHtR6YdZiMWWo=
|
||||||
|
-----END PRIVATE KEY-----`
|
||||||
|
|
||||||
|
func CreateSAMLSP(root string, idpMetadata *saml.EntityDescriptor, binding string) (*samlsp.Middleware, error) {
|
||||||
|
rootURL, err := url.Parse(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyPair, err := tls.X509KeyPair([]byte(spCertificate), []byte(spKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sp, err := samlsp.New(samlsp.Options{
|
||||||
|
URL: *rootURL,
|
||||||
|
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
|
||||||
|
Certificate: keyPair.Leaf,
|
||||||
|
IDPMetadata: idpMetadata,
|
||||||
|
UseArtifactResponse: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sp.Binding = binding
|
||||||
|
sp.ResponseBinding = binding
|
||||||
|
return sp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *samlsp.Middleware) (*management.AddSAMLAppResponse, error) {
|
||||||
|
spMetadata, err := xml.MarshalIndent(m.ServiceProvider.Metadata(), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ResponseBinding == saml.HTTPRedirectBinding {
|
||||||
|
metadata := strings.Replace(string(spMetadata), saml.HTTPPostBinding, saml.HTTPRedirectBinding, 2)
|
||||||
|
spMetadata = []byte(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := i.Client.Mgmt.AddSAMLApp(ctx, &management.AddSAMLAppRequest{
|
||||||
|
ProjectId: projectID,
|
||||||
|
Name: fmt.Sprintf("app-%s", gofakeit.AppName()),
|
||||||
|
Metadata: &management.AddSAMLAppRequest_MetadataXml{MetadataXml: spMetadata},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, await(func() error {
|
||||||
|
_, err := i.Client.Mgmt.GetProjectByID(ctx, &management.GetProjectByIDRequest{
|
||||||
|
Id: projectID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{
|
||||||
|
ProjectId: projectID,
|
||||||
|
AppId: resp.GetAppId(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient string, acs saml.Endpoint, relayState string, responseBinding string) (authRequestID string, err error) {
|
||||||
|
authReq, err := m.ServiceProvider.MakeAuthenticationRequest(acs.Location, acs.Binding, responseBinding)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := GetRequest(redirectURL.String(), map[string]string{oidc_internal.LoginClientHeader: loginClient})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := CheckRedirect(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("check redirect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixWithHost := i.Issuer() + i.Config.LoginURLV2
|
||||||
|
if !strings.HasPrefix(loc.String(), prefixWithHost) {
|
||||||
|
return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String())
|
||||||
|
}
|
||||||
|
return strings.TrimPrefix(loc.String(), prefixWithHost), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) FailSAMLAuthRequest(ctx context.Context, id string, reason saml_pb.ErrorReason) *saml_pb.CreateResponseResponse {
|
||||||
|
resp, err := i.Client.SAMLv2.CreateResponse(ctx, &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: id,
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Error{Error: &saml_pb.AuthorizationError{Error: reason}},
|
||||||
|
})
|
||||||
|
logging.OnError(err).Panic("create human user")
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) SuccessfulSAMLAuthRequest(ctx context.Context, userId, id string) *saml_pb.CreateResponseResponse {
|
||||||
|
respSession, err := i.Client.SessionV2.CreateSession(ctx, &session_pb.CreateSessionRequest{
|
||||||
|
Checks: &session_pb.Checks{
|
||||||
|
User: &session_pb.CheckUser{
|
||||||
|
Search: &session_pb.CheckUser_UserId{
|
||||||
|
UserId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
logging.OnError(err).Panic("create session")
|
||||||
|
|
||||||
|
resp, err := i.Client.SAMLv2.CreateResponse(ctx, &saml_pb.CreateResponseRequest{
|
||||||
|
SamlRequestId: id,
|
||||||
|
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||||
|
Session: &saml_pb.Session{
|
||||||
|
SessionId: respSession.GetSessionId(),
|
||||||
|
SessionToken: respSession.GetSessionToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
logging.OnError(err).Panic("create human user")
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) GetSAMLIDPMetadata() (*saml.EntityDescriptor, error) {
|
||||||
|
idpEntityID := http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure) + "/saml/v2/metadata"
|
||||||
|
resp, err := http.Get(idpEntityID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entityDescriptor := new(saml.EntityDescriptor)
|
||||||
|
if err := xml.Unmarshal(data, entityDescriptor); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityDescriptor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) Issuer() string {
|
||||||
|
return http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure)
|
||||||
|
}
|
@ -69,6 +69,7 @@ var (
|
|||||||
DeviceAuthProjection *handler.Handler
|
DeviceAuthProjection *handler.Handler
|
||||||
SessionProjection *handler.Handler
|
SessionProjection *handler.Handler
|
||||||
AuthRequestProjection *handler.Handler
|
AuthRequestProjection *handler.Handler
|
||||||
|
SamlRequestProjection *handler.Handler
|
||||||
MilestoneProjection *handler.Handler
|
MilestoneProjection *handler.Handler
|
||||||
QuotaProjection *quotaProjection
|
QuotaProjection *quotaProjection
|
||||||
LimitsProjection *handler.Handler
|
LimitsProjection *handler.Handler
|
||||||
@ -157,6 +158,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
|
|||||||
DeviceAuthProjection = newDeviceAuthProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["device_auth"]))
|
DeviceAuthProjection = newDeviceAuthProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["device_auth"]))
|
||||||
SessionProjection = newSessionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["sessions"]))
|
SessionProjection = newSessionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["sessions"]))
|
||||||
AuthRequestProjection = newAuthRequestProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["auth_requests"]))
|
AuthRequestProjection = newAuthRequestProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["auth_requests"]))
|
||||||
|
SamlRequestProjection = newSamlRequestProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["saml_requests"]))
|
||||||
MilestoneProjection = newMilestoneProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["milestones"]))
|
MilestoneProjection = newMilestoneProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["milestones"]))
|
||||||
QuotaProjection = newQuotaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["quotas"]))
|
QuotaProjection = newQuotaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["quotas"]))
|
||||||
LimitsProjection = newLimitsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["limits"]))
|
LimitsProjection = newLimitsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["limits"]))
|
||||||
@ -286,6 +288,7 @@ func newProjectionsList() {
|
|||||||
DeviceAuthProjection,
|
DeviceAuthProjection,
|
||||||
SessionProjection,
|
SessionProjection,
|
||||||
AuthRequestProjection,
|
AuthRequestProjection,
|
||||||
|
SamlRequestProjection,
|
||||||
MilestoneProjection,
|
MilestoneProjection,
|
||||||
QuotaProjection.handler,
|
QuotaProjection.handler,
|
||||||
LimitsProjection,
|
LimitsProjection,
|
||||||
|
132
internal/query/projection/saml_request.go
Normal file
132
internal/query/projection/saml_request.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package projection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SamlRequestsProjectionTable = "projections.saml_requests"
|
||||||
|
|
||||||
|
SamlRequestColumnID = "id"
|
||||||
|
SamlRequestColumnCreationDate = "creation_date"
|
||||||
|
SamlRequestColumnChangeDate = "change_date"
|
||||||
|
SamlRequestColumnSequence = "sequence"
|
||||||
|
SamlRequestColumnResourceOwner = "resource_owner"
|
||||||
|
SamlRequestColumnInstanceID = "instance_id"
|
||||||
|
SamlRequestColumnLoginClient = "login_client"
|
||||||
|
SamlRequestColumnIssuer = "issuer"
|
||||||
|
SamlRequestColumnACS = "acs"
|
||||||
|
SamlRequestColumnRelayState = "relay_state"
|
||||||
|
SamlRequestColumnBinding = "binding"
|
||||||
|
)
|
||||||
|
|
||||||
|
type samlRequestProjection struct{}
|
||||||
|
|
||||||
|
// Name implements handler.Projection.
|
||||||
|
func (*samlRequestProjection) Name() string {
|
||||||
|
return SamlRequestsProjectionTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSamlRequestProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||||
|
return handler.NewHandler(ctx, &config, new(samlRequestProjection))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*samlRequestProjection) Init() *old_handler.Check {
|
||||||
|
return handler.NewMultiTableCheck(
|
||||||
|
handler.NewTable([]*handler.InitColumn{
|
||||||
|
handler.NewColumn(SamlRequestColumnID, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(SamlRequestColumnCreationDate, handler.ColumnTypeTimestamp),
|
||||||
|
handler.NewColumn(SamlRequestColumnChangeDate, handler.ColumnTypeTimestamp),
|
||||||
|
handler.NewColumn(SamlRequestColumnSequence, handler.ColumnTypeInt64),
|
||||||
|
handler.NewColumn(SamlRequestColumnResourceOwner, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(SamlRequestColumnInstanceID, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(SamlRequestColumnLoginClient, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(SamlRequestColumnIssuer, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(SamlRequestColumnACS, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(SamlRequestColumnRelayState, handler.ColumnTypeText),
|
||||||
|
handler.NewColumn(SamlRequestColumnBinding, handler.ColumnTypeText),
|
||||||
|
},
|
||||||
|
handler.NewPrimaryKey(SamlRequestColumnInstanceID, SamlRequestColumnID),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *samlRequestProjection) Reducers() []handler.AggregateReducer {
|
||||||
|
return []handler.AggregateReducer{
|
||||||
|
{
|
||||||
|
Aggregate: samlrequest.AggregateType,
|
||||||
|
EventReducers: []handler.EventReducer{
|
||||||
|
{
|
||||||
|
Event: samlrequest.AddedType,
|
||||||
|
Reduce: p.reduceSamlRequestAdded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Event: samlrequest.SucceededType,
|
||||||
|
Reduce: p.reduceSamlRequestEnded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Event: samlrequest.FailedType,
|
||||||
|
Reduce: p.reduceSamlRequestEnded,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Aggregate: instance.AggregateType,
|
||||||
|
EventReducers: []handler.EventReducer{
|
||||||
|
{
|
||||||
|
Event: instance.InstanceRemovedEventType,
|
||||||
|
Reduce: reduceInstanceRemovedHelper(SamlRequestColumnInstanceID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *samlRequestProjection) reduceSamlRequestAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
e, ok := event.(*samlrequest.AddedEvent)
|
||||||
|
if !ok {
|
||||||
|
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Sfwfa", "reduce.wrong.event.type %s", samlrequest.AddedType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.NewCreateStatement(
|
||||||
|
e,
|
||||||
|
[]handler.Column{
|
||||||
|
handler.NewCol(SamlRequestColumnID, e.Aggregate().ID),
|
||||||
|
handler.NewCol(SamlRequestColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
|
handler.NewCol(SamlRequestColumnCreationDate, e.CreationDate()),
|
||||||
|
handler.NewCol(SamlRequestColumnChangeDate, e.CreationDate()),
|
||||||
|
handler.NewCol(SamlRequestColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||||
|
handler.NewCol(SamlRequestColumnSequence, e.Sequence()),
|
||||||
|
handler.NewCol(SamlRequestColumnLoginClient, e.LoginClient),
|
||||||
|
handler.NewCol(SamlRequestColumnIssuer, e.Issuer),
|
||||||
|
handler.NewCol(SamlRequestColumnACS, e.ACSURL),
|
||||||
|
handler.NewCol(SamlRequestColumnRelayState, e.RelayState),
|
||||||
|
handler.NewCol(SamlRequestColumnBinding, e.Binding),
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *samlRequestProjection) reduceSamlRequestEnded(event eventstore.Event) (*handler.Statement, error) {
|
||||||
|
switch event.(type) {
|
||||||
|
case *samlrequest.SucceededEvent,
|
||||||
|
*samlrequest.FailedEvent:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ASF3h", "reduce.wrong.event.type %s", []eventstore.EventType{samlrequest.SucceededType, samlrequest.FailedType})
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.NewDeleteStatement(
|
||||||
|
event,
|
||||||
|
[]handler.Condition{
|
||||||
|
handler.NewCond(SamlRequestColumnID, event.Aggregate().ID),
|
||||||
|
handler.NewCond(SamlRequestColumnInstanceID, event.Aggregate().InstanceID),
|
||||||
|
},
|
||||||
|
), nil
|
||||||
|
}
|
123
internal/query/projection/saml_request_test.go
Normal file
123
internal/query/projection/saml_request_test.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package projection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSamlRequestProjection_reduces(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
event func(t *testing.T) eventstore.Event
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||||
|
want wantReduce
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "reduceSamlRequestAdded",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
samlrequest.AddedType,
|
||||||
|
samlrequest.AggregateType,
|
||||||
|
[]byte(`{"login_client": "loginClient", "issuer": "issuer", "acs_url": "acs", "relay_state": "relayState", "binding": "binding"}`),
|
||||||
|
), eventstore.GenericEventMapper[samlrequest.AddedEvent]),
|
||||||
|
},
|
||||||
|
reduce: (&samlRequestProjection{}).reduceSamlRequestAdded,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("saml_request"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.saml_requests (id, instance_id, creation_date, change_date, resource_owner, sequence, login_client, issuer, acs, relay_state, binding) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"instance-id",
|
||||||
|
anyArg{},
|
||||||
|
anyArg{},
|
||||||
|
"ro-id",
|
||||||
|
uint64(15),
|
||||||
|
"loginClient",
|
||||||
|
"issuer",
|
||||||
|
"acs",
|
||||||
|
"relayState",
|
||||||
|
"binding",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceSamlRequestFailed",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
samlrequest.FailedType,
|
||||||
|
samlrequest.AggregateType,
|
||||||
|
[]byte(`{"reason": 0}`),
|
||||||
|
), eventstore.GenericEventMapper[samlrequest.FailedEvent]),
|
||||||
|
},
|
||||||
|
reduce: (&samlRequestProjection{}).reduceSamlRequestEnded,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("saml_request"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.saml_requests WHERE (id = $1) AND (instance_id = $2)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"instance-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceSamlRequestSucceeded",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
samlrequest.SucceededType,
|
||||||
|
samlrequest.AggregateType,
|
||||||
|
nil,
|
||||||
|
), eventstore.GenericEventMapper[samlrequest.SucceededEvent]),
|
||||||
|
},
|
||||||
|
reduce: (&samlRequestProjection{}).reduceSamlRequestEnded,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("saml_request"),
|
||||||
|
sequence: 15,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.saml_requests WHERE (id = $1) AND (instance_id = $2)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"agg-id",
|
||||||
|
"instance-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := baseEvent(t)
|
||||||
|
got, err := tt.reduce(event)
|
||||||
|
if !zerrors.IsErrorInvalidArgument(err) {
|
||||||
|
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
event = tt.args.event(t)
|
||||||
|
got, err = tt.reduce(event)
|
||||||
|
assertReduce(t, got, err, SamlRequestsProjectionTable, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
81
internal/query/saml_request.go
Normal file
81
internal/query/saml_request.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/api/call"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/query/projection"
|
||||||
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SamlRequest struct {
|
||||||
|
ID string
|
||||||
|
CreationDate time.Time
|
||||||
|
LoginClient string
|
||||||
|
Issuer string
|
||||||
|
ACS string
|
||||||
|
RelayState string
|
||||||
|
Binding string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SamlRequest) checkLoginClient(ctx context.Context) error {
|
||||||
|
if uid := authz.GetCtxData(ctx).UserID; uid != a.LoginClient {
|
||||||
|
return zerrors.ThrowPermissionDenied(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed saml_request_by_id.sql
|
||||||
|
var samlRequestByIDQuery string
|
||||||
|
|
||||||
|
func (q *Queries) samlRequestByIDQuery(ctx context.Context) string {
|
||||||
|
return fmt.Sprintf(samlRequestByIDQuery, q.client.Timetravel(call.Took(ctx)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, id string, checkLoginClient bool) (_ *SamlRequest, err error) {
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
if shouldTriggerBulk {
|
||||||
|
_, traceSpan := tracing.NewNamedSpan(ctx, "TriggerSamlRequestProjection")
|
||||||
|
ctx, err = projection.SamlRequestProjection.Trigger(ctx, handler.WithAwaitRunning())
|
||||||
|
logging.OnError(err).Debug("trigger failed")
|
||||||
|
traceSpan.EndWithError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := new(SamlRequest)
|
||||||
|
err = q.client.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
func(row *sql.Row) error {
|
||||||
|
return row.Scan(
|
||||||
|
&dst.ID, &dst.CreationDate, &dst.LoginClient, &dst.Issuer, &dst.ACS, &dst.RelayState, &dst.Binding,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
q.samlRequestByIDQuery(ctx),
|
||||||
|
id, authz.GetInstance(ctx).InstanceID(),
|
||||||
|
)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, zerrors.ThrowNotFound(err, "QUERY-Thee9", "Errors.SamlRequest.NotExisting")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, zerrors.ThrowInternal(err, "QUERY-Ou8ue", "Errors.Internal")
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkLoginClient {
|
||||||
|
if err = dst.checkLoginClient(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst, nil
|
||||||
|
}
|
11
internal/query/saml_request_by_id.sql
Normal file
11
internal/query/saml_request_by_id.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
select
|
||||||
|
id,
|
||||||
|
creation_date,
|
||||||
|
login_client,
|
||||||
|
issuer,
|
||||||
|
acs,
|
||||||
|
relay_state,
|
||||||
|
binding
|
||||||
|
from projections.saml_requests %s
|
||||||
|
where id = $1 and instance_id = $2
|
||||||
|
limit 1;
|
127
internal/query/saml_request_test.go
Normal file
127
internal/query/saml_request_test.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
|
"github.com/zitadel/zitadel/internal/query/projection"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQueries_SamlRequestByID(t *testing.T) {
|
||||||
|
expQuery := regexp.QuoteMeta(fmt.Sprintf(
|
||||||
|
samlRequestByIDQuery,
|
||||||
|
asOfSystemTime,
|
||||||
|
))
|
||||||
|
|
||||||
|
cols := []string{
|
||||||
|
projection.SamlRequestColumnID,
|
||||||
|
projection.SamlRequestColumnCreationDate,
|
||||||
|
projection.SamlRequestColumnLoginClient,
|
||||||
|
projection.SamlRequestColumnIssuer,
|
||||||
|
projection.SamlRequestColumnACS,
|
||||||
|
projection.SamlRequestColumnRelayState,
|
||||||
|
projection.SamlRequestColumnBinding,
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
shouldTriggerBulk bool
|
||||||
|
id string
|
||||||
|
checkLoginClient bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expect sqlExpectation
|
||||||
|
want *SamlRequest
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success, all values",
|
||||||
|
args: args{
|
||||||
|
shouldTriggerBulk: false,
|
||||||
|
id: "123",
|
||||||
|
checkLoginClient: true,
|
||||||
|
},
|
||||||
|
expect: mockQuery(expQuery, cols, []driver.Value{
|
||||||
|
"id",
|
||||||
|
testNow,
|
||||||
|
"loginClient",
|
||||||
|
"issuer",
|
||||||
|
"acs",
|
||||||
|
"relayState",
|
||||||
|
"binding",
|
||||||
|
}, "123", "instanceID"),
|
||||||
|
want: &SamlRequest{
|
||||||
|
ID: "id",
|
||||||
|
CreationDate: testNow,
|
||||||
|
LoginClient: "loginClient",
|
||||||
|
Issuer: "issuer",
|
||||||
|
ACS: "acs",
|
||||||
|
RelayState: "relayState",
|
||||||
|
Binding: "binding",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no rows",
|
||||||
|
args: args{
|
||||||
|
shouldTriggerBulk: false,
|
||||||
|
id: "123",
|
||||||
|
},
|
||||||
|
expect: mockQueryScanErr(expQuery, cols, nil, "123", "instanceID"),
|
||||||
|
wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-Thee9", "Errors.SamlRequest.NotExisting"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query error",
|
||||||
|
args: args{
|
||||||
|
shouldTriggerBulk: false,
|
||||||
|
id: "123",
|
||||||
|
},
|
||||||
|
expect: mockQueryErr(expQuery, sql.ErrConnDone, "123", "instanceID"),
|
||||||
|
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ou8ue", "Errors.Internal"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong login client",
|
||||||
|
args: args{
|
||||||
|
shouldTriggerBulk: false,
|
||||||
|
id: "123",
|
||||||
|
checkLoginClient: true,
|
||||||
|
},
|
||||||
|
expect: mockQuery(expQuery, cols, []driver.Value{
|
||||||
|
"id",
|
||||||
|
testNow,
|
||||||
|
"wrongLoginClient",
|
||||||
|
"issuer",
|
||||||
|
"acs",
|
||||||
|
"relayState",
|
||||||
|
"binding",
|
||||||
|
}, "123", "instanceID"),
|
||||||
|
wantErr: zerrors.ThrowPermissionDeniedf(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
execMock(t, tt.expect, func(db *sql.DB) {
|
||||||
|
q := &Queries{
|
||||||
|
client: &database.DB{
|
||||||
|
DB: db,
|
||||||
|
Database: &prepareDB{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||||
|
|
||||||
|
got, err := q.SamlRequestByID(ctx, tt.args.shouldTriggerBulk, tt.args.id, tt.args.checkLoginClient)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
26
internal/repository/samlrequest/aggregate.go
Normal file
26
internal/repository/samlrequest/aggregate.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package samlrequest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AggregateType = "saml_request"
|
||||||
|
AggregateVersion = "v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Aggregate struct {
|
||||||
|
eventstore.Aggregate
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAggregate(id, instanceID string) *Aggregate {
|
||||||
|
return &Aggregate{
|
||||||
|
Aggregate: eventstore.Aggregate{
|
||||||
|
Type: AggregateType,
|
||||||
|
Version: AggregateVersion,
|
||||||
|
ID: id,
|
||||||
|
ResourceOwner: instanceID,
|
||||||
|
InstanceID: instanceID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
10
internal/repository/samlrequest/eventstore.go
Normal file
10
internal/repository/samlrequest/eventstore.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package samlrequest
|
||||||
|
|
||||||
|
import "github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, AddedType, eventstore.GenericEventMapper[AddedEvent])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, SessionLinkedType, eventstore.GenericEventMapper[SessionLinkedEvent])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, FailedType, eventstore.GenericEventMapper[FailedEvent])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, SucceededType, eventstore.GenericEventMapper[SucceededEvent])
|
||||||
|
}
|
172
internal/repository/samlrequest/saml_request.go
Normal file
172
internal/repository/samlrequest/saml_request.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package samlrequest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
samlRequestEventPrefix = "saml_request."
|
||||||
|
AddedType = samlRequestEventPrefix + "added"
|
||||||
|
FailedType = samlRequestEventPrefix + "failed"
|
||||||
|
SessionLinkedType = samlRequestEventPrefix + "session.linked"
|
||||||
|
SucceededType = samlRequestEventPrefix + "succeeded"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddedEvent struct {
|
||||||
|
*eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
LoginClient string `json:"login_client,omitempty"`
|
||||||
|
ApplicationID string `json:"application_id,omitempty"`
|
||||||
|
ACSURL string `json:"acs_url,omitempty"`
|
||||||
|
RelayState string `json:"relay_state,omitempty"`
|
||||||
|
RequestID string `json:"request_id,omitempty"`
|
||||||
|
Binding string `json:"binding,omitempty"`
|
||||||
|
Issuer string `json:"issuer,omitempty"`
|
||||||
|
Destination string `json:"destination,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddedEvent(ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
loginClient,
|
||||||
|
applicationID string,
|
||||||
|
acsURL string,
|
||||||
|
relayState string,
|
||||||
|
requestID string,
|
||||||
|
binding string,
|
||||||
|
issuer string,
|
||||||
|
destination string,
|
||||||
|
) *AddedEvent {
|
||||||
|
return &AddedEvent{
|
||||||
|
BaseEvent: eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
AddedType,
|
||||||
|
),
|
||||||
|
LoginClient: loginClient,
|
||||||
|
ApplicationID: applicationID,
|
||||||
|
ACSURL: acsURL,
|
||||||
|
RelayState: relayState,
|
||||||
|
RequestID: requestID,
|
||||||
|
Binding: binding,
|
||||||
|
Issuer: issuer,
|
||||||
|
Destination: destination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionLinkedEvent struct {
|
||||||
|
*eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
AuthTime time.Time `json:"auth_time"`
|
||||||
|
AuthMethods []domain.UserAuthMethodType `json:"auth_methods"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SessionLinkedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SessionLinkedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionLinkedEvent(ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
sessionID,
|
||||||
|
userID string,
|
||||||
|
authTime time.Time,
|
||||||
|
authMethods []domain.UserAuthMethodType,
|
||||||
|
) *SessionLinkedEvent {
|
||||||
|
return &SessionLinkedEvent{
|
||||||
|
BaseEvent: eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
SessionLinkedType,
|
||||||
|
),
|
||||||
|
SessionID: sessionID,
|
||||||
|
UserID: userID,
|
||||||
|
AuthTime: authTime,
|
||||||
|
AuthMethods: authMethods,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SessionLinkedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = event
|
||||||
|
}
|
||||||
|
|
||||||
|
type FailedEvent struct {
|
||||||
|
*eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
Reason domain.SAMLErrorReason `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FailedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFailedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
reason domain.SAMLErrorReason,
|
||||||
|
) *FailedEvent {
|
||||||
|
return &FailedEvent{
|
||||||
|
BaseEvent: eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
FailedType,
|
||||||
|
),
|
||||||
|
Reason: reason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FailedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = event
|
||||||
|
}
|
||||||
|
|
||||||
|
type SucceededEvent struct {
|
||||||
|
*eventstore.BaseEvent `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SucceededEvent) Payload() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SucceededEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSucceededEvent(ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
) *SucceededEvent {
|
||||||
|
return &SucceededEvent{
|
||||||
|
BaseEvent: eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
SucceededType,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SucceededEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = event
|
||||||
|
}
|
25
internal/repository/samlsession/aggregate.go
Normal file
25
internal/repository/samlsession/aggregate.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package samlsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AggregateType = "saml_session"
|
||||||
|
AggregateVersion = "v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Aggregate struct {
|
||||||
|
eventstore.Aggregate
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAggregate(id, resourceOwner string) *Aggregate {
|
||||||
|
return &Aggregate{
|
||||||
|
Aggregate: eventstore.Aggregate{
|
||||||
|
Type: AggregateType,
|
||||||
|
Version: AggregateVersion,
|
||||||
|
ID: id,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
12
internal/repository/samlsession/eventstore.go
Normal file
12
internal/repository/samlsession/eventstore.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package samlsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, AddedType, eventstore.GenericEventMapper[AddedEvent])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, SAMLResponseAddedType, eventstore.GenericEventMapper[SAMLResponseAddedEvent])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, SAMLResponseRevokedType, eventstore.GenericEventMapper[SAMLResponseRevokedEvent])
|
||||||
|
|
||||||
|
}
|
139
internal/repository/samlsession/saml_session.go
Normal file
139
internal/repository/samlsession/saml_session.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package samlsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
samlSessionEventPrefix = "saml_session."
|
||||||
|
AddedType = samlSessionEventPrefix + "added"
|
||||||
|
SAMLResponseAddedType = samlSessionEventPrefix + "saml_response.added"
|
||||||
|
SAMLResponseRevokedType = samlSessionEventPrefix + "saml_response.revoked"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
UserID string `json:"userID"`
|
||||||
|
UserResourceOwner string `json:"userResourceOwner"`
|
||||||
|
SessionID string `json:"sessionID"`
|
||||||
|
EntityID string `json:"entityID"`
|
||||||
|
Audience []string `json:"audience"`
|
||||||
|
AuthMethods []domain.UserAuthMethodType `json:"authMethods"`
|
||||||
|
AuthTime time.Time `json:"authTime"`
|
||||||
|
PreferredLanguage *language.Tag `json:"preferredLanguage,omitempty"`
|
||||||
|
UserAgent *domain.UserAgent `json:"userAgent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = *event
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddedEvent(ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
userID,
|
||||||
|
userResourceOwner,
|
||||||
|
sessionID,
|
||||||
|
entityID string,
|
||||||
|
audience []string,
|
||||||
|
authMethods []domain.UserAuthMethodType,
|
||||||
|
authTime time.Time,
|
||||||
|
preferredLanguage *language.Tag,
|
||||||
|
userAgent *domain.UserAgent,
|
||||||
|
) *AddedEvent {
|
||||||
|
return &AddedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
AddedType,
|
||||||
|
),
|
||||||
|
UserID: userID,
|
||||||
|
UserResourceOwner: userResourceOwner,
|
||||||
|
SessionID: sessionID,
|
||||||
|
EntityID: entityID,
|
||||||
|
Audience: audience,
|
||||||
|
AuthMethods: authMethods,
|
||||||
|
AuthTime: authTime,
|
||||||
|
PreferredLanguage: preferredLanguage,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SAMLResponseAddedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Lifetime time.Duration `json:"lifetime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SAMLResponseAddedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SAMLResponseAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SAMLResponseAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = *event
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSAMLResponseAddedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
id string,
|
||||||
|
lifetime time.Duration,
|
||||||
|
) *SAMLResponseAddedEvent {
|
||||||
|
return &SAMLResponseAddedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
SAMLResponseAddedType,
|
||||||
|
),
|
||||||
|
ID: id,
|
||||||
|
Lifetime: lifetime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SAMLResponseRevokedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SAMLResponseRevokedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SAMLResponseRevokedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SAMLResponseRevokedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = *event
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSAMLResponseRevokedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
) *SAMLResponseRevokedEvent {
|
||||||
|
return &SAMLResponseRevokedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
SAMLResponseRevokedType,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
@ -566,6 +566,13 @@ Errors:
|
|||||||
Token:
|
Token:
|
||||||
Invalid: Токенът е невалиден
|
Invalid: Токенът е невалиден
|
||||||
Expired: Токенът е изтекъл
|
Expired: Токенът е изтекъл
|
||||||
|
InvalidClient: Токенът не е издаден за този клиент
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest вече съществува
|
||||||
|
NotExisting: SAMLRequest не съществува
|
||||||
|
WrongLoginClient: SAMLRequest, създаден от друг клиент за влизане
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse не е издаден за този клиент
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Функцията не съществува
|
NotExisting: Функцията не съществува
|
||||||
TypeNotSupported: Типът функция не се поддържа
|
TypeNotSupported: Типът функция не се поддържа
|
||||||
@ -640,6 +647,8 @@ AggregateTypes:
|
|||||||
system: Система
|
system: Система
|
||||||
session: Сесия
|
session: Сесия
|
||||||
web_key: Уеб ключ
|
web_key: Уеб ключ
|
||||||
|
saml_request: SAML заявка
|
||||||
|
saml_session: SAML сесия
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -547,6 +547,12 @@ Errors:
|
|||||||
Invalid: Token je neplatný
|
Invalid: Token je neplatný
|
||||||
Expired: Token vypršel
|
Expired: Token vypršel
|
||||||
InvalidClient: Token nebyl vydán pro tohoto klienta
|
InvalidClient: Token nebyl vydán pro tohoto klienta
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest již existuje
|
||||||
|
NotExisting: SAMLRequest neexistuje
|
||||||
|
WrongLoginClient: SAMLRequest vytvořený jiným přihlašovacím klientem
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: Pro tohoto klienta nebyla vydána odpověď SAMLResponse
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Funkce neexistuje
|
NotExisting: Funkce neexistuje
|
||||||
TypeNotSupported: Typ funkce není podporován
|
TypeNotSupported: Typ funkce není podporován
|
||||||
@ -621,6 +627,8 @@ AggregateTypes:
|
|||||||
system: Systém
|
system: Systém
|
||||||
session: Sezení
|
session: Sezení
|
||||||
web_key: Webový klíč
|
web_key: Webový klíč
|
||||||
|
saml_request: Žádost SAML
|
||||||
|
saml_session: Relace SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: Token ist ungültig
|
Invalid: Token ist ungültig
|
||||||
Expired: Token ist abgelaufen
|
Expired: Token ist abgelaufen
|
||||||
InvalidClient: Token wurde nicht für diesen Client ausgestellt
|
InvalidClient: Token wurde nicht für diesen Client ausgestellt
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest existiert bereits
|
||||||
|
NotExisting: SAMLRequest existiert nicht
|
||||||
|
WrongLoginClient: SAMLRequest wurde con einem andere Login-Client erstellt
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse wurde nicht für diesen Client ausgestellt
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Feature existiert nicht
|
NotExisting: Feature existiert nicht
|
||||||
TypeNotSupported: Feature Typ wird nicht unterstützt
|
TypeNotSupported: Feature Typ wird nicht unterstützt
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: System
|
system: System
|
||||||
session: Session
|
session: Session
|
||||||
web_key: Webschlüssel
|
web_key: Webschlüssel
|
||||||
|
saml_request: SAML Request
|
||||||
|
saml_session: SAML Session
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -550,6 +550,12 @@ Errors:
|
|||||||
Invalid: Token is invalid
|
Invalid: Token is invalid
|
||||||
Expired: Token is expired
|
Expired: Token is expired
|
||||||
InvalidClient: Token was not issued for this client
|
InvalidClient: Token was not issued for this client
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest already exists
|
||||||
|
NotExisting: SAMLRequest does not exist
|
||||||
|
WrongLoginClient: SAMLRequest created by other login client
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse was not issued for this client
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Feature does not exist
|
NotExisting: Feature does not exist
|
||||||
TypeNotSupported: Feature type is not supported
|
TypeNotSupported: Feature type is not supported
|
||||||
@ -624,6 +630,8 @@ AggregateTypes:
|
|||||||
system: System
|
system: System
|
||||||
session: Session
|
session: Session
|
||||||
web_key: Web Key
|
web_key: Web Key
|
||||||
|
saml_request: SAML Request
|
||||||
|
saml_session: SAML Session
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: El token no es válido
|
Invalid: El token no es válido
|
||||||
Expired: El token ha caducado
|
Expired: El token ha caducado
|
||||||
InvalidClient: El token no ha sido emitido para este cliente
|
InvalidClient: El token no ha sido emitido para este cliente
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest ya existe
|
||||||
|
NotExisting: SAMLRequest no existe
|
||||||
|
WrongLoginClient: SAMLRequest creado por otro cliente de inicio de sesión
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse no ha sido emitido para este cliente
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: La característica no existe
|
NotExisting: La característica no existe
|
||||||
TypeNotSupported: El tipo de característica no es compatible
|
TypeNotSupported: El tipo de característica no es compatible
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: Sistema
|
system: Sistema
|
||||||
session: Sesión
|
session: Sesión
|
||||||
web_key: Clave web
|
web_key: Clave web
|
||||||
|
saml_request: Solicitud SAML
|
||||||
|
saml_session: Sesión SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: Le jeton n'est pas valide
|
Invalid: Le jeton n'est pas valide
|
||||||
Expired: Le jeton est expiré
|
Expired: Le jeton est expiré
|
||||||
InvalidClient: Le token n'a pas été émis pour ce client
|
InvalidClient: Le token n'a pas été émis pour ce client
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest existe déjà
|
||||||
|
NotExisting: SAMLRequest n'existe pas
|
||||||
|
WrongLoginClient: SAMLRequest créé par un autre client de connexion
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse n'a pas été émise pour ce client
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: La fonctionnalité n'existe pas
|
NotExisting: La fonctionnalité n'existe pas
|
||||||
TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge
|
TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: Système
|
system: Système
|
||||||
session: Session
|
session: Session
|
||||||
web_key: Clé Web
|
web_key: Clé Web
|
||||||
|
saml_request: Requête SAML
|
||||||
|
saml_session: Session SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: A Token érvénytelen
|
Invalid: A Token érvénytelen
|
||||||
Expired: A Token lejárt
|
Expired: A Token lejárt
|
||||||
InvalidClient: A Token nem ehhez a klienshez lett kiadva
|
InvalidClient: A Token nem ehhez a klienshez lett kiadva
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: A SAMLRequest már létezik
|
||||||
|
NotExisting: A SAMLRequest nem létezik
|
||||||
|
WrongLoginClient: A SAMLRequest egy másik bejelentkezési ügyfél által létrehozott
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse nem lett kiadva ehhez az ügyfélhez
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: A funkció nem létezik
|
NotExisting: A funkció nem létezik
|
||||||
TypeNotSupported: A funkció típusa nem támogatott
|
TypeNotSupported: A funkció típusa nem támogatott
|
||||||
@ -599,6 +605,7 @@ Errors:
|
|||||||
FeatureDisabled: A webkulcs funkció le van tiltva
|
FeatureDisabled: A webkulcs funkció le van tiltva
|
||||||
NoActive: Aktív web kulcs nem található
|
NoActive: Aktív web kulcs nem található
|
||||||
NotFound: Web kulcs nem található
|
NotFound: Web kulcs nem található
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Művelet
|
action: Művelet
|
||||||
instance: Példány
|
instance: Példány
|
||||||
@ -622,6 +629,9 @@ AggregateTypes:
|
|||||||
system: Rendszer
|
system: Rendszer
|
||||||
session: Munkamenet
|
session: Munkamenet
|
||||||
web_key: Webkulcs
|
web_key: Webkulcs
|
||||||
|
saml_request: SAML-kérés
|
||||||
|
saml_session: SAML munkamenet
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
set: Végrehajtási készlet
|
set: Végrehajtási készlet
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: Token tidak valid
|
Invalid: Token tidak valid
|
||||||
Expired: Token sudah habis masa berlakunya
|
Expired: Token sudah habis masa berlakunya
|
||||||
InvalidClient: Token tidak dikeluarkan untuk klien ini
|
InvalidClient: Token tidak dikeluarkan untuk klien ini
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest sudah ada
|
||||||
|
NotExisting: SAMLRequest tidak ada
|
||||||
|
WrongLoginClient: SAMLRequest dibuat oleh klien login lainnya
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse tidak dikeluarkan untuk klien ini
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Fitur tidak ada
|
NotExisting: Fitur tidak ada
|
||||||
TypeNotSupported: Jenis fitur tidak didukung
|
TypeNotSupported: Jenis fitur tidak didukung
|
||||||
@ -594,6 +600,7 @@ Errors:
|
|||||||
FeatureDisabled: Fitur kunci web dinonaktifkan
|
FeatureDisabled: Fitur kunci web dinonaktifkan
|
||||||
NoActive: Tidak ditemukan kunci web aktif
|
NoActive: Tidak ditemukan kunci web aktif
|
||||||
NotFound: Kunci web tidak ditemukan
|
NotFound: Kunci web tidak ditemukan
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Tindakan
|
action: Tindakan
|
||||||
instance: Contoh
|
instance: Contoh
|
||||||
@ -617,6 +624,9 @@ AggregateTypes:
|
|||||||
system: Sistem
|
system: Sistem
|
||||||
session: Sidang
|
session: Sidang
|
||||||
web_key: Kunci Web
|
web_key: Kunci Web
|
||||||
|
saml_request: Sesi SAML
|
||||||
|
saml_session: Permintaan SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
set: Kumpulan eksekusi
|
set: Kumpulan eksekusi
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: Token non è valido
|
Invalid: Token non è valido
|
||||||
Expired: Token è scaduto
|
Expired: Token è scaduto
|
||||||
InvalidClient: Il token non è stato emesso per questo cliente
|
InvalidClient: Il token non è stato emesso per questo cliente
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest esiste già
|
||||||
|
NotExisting: SAMLRequest non esiste
|
||||||
|
WrongLoginClient: SAMLRequest creato da un altro client di accesso
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse non è stato emesso per questo client
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: La funzionalità non esiste
|
NotExisting: La funzionalità non esiste
|
||||||
TypeNotSupported: Il tipo di funzionalità non è supportato
|
TypeNotSupported: Il tipo di funzionalità non è supportato
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: Sistema
|
system: Sistema
|
||||||
session: Sessione
|
session: Sessione
|
||||||
web_key: Chiave Web
|
web_key: Chiave Web
|
||||||
|
saml_request: Richiesta SAML
|
||||||
|
saml_session: Sessione SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -538,6 +538,12 @@ Errors:
|
|||||||
Invalid: トークンが無効です
|
Invalid: トークンが無効です
|
||||||
Expired: トークンの有効期限が切れている
|
Expired: トークンの有効期限が切れている
|
||||||
InvalidClient: トークンが発行されていません
|
InvalidClient: トークンが発行されていません
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLリクエストはすでに存在します
|
||||||
|
NotExisting: SAMLリクエストが存在しません
|
||||||
|
WrongLoginClient: 他のログイン クライアントによって作成された SAMLRequest
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: このクライアントに対してSAMLResponseは発行されませんでした
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: 機能が存在しません
|
NotExisting: 機能が存在しません
|
||||||
TypeNotSupported: 機能タイプはサポートされていません
|
TypeNotSupported: 機能タイプはサポートされていません
|
||||||
@ -612,6 +618,8 @@ AggregateTypes:
|
|||||||
system: システム
|
system: システム
|
||||||
session: セッション
|
session: セッション
|
||||||
web_key: Web キー
|
web_key: Web キー
|
||||||
|
saml_request: SAML リクエスト
|
||||||
|
saml_session: SAMLセッション
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -550,6 +550,12 @@ Errors:
|
|||||||
Invalid: 토큰이 유효하지 않습니다
|
Invalid: 토큰이 유효하지 않습니다
|
||||||
Expired: 토큰이 만료되었습니다
|
Expired: 토큰이 만료되었습니다
|
||||||
InvalidClient: 토큰이 이 클라이언트에 대해 발행되지 않았습니다
|
InvalidClient: 토큰이 이 클라이언트에 대해 발행되지 않았습니다
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest가 이미 존재합니다
|
||||||
|
NotExisting: SAMLRequest가 존재하지 않습니다
|
||||||
|
WrongLoginClient: 다른 로그인 클라이언트가 생성한 SAMLRequest
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: 이 클라이언트에 대해 SAMLResponse가 발행되지 않았습니다.
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: 기능이 존재하지 않습니다
|
NotExisting: 기능이 존재하지 않습니다
|
||||||
TypeNotSupported: 기능 유형이 지원되지 않습니다
|
TypeNotSupported: 기능 유형이 지원되지 않습니다
|
||||||
@ -624,6 +630,8 @@ AggregateTypes:
|
|||||||
system: 시스템
|
system: 시스템
|
||||||
session: 세션
|
session: 세션
|
||||||
web_key: 웹 키
|
web_key: 웹 키
|
||||||
|
saml_request: SAML 요청
|
||||||
|
saml_session: SAML 세션
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -548,6 +548,12 @@ Errors:
|
|||||||
Invalid: токенот е неважечки
|
Invalid: токенот е неважечки
|
||||||
Expired: токенот е истечен
|
Expired: токенот е истечен
|
||||||
InvalidClient: Токен не беше издаден на овој клиент
|
InvalidClient: Токен не беше издаден на овој клиент
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest веќе постои
|
||||||
|
NotExisting: SAMLRequest не постои
|
||||||
|
WrongLoginClient: SAML Барање создадено од друг клиент за најавување
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse не беше издаден за овој клиент
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Функцијата не постои
|
NotExisting: Функцијата не постои
|
||||||
TypeNotSupported: Типот на функција не е поддржан
|
TypeNotSupported: Типот на функција не е поддржан
|
||||||
@ -622,6 +628,8 @@ AggregateTypes:
|
|||||||
system: Систем
|
system: Систем
|
||||||
session: Сесија
|
session: Сесија
|
||||||
web_key: Веб клуч
|
web_key: Веб клуч
|
||||||
|
saml_request: Барање SAML
|
||||||
|
saml_session: SAML сесија
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: Token is ongeldig
|
Invalid: Token is ongeldig
|
||||||
Expired: Token is verlopen
|
Expired: Token is verlopen
|
||||||
InvalidClient: Token is niet uitgegeven voor deze client
|
InvalidClient: Token is niet uitgegeven voor deze client
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest bestaat al
|
||||||
|
NotExisting: SAMLRequest bestaat niet
|
||||||
|
WrongLoginClient: SAMLRequest aangemaakt door andere login client
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse is niet uitgegeven voor deze client
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Functie bestaat niet
|
NotExisting: Functie bestaat niet
|
||||||
TypeNotSupported: Functie type wordt niet ondersteund
|
TypeNotSupported: Functie type wordt niet ondersteund
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: Systeem
|
system: Systeem
|
||||||
session: Sessie
|
session: Sessie
|
||||||
web_key: Websleutel
|
web_key: Websleutel
|
||||||
|
saml_request: SAML-aanvraag
|
||||||
|
saml_session: SAML-sessie
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: Token jest nieprawidłowy
|
Invalid: Token jest nieprawidłowy
|
||||||
Expired: Token wygasł
|
Expired: Token wygasł
|
||||||
InvalidClient: Token nie został wydany dla tego klienta
|
InvalidClient: Token nie został wydany dla tego klienta
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest już istnieje
|
||||||
|
NotExisting: SAMLRequest nie istnieje
|
||||||
|
WrongLoginClient: SAMLRequest utworzony przez innego klienta logowania
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse nie został wydany dla tego klienta
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Funkcja nie istnieje
|
NotExisting: Funkcja nie istnieje
|
||||||
TypeNotSupported: Typ funkcji nie jest obsługiwany
|
TypeNotSupported: Typ funkcji nie jest obsługiwany
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: System
|
system: System
|
||||||
session: Sesja
|
session: Sesja
|
||||||
web_key: Klucz internetowy
|
web_key: Klucz internetowy
|
||||||
|
saml_request: Żądanie SAML
|
||||||
|
saml_session: Sesja SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -544,6 +544,16 @@ Errors:
|
|||||||
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
|
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
|
||||||
OIDCSession:
|
OIDCSession:
|
||||||
RefreshTokenInvalid: O Refresh Token é inválido
|
RefreshTokenInvalid: O Refresh Token é inválido
|
||||||
|
Token:
|
||||||
|
Invalid: O token é inválido
|
||||||
|
Expired: O token expirou
|
||||||
|
InvalidClient: O token não foi emitido para este cliente
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: O SAMLRequest já existe
|
||||||
|
NotExisting: O SAMLRequest não existe
|
||||||
|
WrongLoginClient: SAMLRequest criado por outro cliente de login
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: O SAMLResponse não foi emitido para este cliente
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: O recurso não existe
|
NotExisting: O recurso não existe
|
||||||
TypeNotSupported: O tipo de recurso não é compatível
|
TypeNotSupported: O tipo de recurso não é compatível
|
||||||
@ -618,6 +628,8 @@ AggregateTypes:
|
|||||||
system: Sistema
|
system: Sistema
|
||||||
session: Sessão
|
session: Sessão
|
||||||
web_key: Chave da Web
|
web_key: Chave da Web
|
||||||
|
saml_request: Solicitação SAML
|
||||||
|
saml_session: Sessão SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -538,6 +538,12 @@ Errors:
|
|||||||
Invalid: Токен недействителен
|
Invalid: Токен недействителен
|
||||||
Expired: Срок действия токена истек
|
Expired: Срок действия токена истек
|
||||||
InvalidClient: Токен не был выпущен для этого клиента
|
InvalidClient: Токен не был выпущен для этого клиента
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest уже существует
|
||||||
|
NotExisting: SAMLRequest не существует
|
||||||
|
WrongLoginClient: SAMLRequest создан другим клиентом входа
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse не был отправлен для этого клиента
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: ункция не существует
|
NotExisting: ункция не существует
|
||||||
TypeNotSupported: Тип объекта не поддерживается
|
TypeNotSupported: Тип объекта не поддерживается
|
||||||
@ -612,6 +618,8 @@ AggregateTypes:
|
|||||||
system: Система
|
system: Система
|
||||||
session: Сеанс
|
session: Сеанс
|
||||||
web_key: Веб-ключ
|
web_key: Веб-ключ
|
||||||
|
saml_request: SAML-запрос
|
||||||
|
saml_session: Сессия SAML
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: Token är ogiltig
|
Invalid: Token är ogiltig
|
||||||
Expired: Token har gått ut
|
Expired: Token har gått ut
|
||||||
InvalidClient: Token utfärdades inte för denna klient
|
InvalidClient: Token utfärdades inte för denna klient
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest finns redan
|
||||||
|
NotExisting: SAMLRequest finns inte
|
||||||
|
WrongLoginClient: SAMLRequest skapad av annan inloggningsklient
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: SAMLResponse utfärdades inte för den här klienten
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: Funktionen existerar inte
|
NotExisting: Funktionen existerar inte
|
||||||
TypeNotSupported: Funktionstypen stöds inte
|
TypeNotSupported: Funktionstypen stöds inte
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: System
|
system: System
|
||||||
session: Session
|
session: Session
|
||||||
web_key: Webbnyckel
|
web_key: Webbnyckel
|
||||||
|
saml_request: SAML-förfrågan
|
||||||
|
saml_session: SAML-session
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
@ -549,6 +549,12 @@ Errors:
|
|||||||
Invalid: 令牌无效
|
Invalid: 令牌无效
|
||||||
Expired: 令牌已过期
|
Expired: 令牌已过期
|
||||||
InvalidClient: 没有为该客户发放令牌
|
InvalidClient: 没有为该客户发放令牌
|
||||||
|
SAMLRequest:
|
||||||
|
AlreadyExists: SAMLRequest 已存在
|
||||||
|
NotExisting: SAMLRequest不存在
|
||||||
|
WrongLoginClient: 其他登录客户端创建的 SAMLRequest
|
||||||
|
SAMLSession:
|
||||||
|
InvalidClient: 未向该客户端发出 SAMLResponse
|
||||||
Feature:
|
Feature:
|
||||||
NotExisting: 功能不存在
|
NotExisting: 功能不存在
|
||||||
TypeNotSupported: 不支持功能类型
|
TypeNotSupported: 不支持功能类型
|
||||||
@ -623,6 +629,8 @@ AggregateTypes:
|
|||||||
system: 系统
|
system: 系统
|
||||||
session: 会话
|
session: 会话
|
||||||
web_key: Web 密钥
|
web_key: Web 密钥
|
||||||
|
saml_request: SAML 请求
|
||||||
|
saml_session: SAML 会话
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
execution:
|
execution:
|
||||||
|
3
pkg/grpc/saml/v2/saml.go
Normal file
3
pkg/grpc/saml/v2/saml.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package saml
|
||||||
|
|
||||||
|
type Redirect = isCreateResponseRequest_ResponseKind
|
@ -169,8 +169,8 @@ message CreateCallbackRequest {
|
|||||||
string auth_request_id = 1 [
|
string auth_request_id = 1 [
|
||||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
description: "Set this field when the authorization flow failed. It creates a callback URL to the application, with the error details set.";
|
description: "ID of the Auth Request.";
|
||||||
ref: "https://openid.net/specs/openid-connect-core-1_0.html#AuthError";
|
example: "\"163840776835432705\"";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
71
proto/zitadel/saml/v2/authorization.proto
Normal file
71
proto/zitadel/saml/v2/authorization.proto
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.saml.v2;
|
||||||
|
|
||||||
|
import "google/protobuf/duration.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/saml/v2;saml";
|
||||||
|
|
||||||
|
message SAMLRequest{
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
|
||||||
|
external_docs: {
|
||||||
|
url: "https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html";
|
||||||
|
description: "Find out more about SAMLRequest parameters";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
string id = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "ID of the SAMLRequest";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
google.protobuf.Timestamp creation_date = 2 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "Time when the SAMLRequest was created";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
string issuer = 3 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "SAML entityID of the application that created the SAMLRequest";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
string assertion_consumer_service = 4 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "URL which points back to the assertion consumer service of the application";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
string relay_state = 5 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "RelayState provided by the application for the request";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
string binding = 6 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "Binding used by the application for the request";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthorizationError {
|
||||||
|
ErrorReason error = 1;
|
||||||
|
optional string error_description = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ErrorReason {
|
||||||
|
ERROR_REASON_UNSPECIFIED = 0;
|
||||||
|
|
||||||
|
ERROR_REASON_VERSION_MISSMATCH = 1;
|
||||||
|
ERROR_REASON_AUTH_N_FAILED = 2;
|
||||||
|
ERROR_REASON_INVALID_ATTR_NAME_OR_VALUE = 3;
|
||||||
|
ERROR_REASON_INVALID_NAMEID_POLICY = 4;
|
||||||
|
ERROR_REASON_REQUEST_DENIED =5;
|
||||||
|
ERROR_REASON_REQUEST_UNSUPPORTED = 6;
|
||||||
|
ERROR_REASON_UNSUPPORTED_BINDING = 7;
|
||||||
|
}
|
227
proto/zitadel/saml/v2/saml_service.proto
Normal file
227
proto/zitadel/saml/v2/saml_service.proto
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.saml.v2;
|
||||||
|
|
||||||
|
import "zitadel/object/v2/object.proto";
|
||||||
|
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||||
|
import "zitadel/saml/v2/authorization.proto";
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/saml/v2;saml";
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||||
|
info: {
|
||||||
|
title: "SAML Service";
|
||||||
|
version: "2.0";
|
||||||
|
description: "Get SAML Auth Request details and create callback URLs.";
|
||||||
|
contact:{
|
||||||
|
name: "ZITADEL"
|
||||||
|
url: "https://zitadel.com"
|
||||||
|
email: "hi@zitadel.com"
|
||||||
|
}
|
||||||
|
license: {
|
||||||
|
name: "Apache 2.0",
|
||||||
|
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
schemes: HTTPS;
|
||||||
|
schemes: HTTP;
|
||||||
|
|
||||||
|
consumes: "application/json";
|
||||||
|
consumes: "application/grpc";
|
||||||
|
|
||||||
|
produces: "application/json";
|
||||||
|
produces: "application/grpc";
|
||||||
|
|
||||||
|
consumes: "application/grpc-web+proto";
|
||||||
|
produces: "application/grpc-web+proto";
|
||||||
|
|
||||||
|
host: "$CUSTOM-DOMAIN";
|
||||||
|
base_path: "/";
|
||||||
|
|
||||||
|
external_docs: {
|
||||||
|
description: "Detailed information about ZITADEL",
|
||||||
|
url: "https://zitadel.com/docs"
|
||||||
|
}
|
||||||
|
security_definitions: {
|
||||||
|
security: {
|
||||||
|
key: "OAuth2";
|
||||||
|
value: {
|
||||||
|
type: TYPE_OAUTH2;
|
||||||
|
flow: FLOW_ACCESS_CODE;
|
||||||
|
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
|
||||||
|
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
|
||||||
|
scopes: {
|
||||||
|
scope: {
|
||||||
|
key: "openid";
|
||||||
|
value: "openid";
|
||||||
|
}
|
||||||
|
scope: {
|
||||||
|
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||||
|
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
security: {
|
||||||
|
security_requirement: {
|
||||||
|
key: "OAuth2";
|
||||||
|
value: {
|
||||||
|
scope: "openid";
|
||||||
|
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses: {
|
||||||
|
key: "403";
|
||||||
|
value: {
|
||||||
|
description: "Returned when the user does not have permission to access the resource.";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/rpcStatus";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses: {
|
||||||
|
key: "404";
|
||||||
|
value: {
|
||||||
|
description: "Returned when the resource does not exist.";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/rpcStatus";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
service SAMLService {
|
||||||
|
rpc GetSAMLRequest (GetSAMLRequestRequest) returns (GetSAMLRequestResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/v2/saml/saml_requests/{saml_request_id}"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Get SAML Request details";
|
||||||
|
description: "Get SAML Request details by ID. Returns details that are parsed from the application's SAML Request."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc CreateResponse (CreateResponseRequest) returns (CreateResponseResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v2/saml/saml_requests/{saml_request_id}"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Finalize a SAML Request and get the response.";
|
||||||
|
description: "Finalize a SAML Request and get the response definition for success or failure. The response must be handled as per the SAML definition to inform the application about the success or failure. On success, the response contains details for the application to obtain the SAMLResponse. This method can only be called once for an SAML request."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetSAMLRequestRequest {
|
||||||
|
// ID of the SAML Request, as obtained from the redirect URL.
|
||||||
|
string saml_request_id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"163840776835432705\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetSAMLRequestResponse {
|
||||||
|
SAMLRequest saml_request = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateResponseRequest {
|
||||||
|
// ID of the SAML Request.
|
||||||
|
string saml_request_id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"163840776835432705\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
oneof response_kind {
|
||||||
|
option (validate.required) = true;
|
||||||
|
Session session = 2;
|
||||||
|
// Set this field when the authorization flow failed. It creates a response depending on the SP, with the error details set.
|
||||||
|
AuthorizationError error = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Session {
|
||||||
|
// ID of the session, used to login the user. Connects the session to the SAML Request.
|
||||||
|
string session_id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"163840776835432705\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Token to verify the session is valid.
|
||||||
|
string session_token = 2 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateResponseResponse {
|
||||||
|
zitadel.object.v2.Details details = 1;
|
||||||
|
// URL including the Assertion Consumer Service where the user should be redirected or has to call per POST, depending on the binding. Contains details for the application to obtain the response on success, or error details on failure. Note that this field must be treated as credentials, as the contained SAMLResponse or code can be used on behalve of the user.
|
||||||
|
string url = 2 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"https://client.example.org/cb\""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Binding is defined through the request, what the IDP is able to use and what bindings are available for the SP.
|
||||||
|
oneof binding {
|
||||||
|
// Set if the binding is Redirect-Binding, where the user can directly be redirected to the application, using a \"302 FOUND\" status to the URL.
|
||||||
|
RedirectResponse redirect = 3;
|
||||||
|
// Set if the binding is POST-Binding, where the application expects to be called per HTTP POST with the SAMLResponse and RelayState in the form body.
|
||||||
|
PostResponse post = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message RedirectResponse{}
|
||||||
|
message PostResponse{
|
||||||
|
string relay_state = 1;
|
||||||
|
string saml_response = 2;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user