Merge branch 'main' into fix-project-grant-owners

This commit is contained in:
adlerhurst
2024-12-19 14:35:54 +01:00
57 changed files with 3947 additions and 22 deletions

View 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())
}
}
})
}
}

View 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
}
}

View 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
}

View 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
}
}

View 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 != ""
}

View File

@@ -24,7 +24,13 @@ const (
)
type Config struct {
ProviderConfig *provider.Config
ProviderConfig *provider.Config
DefaultLoginURLV2 string
}
type Provider struct {
*provider.Provider
command *command.Commands
}
func NewProvider(
@@ -40,7 +46,7 @@ func NewProvider(
instanceHandler,
userAgentCookie func(http.Handler) http.Handler,
accessHandler *middleware.AccessInterceptor,
) (*provider.Provider, error) {
) (*Provider, error) {
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
provStorage, err := newStorage(
@@ -51,6 +57,8 @@ func NewProvider(
certEncAlg,
es,
projections,
fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
conf.DefaultLoginURLV2,
)
if err != nil {
return nil, err
@@ -73,12 +81,19 @@ func NewProvider(
options = append(options, provider.WithAllowInsecure())
}
return provider.NewProvider(
p, err := provider.NewProvider(
provStorage,
HandlerPrefix,
conf.ProviderConfig,
options...,
)
if err != nil {
return nil, err
}
return &Provider{
p,
command,
}, nil
}
func newStorage(
@@ -89,16 +104,19 @@ func newStorage(
certEncAlg crypto.EncryptionAlgorithm,
es *eventstore.Eventstore,
db *database.DB,
defaultLoginURL string,
defaultLoginURLV2 string,
) (*Storage, error) {
return &Storage{
encAlg: encAlg,
certEncAlg: certEncAlg,
locker: crdb.NewLocker(db.DB, locksTable, signingKey),
eventstore: es,
repo: repo,
command: command,
query: query,
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
encAlg: encAlg,
certEncAlg: certEncAlg,
locker: crdb.NewLocker(db.DB, locksTable, signingKey),
eventstore: es,
repo: repo,
command: command,
query: query,
defaultLoginURL: defaultLoginURL,
defaultLoginURLv2: defaultLoginURLV2,
}, nil
}

View File

@@ -3,6 +3,7 @@ package saml
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/dop251/goja"
@@ -16,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/actions/object"
"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/auth/repository"
"github.com/zitadel/zitadel/internal/command"
@@ -33,6 +35,10 @@ var _ provider.IdentityProviderStorage = &Storage{}
var _ provider.AuthStorage = &Storage{}
var _ provider.UserStorage = &Storage{}
const (
LoginClientHeader = "x-zitadel-login-client"
)
type Storage struct {
certChan <-chan interface{}
defaultCertificateLifetime time.Duration
@@ -51,7 +57,8 @@ type Storage struct {
command *command.Commands
query *query.Queries
defaultLoginURL string
defaultLoginURL string
defaultLoginURLv2 string
}
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{
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) {
ctx, span := tracing.NewSpan(ctx)
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)
if !ok {
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) {
ctx, span := tracing.NewSpan(ctx)
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)
if !ok {
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-D3g21", "no user agent id")

View 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
}

View 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")
}

View 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)
})
}
}

View 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
}

View 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()
}

View 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)
})
}
}

View 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"
}
}

View File

@@ -0,0 +1,10 @@
package domain
type SAMLRequestState int
const (
SAMLRequestStateUnspecified SAMLRequestState = iota
SAMLRequestStateAdded
SAMLRequestStateFailed
SAMLRequestStateSucceeded
)

View File

@@ -0,0 +1,9 @@
package domain
type SAMLSessionState int32
const (
SAMLSessionStateUnspecified SAMLSessionState = iota
SAMLSessionStateActive
SAMLSessionStateTerminated
)

View File

@@ -34,6 +34,7 @@ import (
user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/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"
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
@@ -65,6 +66,7 @@ type Client struct {
WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient
IDPv2 idp_pb.IdentityProviderServiceClient
UserV3Alpha user_v3alpha.ZITADELUsersClient
SAMLv2 saml_pb.SAMLServiceClient
}
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),
IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
SAMLv2: saml_pb.NewSAMLServiceClient(cc),
}
return client, client.pollHealth(ctx)
}

View 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)
}

View File

@@ -69,6 +69,7 @@ var (
DeviceAuthProjection *handler.Handler
SessionProjection *handler.Handler
AuthRequestProjection *handler.Handler
SamlRequestProjection *handler.Handler
MilestoneProjection *handler.Handler
QuotaProjection *quotaProjection
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"]))
SessionProjection = newSessionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["sessions"]))
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"]))
QuotaProjection = newQuotaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["quotas"]))
LimitsProjection = newLimitsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["limits"]))
@@ -286,6 +288,7 @@ func newProjectionsList() {
DeviceAuthProjection,
SessionProjection,
AuthRequestProjection,
SamlRequestProjection,
MilestoneProjection,
QuotaProjection.handler,
LimitsProjection,

View 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
}

View 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)
})
}
}

View 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
}

View 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;

View 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)
})
})
}
}

View 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,
},
}
}

View 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])
}

View 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
}

View 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,
},
}
}

View 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])
}

View 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,
),
}
}

View File

@@ -566,6 +566,13 @@ Errors:
Token:
Invalid: Токенът е невалиден
Expired: Токенът е изтекъл
InvalidClient: Токенът не е издаден за този клиент
SAMLRequest:
AlreadyExists: SAMLRequest вече съществува
NotExisting: SAMLRequest не съществува
WrongLoginClient: SAMLRequest, създаден от друг клиент за влизане
SAMLSession:
InvalidClient: SAMLResponse не е издаден за този клиент
Feature:
NotExisting: Функцията не съществува
TypeNotSupported: Типът функция не се поддържа
@@ -640,6 +647,8 @@ AggregateTypes:
system: Система
session: Сесия
web_key: Уеб ключ
saml_request: SAML заявка
saml_session: SAML сесия
EventTypes:
execution:

View File

@@ -547,6 +547,12 @@ Errors:
Invalid: Token je neplatný
Expired: Token vypršel
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:
NotExisting: Funkce neexistuje
TypeNotSupported: Typ funkce není podporován
@@ -621,6 +627,8 @@ AggregateTypes:
system: Systém
session: Sezení
web_key: Webový klíč
saml_request: Žádost SAML
saml_session: Relace SAML
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: Token ist ungültig
Expired: Token ist abgelaufen
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:
NotExisting: Feature existiert nicht
TypeNotSupported: Feature Typ wird nicht unterstützt
@@ -623,6 +629,8 @@ AggregateTypes:
system: System
session: Session
web_key: Webschlüssel
saml_request: SAML Request
saml_session: SAML Session
EventTypes:
execution:

View File

@@ -550,6 +550,12 @@ Errors:
Invalid: Token is invalid
Expired: Token is expired
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:
NotExisting: Feature does not exist
TypeNotSupported: Feature type is not supported
@@ -624,6 +630,8 @@ AggregateTypes:
system: System
session: Session
web_key: Web Key
saml_request: SAML Request
saml_session: SAML Session
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: El token no es válido
Expired: El token ha caducado
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:
NotExisting: La característica no existe
TypeNotSupported: El tipo de característica no es compatible
@@ -623,6 +629,8 @@ AggregateTypes:
system: Sistema
session: Sesión
web_key: Clave web
saml_request: Solicitud SAML
saml_session: Sesión SAML
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: Le jeton n'est pas valide
Expired: Le jeton est expiré
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:
NotExisting: La fonctionnalité n'existe pas
TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge
@@ -623,6 +629,8 @@ AggregateTypes:
system: Système
session: Session
web_key: Clé Web
saml_request: Requête SAML
saml_session: Session SAML
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: A Token érvénytelen
Expired: A Token lejárt
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:
NotExisting: A funkció nem létezik
TypeNotSupported: A funkció típusa nem támogatott
@@ -599,6 +605,7 @@ Errors:
FeatureDisabled: A webkulcs funkció le van tiltva
NoActive: Aktív web kulcs nem található
NotFound: Web kulcs nem található
AggregateTypes:
action: Művelet
instance: Példány
@@ -622,6 +629,9 @@ AggregateTypes:
system: Rendszer
session: Munkamenet
web_key: Webkulcs
saml_request: SAML-kérés
saml_session: SAML munkamenet
EventTypes:
execution:
set: Végrehajtási készlet

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: Token tidak valid
Expired: Token sudah habis masa berlakunya
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:
NotExisting: Fitur tidak ada
TypeNotSupported: Jenis fitur tidak didukung
@@ -594,6 +600,7 @@ Errors:
FeatureDisabled: Fitur kunci web dinonaktifkan
NoActive: Tidak ditemukan kunci web aktif
NotFound: Kunci web tidak ditemukan
AggregateTypes:
action: Tindakan
instance: Contoh
@@ -617,6 +624,9 @@ AggregateTypes:
system: Sistem
session: Sidang
web_key: Kunci Web
saml_request: Sesi SAML
saml_session: Permintaan SAML
EventTypes:
execution:
set: Kumpulan eksekusi

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: Token non è valido
Expired: Token è scaduto
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:
NotExisting: La funzionalità non esiste
TypeNotSupported: Il tipo di funzionalità non è supportato
@@ -623,6 +629,8 @@ AggregateTypes:
system: Sistema
session: Sessione
web_key: Chiave Web
saml_request: Richiesta SAML
saml_session: Sessione SAML
EventTypes:
execution:

View File

@@ -538,6 +538,12 @@ Errors:
Invalid: トークンが無効です
Expired: トークンの有効期限が切れている
InvalidClient: トークンが発行されていません
SAMLRequest:
AlreadyExists: SAMLリクエストはすでに存在します
NotExisting: SAMLリクエストが存在しません
WrongLoginClient: 他のログイン クライアントによって作成された SAMLRequest
SAMLSession:
InvalidClient: このクライアントに対してSAMLResponseは発行されませんでした
Feature:
NotExisting: 機能が存在しません
TypeNotSupported: 機能タイプはサポートされていません
@@ -612,6 +618,8 @@ AggregateTypes:
system: システム
session: セッション
web_key: Web キー
saml_request: SAML リクエスト
saml_session: SAMLセッション
EventTypes:
execution:

View File

@@ -550,6 +550,12 @@ Errors:
Invalid: 토큰이 유효하지 않습니다
Expired: 토큰이 만료되었습니다
InvalidClient: 토큰이 이 클라이언트에 대해 발행되지 않았습니다
SAMLRequest:
AlreadyExists: SAMLRequest가 이미 존재합니다
NotExisting: SAMLRequest가 존재하지 않습니다
WrongLoginClient: 다른 로그인 클라이언트가 생성한 SAMLRequest
SAMLSession:
InvalidClient: 이 클라이언트에 대해 SAMLResponse가 발행되지 않았습니다.
Feature:
NotExisting: 기능이 존재하지 않습니다
TypeNotSupported: 기능 유형이 지원되지 않습니다
@@ -624,6 +630,8 @@ AggregateTypes:
system: 시스템
session: 세션
web_key: 웹 키
saml_request: SAML 요청
saml_session: SAML 세션
EventTypes:
execution:

View File

@@ -548,6 +548,12 @@ Errors:
Invalid: токенот е неважечки
Expired: токенот е истечен
InvalidClient: Токен не беше издаден на овој клиент
SAMLRequest:
AlreadyExists: SAMLRequest веќе постои
NotExisting: SAMLRequest не постои
WrongLoginClient: SAML Барање создадено од друг клиент за најавување
SAMLSession:
InvalidClient: SAMLResponse не беше издаден за овој клиент
Feature:
NotExisting: Функцијата не постои
TypeNotSupported: Типот на функција не е поддржан
@@ -622,6 +628,8 @@ AggregateTypes:
system: Систем
session: Сесија
web_key: Веб клуч
saml_request: Барање SAML
saml_session: SAML сесија
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: Token is ongeldig
Expired: Token is verlopen
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:
NotExisting: Functie bestaat niet
TypeNotSupported: Functie type wordt niet ondersteund
@@ -623,6 +629,8 @@ AggregateTypes:
system: Systeem
session: Sessie
web_key: Websleutel
saml_request: SAML-aanvraag
saml_session: SAML-sessie
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: Token jest nieprawidłowy
Expired: Token wygasł
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:
NotExisting: Funkcja nie istnieje
TypeNotSupported: Typ funkcji nie jest obsługiwany
@@ -623,6 +629,8 @@ AggregateTypes:
system: System
session: Sesja
web_key: Klucz internetowy
saml_request: Żądanie SAML
saml_session: Sesja SAML
EventTypes:
execution:

View File

@@ -544,6 +544,16 @@ Errors:
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
OIDCSession:
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:
NotExisting: O recurso não existe
TypeNotSupported: O tipo de recurso não é compatível
@@ -618,6 +628,8 @@ AggregateTypes:
system: Sistema
session: Sessão
web_key: Chave da Web
saml_request: Solicitação SAML
saml_session: Sessão SAML
EventTypes:
execution:

View File

@@ -538,6 +538,12 @@ Errors:
Invalid: Токен недействителен
Expired: Срок действия токена истек
InvalidClient: Токен не был выпущен для этого клиента
SAMLRequest:
AlreadyExists: SAMLRequest уже существует
NotExisting: SAMLRequest не существует
WrongLoginClient: SAMLRequest создан другим клиентом входа
SAMLSession:
InvalidClient: SAMLResponse не был отправлен для этого клиента
Feature:
NotExisting: ункция не существует
TypeNotSupported: Тип объекта не поддерживается
@@ -612,6 +618,8 @@ AggregateTypes:
system: Система
session: Сеанс
web_key: Веб-ключ
saml_request: SAML-запрос
saml_session: Сессия SAML
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: Token är ogiltig
Expired: Token har gått ut
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:
NotExisting: Funktionen existerar inte
TypeNotSupported: Funktionstypen stöds inte
@@ -623,6 +629,8 @@ AggregateTypes:
system: System
session: Session
web_key: Webbnyckel
saml_request: SAML-förfrågan
saml_session: SAML-session
EventTypes:
execution:

View File

@@ -549,6 +549,12 @@ Errors:
Invalid: 令牌无效
Expired: 令牌已过期
InvalidClient: 没有为该客户发放令牌
SAMLRequest:
AlreadyExists: SAMLRequest 已存在
NotExisting: SAMLRequest不存在
WrongLoginClient: 其他登录客户端创建的 SAMLRequest
SAMLSession:
InvalidClient: 未向该客户端发出 SAMLResponse
Feature:
NotExisting: 功能不存在
TypeNotSupported: 不支持功能类型
@@ -623,6 +629,8 @@ AggregateTypes:
system: 系统
session: 会话
web_key: Web 密钥
saml_request: SAML 请求
saml_session: SAML 会话
EventTypes:
execution: