mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-07 06:52:20 +00:00
Merge branch 'main' into fix-project-grant-owners
This commit is contained in:
367
internal/api/grpc/saml/v2/integration/saml_test.go
Normal file
367
internal/api/grpc/saml/v2/integration/saml_test.go
Normal file
@@ -0,0 +1,367 @@
|
||||
//go:build integration
|
||||
|
||||
package saml_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
Instance *integration.Instance
|
||||
Client saml_pb.SAMLServiceClient
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Instance = integration.NewInstance(ctx)
|
||||
Client = Instance.Client.SAMLv2
|
||||
|
||||
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func TestServer_GetAuthRequest(t *testing.T) {
|
||||
rootURL := "https://sp.example.com"
|
||||
idpMetadata, err := Instance.GetSAMLIDPMetadata()
|
||||
require.NoError(t, err)
|
||||
spMiddlewareRedirect, err := integration.CreateSAMLSP(rootURL, idpMetadata, saml.HTTPRedirectBinding)
|
||||
require.NoError(t, err)
|
||||
spMiddlewarePost, err := integration.CreateSAMLSP(rootURL, idpMetadata, saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
|
||||
acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0]
|
||||
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
|
||||
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewareRedirect)
|
||||
require.NoError(t, err)
|
||||
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewarePost)
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dep func() (string, error)
|
||||
want *oidc_pb.GetAuthRequestResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
dep: func() (string, error) {
|
||||
return "123", nil
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success, redirect binding",
|
||||
dep: func() (string, error) {
|
||||
return Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success, post binding",
|
||||
dep: func() (string, error) {
|
||||
return Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authRequestID, err := tt.dep()
|
||||
require.NoError(t, err)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := Client.GetSAMLRequest(CTX, &saml_pb.GetSAMLRequestRequest{
|
||||
SamlRequestId: authRequestID,
|
||||
})
|
||||
if tt.wantErr {
|
||||
assert.Error(ttt, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(ttt, err)
|
||||
authRequest := got.GetSamlRequest()
|
||||
assert.NotNil(ttt, authRequest)
|
||||
assert.Equal(ttt, authRequestID, authRequest.GetId())
|
||||
assert.WithinRange(ttt, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second))
|
||||
}, retryDuration, tick, "timeout waiting for expected saml request result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreateResponse(t *testing.T) {
|
||||
idpMetadata, err := Instance.GetSAMLIDPMetadata()
|
||||
require.NoError(t, err)
|
||||
rootURLRedirect := "spredirect.example.com"
|
||||
spMiddlewareRedirect, err := integration.CreateSAMLSP("https://"+rootURLRedirect, idpMetadata, saml.HTTPRedirectBinding)
|
||||
require.NoError(t, err)
|
||||
rootURLPost := "sppost.example.com"
|
||||
spMiddlewarePost, err := integration.CreateSAMLSP("https://"+rootURLPost, idpMetadata, saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
|
||||
acsRedirect := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[0]
|
||||
acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1]
|
||||
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewareRedirect)
|
||||
require.NoError(t, err)
|
||||
_, err = Instance.CreateSAMLClient(CTX, project.GetId(), spMiddlewarePost)
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionResp, err := Instance.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: Instance.Users[integration.UserTypeOrgOwner].ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *saml_pb.CreateResponseRequest
|
||||
AuthError string
|
||||
want *saml_pb.CreateResponseResponse
|
||||
wantURL *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: "123",
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session not found",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: "foo",
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session token invalid",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "fail callback, post",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Error{
|
||||
Error: &saml_pb.AuthorizationError{
|
||||
Error: saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED,
|
||||
ErrorDescription: gu.Ptr("nope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &saml_pb.CreateResponseResponse{
|
||||
Url: regexp.QuoteMeta(`https://` + rootURLPost + `/saml/acs`),
|
||||
Binding: &saml_pb.CreateResponseResponse_Post{Post: &saml_pb.PostResponse{
|
||||
RelayState: "notempty",
|
||||
SamlResponse: "notempty",
|
||||
}},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Instance.ID(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "fail callback, post, already failed",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
Instance.FailSAMLAuthRequest(CTX, authRequestID, saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Error{
|
||||
Error: &saml_pb.AuthorizationError{
|
||||
Error: saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED,
|
||||
ErrorDescription: gu.Ptr("nope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "fail callback, redirect",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Error{
|
||||
Error: &saml_pb.AuthorizationError{
|
||||
Error: saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED,
|
||||
ErrorDescription: gu.Ptr("nope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &saml_pb.CreateResponseResponse{
|
||||
Url: `https:\/\/` + rootURLRedirect + `\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)`,
|
||||
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Instance.ID(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "callback, redirect",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewareRedirect, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, gofakeit.BitcoinAddress(), saml.HTTPRedirectBinding)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &saml_pb.CreateResponseResponse{
|
||||
Url: `https:\/\/` + rootURLRedirect + `\/saml\/acs\?RelayState=(.*)&SAMLResponse=(.*)&SigAlg=(.*)&Signature=(.*)`,
|
||||
Binding: &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Instance.ID(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "callback, post",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &saml_pb.CreateResponseResponse{
|
||||
Url: regexp.QuoteMeta(`https://` + rootURLPost + `/saml/acs`),
|
||||
Binding: &saml_pb.CreateResponseResponse_Post{Post: &saml_pb.PostResponse{
|
||||
RelayState: "notempty",
|
||||
SamlResponse: "notempty",
|
||||
}},
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Instance.ID(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "callback, post",
|
||||
req: &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: func() string {
|
||||
authRequestID, err := Instance.CreateSAMLAuthRequest(spMiddlewarePost, Instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding)
|
||||
require.NoError(t, err)
|
||||
Instance.SuccessfulSAMLAuthRequest(CTX, Instance.Users[integration.UserTypeOrgOwner].ID, authRequestID)
|
||||
return authRequestID
|
||||
}(),
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.CreateResponse(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want != nil {
|
||||
assert.Regexp(t, regexp.MustCompile(tt.want.Url), got.GetUrl())
|
||||
if tt.want.GetPost() != nil {
|
||||
assert.NotEmpty(t, got.GetPost().GetRelayState())
|
||||
assert.NotEmpty(t, got.GetPost().GetSamlResponse())
|
||||
}
|
||||
if tt.want.GetRedirect() != nil {
|
||||
assert.NotNil(t, got.GetRedirect())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
112
internal/api/grpc/saml/v2/saml.go
Normal file
112
internal/api/grpc/saml/v2/saml.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/saml"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||
)
|
||||
|
||||
func (s *Server) GetAuthRequest(ctx context.Context, req *saml_pb.GetSAMLRequestRequest) (*saml_pb.GetSAMLRequestResponse, error) {
|
||||
authRequest, err := s.query.SamlRequestByID(ctx, true, req.GetSamlRequestId(), true)
|
||||
if err != nil {
|
||||
logging.WithError(err).Error("query samlRequest by ID")
|
||||
return nil, err
|
||||
}
|
||||
return &saml_pb.GetSAMLRequestResponse{
|
||||
SamlRequest: samlRequestToPb(authRequest),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func samlRequestToPb(a *query.SamlRequest) *saml_pb.SAMLRequest {
|
||||
return &saml_pb.SAMLRequest{
|
||||
Id: a.ID,
|
||||
CreationDate: timestamppb.New(a.CreationDate),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) CreateResponse(ctx context.Context, req *saml_pb.CreateResponseRequest) (*saml_pb.CreateResponseResponse, error) {
|
||||
switch v := req.GetResponseKind().(type) {
|
||||
case *saml_pb.CreateResponseRequest_Error:
|
||||
return s.failSAMLRequest(ctx, req.GetSamlRequestId(), v.Error)
|
||||
case *saml_pb.CreateResponseRequest_Session:
|
||||
return s.linkSessionToSAMLRequest(ctx, req.GetSamlRequestId(), v.Session)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "SAMLv2-0Tfak3fBS0", "verification oneOf %T in method CreateResponse not implemented", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) failSAMLRequest(ctx context.Context, samlRequestID string, ae *saml_pb.AuthorizationError) (*saml_pb.CreateResponseResponse, error) {
|
||||
details, aar, err := s.command.FailSAMLRequest(ctx, samlRequestID, errorReasonToDomain(ae.GetError()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar}
|
||||
url, body, err := s.idp.CreateErrorResponse(authReq, errorReasonToDomain(ae.GetError()), ae.GetErrorDescription())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil
|
||||
}
|
||||
|
||||
func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID string, session *saml_pb.Session) (*saml_pb.CreateResponseResponse, error) {
|
||||
details, aar, err := s.command.LinkSessionToSAMLRequest(ctx, samlRequestID, session.GetSessionId(), session.GetSessionToken(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar}
|
||||
url, body, err := s.idp.CreateResponse(ctx, authReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil
|
||||
}
|
||||
|
||||
func createCallbackResponseFromBinding(details *domain.ObjectDetails, url string, body string, relayState string) *saml_pb.CreateResponseResponse {
|
||||
resp := &saml_pb.CreateResponseResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
Url: url,
|
||||
}
|
||||
|
||||
if body != "" {
|
||||
resp.Binding = &saml_pb.CreateResponseResponse_Post{
|
||||
Post: &saml_pb.PostResponse{
|
||||
RelayState: relayState,
|
||||
SamlResponse: body,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
resp.Binding = &saml_pb.CreateResponseResponse_Redirect{Redirect: &saml_pb.RedirectResponse{}}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func errorReasonToDomain(errorReason saml_pb.ErrorReason) domain.SAMLErrorReason {
|
||||
switch errorReason {
|
||||
case saml_pb.ErrorReason_ERROR_REASON_UNSPECIFIED:
|
||||
return domain.SAMLErrorReasonUnspecified
|
||||
case saml_pb.ErrorReason_ERROR_REASON_VERSION_MISSMATCH:
|
||||
return domain.SAMLErrorReasonVersionMissmatch
|
||||
case saml_pb.ErrorReason_ERROR_REASON_AUTH_N_FAILED:
|
||||
return domain.SAMLErrorReasonAuthNFailed
|
||||
case saml_pb.ErrorReason_ERROR_REASON_INVALID_ATTR_NAME_OR_VALUE:
|
||||
return domain.SAMLErrorReasonInvalidAttrNameOrValue
|
||||
case saml_pb.ErrorReason_ERROR_REASON_INVALID_NAMEID_POLICY:
|
||||
return domain.SAMLErrorReasonInvalidNameIDPolicy
|
||||
case saml_pb.ErrorReason_ERROR_REASON_REQUEST_DENIED:
|
||||
return domain.SAMLErrorReasonRequestDenied
|
||||
case saml_pb.ErrorReason_ERROR_REASON_REQUEST_UNSUPPORTED:
|
||||
return domain.SAMLErrorReasonRequestUnsupported
|
||||
case saml_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_BINDING:
|
||||
return domain.SAMLErrorReasonUnsupportedBinding
|
||||
default:
|
||||
return domain.SAMLErrorReasonUnspecified
|
||||
}
|
||||
}
|
||||
59
internal/api/grpc/saml/v2/server.go
Normal file
59
internal/api/grpc/saml/v2/server.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/api/saml"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||
)
|
||||
|
||||
var _ saml_pb.SAMLServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
saml_pb.UnimplementedSAMLServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
|
||||
idp *saml.Provider
|
||||
externalSecure bool
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
idp *saml.Provider,
|
||||
externalSecure bool,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
idp: idp,
|
||||
externalSecure: externalSecure,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
saml_pb.RegisterSAMLServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return saml_pb.SAMLService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return saml_pb.SAMLService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return saml_pb.SAMLService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return saml_pb.RegisterSAMLServiceHandler
|
||||
}
|
||||
99
internal/api/saml/auth_request.go
Normal file
99
internal/api/saml/auth_request.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
|
||||
"github.com/zitadel/saml/pkg/provider"
|
||||
"github.com/zitadel/saml/pkg/provider/models"
|
||||
"github.com/zitadel/saml/pkg/provider/xml"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func (p *Provider) CreateErrorResponse(authReq models.AuthRequestInt, reason domain.SAMLErrorReason, description string) (string, string, error) {
|
||||
resp := &provider.Response{
|
||||
ProtocolBinding: authReq.GetBindingType(),
|
||||
RelayState: authReq.GetRelayState(),
|
||||
AcsUrl: authReq.GetAccessConsumerServiceURL(),
|
||||
RequestID: authReq.GetAuthRequestID(),
|
||||
Issuer: authReq.GetDestination(),
|
||||
Audience: authReq.GetIssuer(),
|
||||
}
|
||||
return createResponse(p.AuthCallbackErrorResponse(resp, domain.SAMLErrorReasonToString(reason), description), authReq.GetBindingType(), authReq.GetAccessConsumerServiceURL(), resp.RelayState, resp.SigAlg, resp.Signature)
|
||||
}
|
||||
|
||||
func (p *Provider) CreateResponse(ctx context.Context, authReq models.AuthRequestInt) (string, string, error) {
|
||||
resp := &provider.Response{
|
||||
ProtocolBinding: authReq.GetBindingType(),
|
||||
RelayState: authReq.GetRelayState(),
|
||||
AcsUrl: authReq.GetAccessConsumerServiceURL(),
|
||||
RequestID: authReq.GetAuthRequestID(),
|
||||
Issuer: authReq.GetDestination(),
|
||||
Audience: authReq.GetIssuer(),
|
||||
}
|
||||
samlResponse, err := p.AuthCallbackResponse(ctx, authReq, resp)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if err := p.command.CreateSAMLSessionFromSAMLRequest(
|
||||
setContextUserSystem(ctx),
|
||||
authReq.GetID(),
|
||||
samlComplianceChecker(),
|
||||
samlResponse.Id,
|
||||
p.Expiration(),
|
||||
); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return createResponse(samlResponse, authReq.GetBindingType(), authReq.GetAccessConsumerServiceURL(), resp.RelayState, resp.SigAlg, resp.Signature)
|
||||
}
|
||||
|
||||
func createResponse(samlResponse interface{}, binding, acs, relayState, sigAlg, sig string) (string, string, error) {
|
||||
respData, err := xml.Marshal(samlResponse)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
switch binding {
|
||||
case provider.PostBinding:
|
||||
return acs, base64.StdEncoding.EncodeToString(respData), nil
|
||||
case provider.RedirectBinding:
|
||||
respData, err := xml.DeflateAndBase64(respData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
parsed, err := url.Parse(acs)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
values := parsed.Query()
|
||||
values.Add("SAMLResponse", string(respData))
|
||||
values.Add("RelayState", relayState)
|
||||
values.Add("SigAlg", sigAlg)
|
||||
values.Add("Signature", sig)
|
||||
parsed.RawQuery = values.Encode()
|
||||
return parsed.String(), "", nil
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func setContextUserSystem(ctx context.Context) context.Context {
|
||||
data := authz.CtxData{
|
||||
UserID: "SYSTEM",
|
||||
}
|
||||
return authz.SetCtxData(ctx, data)
|
||||
}
|
||||
|
||||
func samlComplianceChecker() command.SAMLRequestComplianceChecker {
|
||||
return func(_ context.Context, samlReq *command.SAMLRequestWriteModel) error {
|
||||
if err := samlReq.CheckAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
45
internal/api/saml/auth_request_converter_v2.go
Normal file
45
internal/api/saml/auth_request_converter_v2.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"github.com/zitadel/saml/pkg/provider/models"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
)
|
||||
|
||||
var _ models.AuthRequestInt = &AuthRequestV2{}
|
||||
|
||||
type AuthRequestV2 struct {
|
||||
*command.CurrentSAMLRequest
|
||||
}
|
||||
|
||||
func (a *AuthRequestV2) GetApplicationID() string {
|
||||
return a.ApplicationID
|
||||
}
|
||||
|
||||
func (a *AuthRequestV2) GetID() string {
|
||||
return a.ID
|
||||
}
|
||||
func (a *AuthRequestV2) GetRelayState() string {
|
||||
return a.RelayState
|
||||
}
|
||||
func (a *AuthRequestV2) GetAccessConsumerServiceURL() string {
|
||||
return a.ACSURL
|
||||
}
|
||||
func (a *AuthRequestV2) GetAuthRequestID() string {
|
||||
return a.RequestID
|
||||
}
|
||||
func (a *AuthRequestV2) GetBindingType() string {
|
||||
return a.Binding
|
||||
}
|
||||
func (a *AuthRequestV2) GetIssuer() string {
|
||||
return a.Issuer
|
||||
}
|
||||
func (a *AuthRequestV2) GetDestination() string {
|
||||
return a.Destination
|
||||
}
|
||||
func (a *AuthRequestV2) GetUserID() string {
|
||||
return a.UserID
|
||||
}
|
||||
func (a *AuthRequestV2) Done() bool {
|
||||
return a.UserID != "" && a.SessionID != ""
|
||||
}
|
||||
@@ -24,7 +24,13 @@ const (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
161
internal/command/saml_request.go
Normal file
161
internal/command/saml_request.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SAMLRequest struct {
|
||||
ID string
|
||||
LoginClient string
|
||||
|
||||
ApplicationID string
|
||||
ACSURL string
|
||||
RelayState string
|
||||
RequestID string
|
||||
Binding string
|
||||
Issuer string
|
||||
Destination string
|
||||
}
|
||||
|
||||
type CurrentSAMLRequest struct {
|
||||
*SAMLRequest
|
||||
SessionID string
|
||||
UserID string
|
||||
AuthMethods []domain.UserAuthMethodType
|
||||
AuthTime time.Time
|
||||
}
|
||||
|
||||
func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest) (_ *CurrentSAMLRequest, err error) {
|
||||
id, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
samlRequest.ID = IDPrefixV2 + id
|
||||
writeModel, err := c.getSAMLRequestWriteModel(ctx, samlRequest.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if writeModel.SAMLRequestState != domain.SAMLRequestStateUnspecified {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-MO3vmsMLUt", "Errors.SAMLRequest.AlreadyExisting")
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewAddedEvent(
|
||||
ctx,
|
||||
&samlrequest.NewAggregate(samlRequest.ID, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||
samlRequest.LoginClient,
|
||||
samlRequest.ApplicationID,
|
||||
samlRequest.ACSURL,
|
||||
samlRequest.RelayState,
|
||||
samlRequest.RequestID,
|
||||
samlRequest.Binding,
|
||||
samlRequest.Issuer,
|
||||
samlRequest.Destination,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) LinkSessionToSAMLRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool) (*domain.ObjectDetails, *CurrentSAMLRequest, error) {
|
||||
writeModel, err := c.getSAMLRequestWriteModel(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if writeModel.SAMLRequestState == domain.SAMLRequestStateUnspecified {
|
||||
return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-GH3PVLSfXC", "Errors.SAMLRequest.NotExisting")
|
||||
}
|
||||
if writeModel.SAMLRequestState != domain.SAMLRequestStateAdded {
|
||||
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-ttPKNdAIFT", "Errors.SAMLRequest.AlreadyHandled")
|
||||
}
|
||||
if checkLoginClient && authz.GetCtxData(ctx).UserID != writeModel.LoginClient {
|
||||
return nil, nil, zerrors.ThrowPermissionDenied(nil, "COMMAND-KCd48Rxt7x", "Errors.SAMLRequest.WrongLoginClient")
|
||||
}
|
||||
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err = sessionWriteModel.CheckIsActive(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewSessionLinkedEvent(
|
||||
ctx, &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||
sessionID,
|
||||
sessionWriteModel.UserID,
|
||||
sessionWriteModel.AuthenticationTime(),
|
||||
sessionWriteModel.AuthMethodTypes(),
|
||||
)); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&writeModel.WriteModel), samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) FailSAMLRequest(ctx context.Context, id string, reason domain.SAMLErrorReason) (*domain.ObjectDetails, *CurrentSAMLRequest, error) {
|
||||
writeModel, err := c.getSAMLRequestWriteModel(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if writeModel.SAMLRequestState != domain.SAMLRequestStateAdded {
|
||||
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-32lGj1Fhjt", "Errors.SAMLRequest.AlreadyHandled")
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, samlrequest.NewFailedEvent(
|
||||
ctx,
|
||||
&samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||
reason,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&writeModel.WriteModel), samlRequestWriteModelToCurrentSAMLRequest(writeModel), nil
|
||||
}
|
||||
|
||||
func samlRequestWriteModelToCurrentSAMLRequest(writeModel *SAMLRequestWriteModel) (_ *CurrentSAMLRequest) {
|
||||
return &CurrentSAMLRequest{
|
||||
SAMLRequest: &SAMLRequest{
|
||||
ID: writeModel.AggregateID,
|
||||
LoginClient: writeModel.LoginClient,
|
||||
ApplicationID: writeModel.ApplicationID,
|
||||
ACSURL: writeModel.ACSURL,
|
||||
RelayState: writeModel.RelayState,
|
||||
RequestID: writeModel.RequestID,
|
||||
Binding: writeModel.Binding,
|
||||
Issuer: writeModel.Issuer,
|
||||
Destination: writeModel.Destination,
|
||||
},
|
||||
SessionID: writeModel.SessionID,
|
||||
UserID: writeModel.UserID,
|
||||
AuthMethods: writeModel.AuthMethods,
|
||||
AuthTime: writeModel.AuthTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) GetCurrentSAMLRequest(ctx context.Context, id string) (_ *CurrentSAMLRequest, err error) {
|
||||
wm, err := c.getSAMLRequestWriteModel(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return samlRequestWriteModelToCurrentSAMLRequest(wm), nil
|
||||
}
|
||||
|
||||
func (c *Commands) getSAMLRequestWriteModel(ctx context.Context, id string) (writeModel *SAMLRequestWriteModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
writeModel = NewSAMLRequestWriteModel(ctx, id)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
||||
88
internal/command/saml_request_model.go
Normal file
88
internal/command/saml_request_model.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SAMLRequestWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
aggregate *eventstore.Aggregate
|
||||
|
||||
LoginClient string
|
||||
ApplicationID string
|
||||
ACSURL string
|
||||
RelayState string
|
||||
RequestID string
|
||||
Binding string
|
||||
Issuer string
|
||||
Destination string
|
||||
|
||||
SessionID string
|
||||
UserID string
|
||||
AuthTime time.Time
|
||||
AuthMethods []domain.UserAuthMethodType
|
||||
SAMLRequestState domain.SAMLRequestState
|
||||
}
|
||||
|
||||
func NewSAMLRequestWriteModel(ctx context.Context, id string) *SAMLRequestWriteModel {
|
||||
return &SAMLRequestWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: id,
|
||||
},
|
||||
aggregate: &samlrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SAMLRequestWriteModel) Reduce() error {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *samlrequest.AddedEvent:
|
||||
m.LoginClient = e.LoginClient
|
||||
m.ApplicationID = e.ApplicationID
|
||||
m.ACSURL = e.ACSURL
|
||||
m.RelayState = e.RelayState
|
||||
m.RequestID = e.RequestID
|
||||
m.Binding = e.Binding
|
||||
m.Issuer = e.Issuer
|
||||
m.Destination = e.Destination
|
||||
m.SAMLRequestState = domain.SAMLRequestStateAdded
|
||||
case *samlrequest.SessionLinkedEvent:
|
||||
m.SessionID = e.SessionID
|
||||
m.UserID = e.UserID
|
||||
m.AuthTime = e.AuthTime
|
||||
m.AuthMethods = e.AuthMethods
|
||||
case *samlrequest.FailedEvent:
|
||||
m.SAMLRequestState = domain.SAMLRequestStateFailed
|
||||
case *samlrequest.SucceededEvent:
|
||||
m.SAMLRequestState = domain.SAMLRequestStateSucceeded
|
||||
}
|
||||
}
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *SAMLRequestWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(samlrequest.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
Builder()
|
||||
}
|
||||
|
||||
// CheckAuthenticated checks that the auth request exists, a session must have been linked
|
||||
func (m *SAMLRequestWriteModel) CheckAuthenticated() error {
|
||||
if m.SessionID == "" {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "AUTHR-3dNRNwSYeC", "Errors.SAMLRequest.NotAuthenticated")
|
||||
}
|
||||
// check that the requests exists, but has not succeeded yet
|
||||
if m.SAMLRequestState == domain.SAMLRequestStateAdded {
|
||||
return nil
|
||||
}
|
||||
return zerrors.ThrowPreconditionFailed(nil, "AUTHR-krQV50AlnJ", "Errors.SAMLRequest.NotAuthenticated")
|
||||
}
|
||||
676
internal/command/saml_request_test.go
Normal file
676
internal/command/saml_request_test.go
Normal file
@@ -0,0 +1,676 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_AddSAMLRequest(t *testing.T) {
|
||||
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
request *SAMLRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *CurrentSAMLRequest
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
"already exists error",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
request: &SAMLRequest{},
|
||||
},
|
||||
nil,
|
||||
zerrors.ThrowPreconditionFailed(nil, "COMMAND-MO3vmsMLUt", "Errors.SAMLRequest.AlreadyExisting"),
|
||||
},
|
||||
{
|
||||
"added",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
request: &SAMLRequest{
|
||||
LoginClient: "login",
|
||||
ApplicationID: "application",
|
||||
ACSURL: "acs",
|
||||
RelayState: "relaystate",
|
||||
RequestID: "request",
|
||||
Binding: "binding",
|
||||
Issuer: "issuer",
|
||||
Destination: "destination",
|
||||
},
|
||||
},
|
||||
&CurrentSAMLRequest{
|
||||
SAMLRequest: &SAMLRequest{
|
||||
ID: "V2_id",
|
||||
LoginClient: "login",
|
||||
ApplicationID: "application",
|
||||
ACSURL: "acs",
|
||||
RelayState: "relaystate",
|
||||
RequestID: "request",
|
||||
Binding: "binding",
|
||||
Issuer: "issuer",
|
||||
Destination: "destination",
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.AddSAMLRequest(tt.args.ctx, tt.args.request)
|
||||
require.ErrorIs(t, tt.wantErr, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_LinkSessionToSAMLRequest(t *testing.T) {
|
||||
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
id string
|
||||
sessionID string
|
||||
sessionToken string
|
||||
checkLoginClient bool
|
||||
}
|
||||
type res struct {
|
||||
details *domain.ObjectDetails
|
||||
authReq *CurrentSAMLRequest
|
||||
wantErr error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"samlRequest not found",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "id",
|
||||
sessionID: "sessionID",
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-GH3PVLSfXC", "Errors.SAMLRequest.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"samlRequest not existing",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("id", "instanceID").Aggregate,
|
||||
domain.SAMLErrorReasonUnspecified,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "id",
|
||||
sessionID: "sessionID",
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-ttPKNdAIFT", "Errors.SAMLRequest.AlreadyHandled"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"wrong login client",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
},
|
||||
args{
|
||||
ctx: authz.NewMockContext("instanceID", "orgID", "wrongLoginClient"),
|
||||
id: "id",
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
checkLoginClient: true,
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-KCd48Rxt7x", "Errors.SAMLRequest.WrongLoginClient"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"session not existing",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "V2_id",
|
||||
sessionID: "sessionID",
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"session expired",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(mockCtx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
"userID", "org1", testNow.Add(-5*time.Minute), &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
testNow.Add(-5*time.Minute)),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
2*time.Minute),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "V2_id",
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Hkl3d", "Errors.Session.Expired"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid session token",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(mockCtx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierInvalid(),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "V2_id",
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "invalid",
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"linked",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(mockCtx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
"userID", "org1", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
2*time.Minute),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
samlrequest.NewSessionLinkedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"sessionID",
|
||||
"userID",
|
||||
testNow,
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "V2_id",
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
|
||||
authReq: &CurrentSAMLRequest{
|
||||
SAMLRequest: &SAMLRequest{
|
||||
ID: "V2_id",
|
||||
LoginClient: "login",
|
||||
ApplicationID: "application",
|
||||
ACSURL: "acs",
|
||||
RelayState: "relaystate",
|
||||
RequestID: "request",
|
||||
Binding: "binding",
|
||||
Issuer: "issuer",
|
||||
Destination: "destination",
|
||||
},
|
||||
SessionID: "sessionID",
|
||||
UserID: "userID",
|
||||
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"linked with login client check",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"loginClient",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(mockCtx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
)),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
"userID", "org1", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
2*time.Minute),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
samlrequest.NewSessionLinkedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"sessionID",
|
||||
"userID",
|
||||
testNow,
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
},
|
||||
args{
|
||||
ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
|
||||
id: "V2_id",
|
||||
sessionID: "sessionID",
|
||||
sessionToken: "token",
|
||||
checkLoginClient: true,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
|
||||
authReq: &CurrentSAMLRequest{
|
||||
SAMLRequest: &SAMLRequest{
|
||||
ID: "V2_id",
|
||||
LoginClient: "loginClient",
|
||||
ApplicationID: "application",
|
||||
ACSURL: "acs",
|
||||
RelayState: "relaystate",
|
||||
RequestID: "request",
|
||||
Binding: "binding",
|
||||
Issuer: "issuer",
|
||||
Destination: "destination",
|
||||
},
|
||||
SessionID: "sessionID",
|
||||
UserID: "userID",
|
||||
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
sessionTokenVerifier: tt.fields.tokenVerifier,
|
||||
}
|
||||
details, got, err := c.LinkSessionToSAMLRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient)
|
||||
require.ErrorIs(t, err, tt.res.wantErr)
|
||||
assertObjectDetails(t, tt.res.details, details)
|
||||
if err == nil {
|
||||
assert.WithinRange(t, got.AuthTime, testNow, testNow)
|
||||
got.AuthTime = time.Time{}
|
||||
}
|
||||
assert.Equal(t, tt.res.authReq, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_FailSAMLRequest(t *testing.T) {
|
||||
mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
id string
|
||||
reason domain.SAMLErrorReason
|
||||
description string
|
||||
}
|
||||
type res struct {
|
||||
details *domain.ObjectDetails
|
||||
samlReq *CurrentSAMLRequest
|
||||
wantErr error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"authRequest not existing",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "foo",
|
||||
reason: domain.SAMLErrorReasonAuthNFailed,
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-32lGj1Fhjt", "Errors.SAMLRequest.AlreadyHandled"),
|
||||
},
|
||||
}, {
|
||||
"already failed",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
domain.SAMLErrorReasonAuthNFailed,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "V2_id",
|
||||
reason: domain.SAMLErrorReasonAuthNFailed,
|
||||
description: "desc",
|
||||
},
|
||||
res{
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-32lGj1Fhjt", "Errors.SAMLRequest.AlreadyHandled"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"failed",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
"login",
|
||||
"application",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate,
|
||||
domain.SAMLErrorReasonAuthNFailed,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: mockCtx,
|
||||
id: "V2_id",
|
||||
reason: domain.SAMLErrorReasonAuthNFailed,
|
||||
description: "desc",
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
|
||||
samlReq: &CurrentSAMLRequest{
|
||||
SAMLRequest: &SAMLRequest{
|
||||
ID: "V2_id",
|
||||
LoginClient: "login",
|
||||
ApplicationID: "application",
|
||||
ACSURL: "acs",
|
||||
RelayState: "relaystate",
|
||||
RequestID: "request",
|
||||
Binding: "binding",
|
||||
Issuer: "issuer",
|
||||
Destination: "destination",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
details, got, err := c.FailSAMLRequest(tt.args.ctx, tt.args.id, tt.args.reason)
|
||||
require.ErrorIs(t, err, tt.res.wantErr)
|
||||
assertObjectDetails(t, tt.res.details, details)
|
||||
assert.Equal(t, tt.res.samlReq, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
186
internal/command/saml_session.go
Normal file
186
internal/command/saml_session.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/activity"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlsession"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SAMLSession struct {
|
||||
SessionID string
|
||||
SAMLResponseID string
|
||||
EntityID string
|
||||
UserID string
|
||||
Audience []string
|
||||
Expiration time.Time
|
||||
AuthMethods []domain.UserAuthMethodType
|
||||
AuthTime time.Time
|
||||
PreferredLanguage *language.Tag
|
||||
UserAgent *domain.UserAgent
|
||||
}
|
||||
|
||||
type SAMLRequestComplianceChecker func(context.Context, *SAMLRequestWriteModel) error
|
||||
|
||||
func (c *Commands) CreateSAMLSessionFromSAMLRequest(ctx context.Context, samlReqId string, complianceCheck SAMLRequestComplianceChecker, samlResponseID string, samlResponseLifetime time.Duration) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if samlReqId == "" {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-0LxK6O31wH", "Errors.SAMLRequest.InvalidCode")
|
||||
}
|
||||
|
||||
samlReqModel, err := c.getSAMLRequestWriteModel(ctx, samlReqId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
sessionModel := NewSessionWriteModel(samlReqModel.SessionID, instanceID)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, sessionModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = sessionModel.CheckIsActive(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.newSAMLSessionAddEvents(ctx, sessionModel.UserID, sessionModel.UserResourceOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = complianceCheck(ctx, samlReqModel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.AddSession(ctx,
|
||||
sessionModel.UserID,
|
||||
sessionModel.UserResourceOwner,
|
||||
sessionModel.AggregateID,
|
||||
samlReqModel.Issuer,
|
||||
[]string{samlReqModel.Issuer},
|
||||
samlReqModel.AuthMethods,
|
||||
samlReqModel.AuthTime,
|
||||
sessionModel.PreferredLanguage,
|
||||
sessionModel.UserAgent,
|
||||
)
|
||||
|
||||
if err = cmd.AddSAMLResponse(ctx, samlResponseID, samlResponseLifetime); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.SetSAMLRequestSuccessful(ctx, samlReqModel.aggregate)
|
||||
_, err = cmd.PushEvents(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) newSAMLSessionAddEvents(ctx context.Context, userID, resourceOwner string, pending ...eventstore.Command) (*SAMLSessionEvents, error) {
|
||||
userStateModel, err := c.userStateWriteModel(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !userStateModel.UserState.IsEnabled() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-1768ZQpmcP", "Errors.User.NotActive")
|
||||
}
|
||||
sessionID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionID = IDPrefixV2 + sessionID
|
||||
return &SAMLSessionEvents{
|
||||
commands: c,
|
||||
idGenerator: c.idGenerator,
|
||||
encryptionAlg: c.keyAlgorithm,
|
||||
events: pending,
|
||||
samlSessionWriteModel: NewSAMLSessionWriteModel(sessionID, resourceOwner),
|
||||
userStateModel: userStateModel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type SAMLSessionEvents struct {
|
||||
commands *Commands
|
||||
idGenerator id.Generator
|
||||
encryptionAlg crypto.EncryptionAlgorithm
|
||||
events []eventstore.Command
|
||||
samlSessionWriteModel *SAMLSessionWriteModel
|
||||
userStateModel *UserV2WriteModel
|
||||
|
||||
// samlResponseID is set by the command
|
||||
samlResponseID string
|
||||
}
|
||||
|
||||
func (c *SAMLSessionEvents) AddSession(
|
||||
ctx context.Context,
|
||||
userID,
|
||||
userResourceOwner,
|
||||
sessionID,
|
||||
entityID string,
|
||||
audience []string,
|
||||
authMethods []domain.UserAuthMethodType,
|
||||
authTime time.Time,
|
||||
preferredLanguage *language.Tag,
|
||||
userAgent *domain.UserAgent,
|
||||
) {
|
||||
c.events = append(c.events, samlsession.NewAddedEvent(
|
||||
ctx,
|
||||
c.samlSessionWriteModel.aggregate,
|
||||
userID,
|
||||
userResourceOwner,
|
||||
sessionID,
|
||||
entityID,
|
||||
audience,
|
||||
authMethods,
|
||||
authTime,
|
||||
preferredLanguage,
|
||||
userAgent,
|
||||
))
|
||||
}
|
||||
|
||||
func (c *SAMLSessionEvents) SetSAMLRequestSuccessful(ctx context.Context, samlRequestAggregate *eventstore.Aggregate) {
|
||||
c.events = append(c.events, samlrequest.NewSucceededEvent(ctx, samlRequestAggregate))
|
||||
}
|
||||
|
||||
func (c *SAMLSessionEvents) SetSAMLRequestFailed(ctx context.Context, samlRequestAggregate *eventstore.Aggregate, err domain.SAMLErrorReason) {
|
||||
c.events = append(c.events, samlrequest.NewFailedEvent(ctx, samlRequestAggregate, err))
|
||||
}
|
||||
|
||||
func (c *SAMLSessionEvents) AddSAMLResponse(ctx context.Context, id string, lifetime time.Duration) error {
|
||||
c.samlResponseID = id
|
||||
c.events = append(c.events, samlsession.NewSAMLResponseAddedEvent(ctx, c.samlSessionWriteModel.aggregate, id, lifetime))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SAMLSessionEvents) PushEvents(ctx context.Context) (*SAMLSession, error) {
|
||||
pushedEvents, err := c.commands.eventstore.Push(ctx, c.events...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(c.samlSessionWriteModel, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session := &SAMLSession{
|
||||
SessionID: c.samlSessionWriteModel.SessionID,
|
||||
EntityID: c.samlSessionWriteModel.EntityID,
|
||||
UserID: c.samlSessionWriteModel.UserID,
|
||||
Audience: c.samlSessionWriteModel.Audience,
|
||||
Expiration: c.samlSessionWriteModel.SAMLResponseExpiration,
|
||||
AuthMethods: c.samlSessionWriteModel.AuthMethods,
|
||||
AuthTime: c.samlSessionWriteModel.AuthTime,
|
||||
PreferredLanguage: c.samlSessionWriteModel.PreferredLanguage,
|
||||
UserAgent: c.samlSessionWriteModel.UserAgent,
|
||||
SAMLResponseID: c.samlSessionWriteModel.SAMLResponseID,
|
||||
}
|
||||
activity.Trigger(ctx, c.samlSessionWriteModel.UserResourceOwner, c.samlSessionWriteModel.UserID, activity.SAMLResponse, c.commands.eventstore.FilterToQueryReducer)
|
||||
return session, nil
|
||||
}
|
||||
102
internal/command/saml_session_model.go
Normal file
102
internal/command/saml_session_model.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlsession"
|
||||
)
|
||||
|
||||
type SAMLSessionWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
UserID string
|
||||
UserResourceOwner string
|
||||
PreferredLanguage *language.Tag
|
||||
SessionID string
|
||||
EntityID string
|
||||
Audience []string
|
||||
AuthMethods []domain.UserAuthMethodType
|
||||
AuthTime time.Time
|
||||
UserAgent *domain.UserAgent
|
||||
State domain.SAMLSessionState
|
||||
SAMLResponseID string
|
||||
SAMLResponseCreation time.Time
|
||||
SAMLResponseExpiration time.Time
|
||||
|
||||
aggregate *eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewSAMLSessionWriteModel(id string, resourceOwner string) *SAMLSessionWriteModel {
|
||||
return &SAMLSessionWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
aggregate: &samlsession.NewAggregate(id, resourceOwner).Aggregate,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SAMLSessionWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *samlsession.AddedEvent:
|
||||
wm.reduceAdded(e)
|
||||
case *samlsession.SAMLResponseAddedEvent:
|
||||
wm.reduceSAMLResponseAdded(e)
|
||||
case *samlsession.SAMLResponseRevokedEvent:
|
||||
wm.reduceSAMLResponseRevoked(e)
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *SAMLSessionWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(samlsession.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
samlsession.AddedType,
|
||||
samlsession.SAMLResponseAddedType,
|
||||
samlsession.SAMLResponseRevokedType,
|
||||
).
|
||||
Builder()
|
||||
|
||||
if wm.ResourceOwner != "" {
|
||||
query.ResourceOwner(wm.ResourceOwner)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func (wm *SAMLSessionWriteModel) reduceAdded(e *samlsession.AddedEvent) {
|
||||
wm.UserID = e.UserID
|
||||
wm.UserResourceOwner = e.UserResourceOwner
|
||||
wm.SessionID = e.SessionID
|
||||
wm.EntityID = e.EntityID
|
||||
wm.Audience = e.Audience
|
||||
wm.AuthMethods = e.AuthMethods
|
||||
wm.AuthTime = e.AuthTime
|
||||
wm.PreferredLanguage = e.PreferredLanguage
|
||||
wm.UserAgent = e.UserAgent
|
||||
wm.State = domain.SAMLSessionStateActive
|
||||
// the write model might be initialized without resource owner,
|
||||
// so update the aggregate
|
||||
if wm.ResourceOwner == "" {
|
||||
wm.aggregate = &samlsession.NewAggregate(wm.AggregateID, e.Aggregate().ResourceOwner).Aggregate
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SAMLSessionWriteModel) reduceSAMLResponseAdded(e *samlsession.SAMLResponseAddedEvent) {
|
||||
wm.SAMLResponseID = e.ID
|
||||
wm.SAMLResponseCreation = e.CreationDate()
|
||||
wm.SAMLResponseExpiration = e.CreationDate().Add(e.Lifetime)
|
||||
}
|
||||
|
||||
func (wm *SAMLSessionWriteModel) reduceSAMLResponseRevoked(e *samlsession.SAMLResponseRevokedEvent) {
|
||||
wm.SAMLResponseID = ""
|
||||
wm.SAMLResponseExpiration = e.CreationDate()
|
||||
}
|
||||
337
internal/command/saml_session_test.go
Normal file
337
internal/command/saml_session_test.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlsession"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func mockSAMLRequestComplianceChecker(returnErr error) SAMLRequestComplianceChecker {
|
||||
return func(context.Context, *SAMLRequestWriteModel) error {
|
||||
return returnErr
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
keyAlgorithm crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
samlRequestID string
|
||||
samlResponseID string
|
||||
complianceCheck SAMLRequestComplianceChecker
|
||||
samlResponseLifetime time.Duration
|
||||
}
|
||||
type res struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"missing code",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
samlRequestID: "",
|
||||
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-0LxK6O31wH", "Errors.SAMLRequest.InvalidCode"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"filter error",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
samlRequestID: "V2_samlRequestID",
|
||||
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||
},
|
||||
res{
|
||||
err: io.ErrClosedPipe,
|
||||
},
|
||||
},
|
||||
{
|
||||
"session filter error",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||
"loginClient",
|
||||
"applicationId",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
samlRequestID: "V2_samlRequestID",
|
||||
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||
},
|
||||
res{
|
||||
err: io.ErrClosedPipe,
|
||||
},
|
||||
},
|
||||
{
|
||||
"inactive session error",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||
"loginClient",
|
||||
"applicationId",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewSessionLinkedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||
"sessionID",
|
||||
"userID",
|
||||
testNow,
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(), // inactive session
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
samlRequestID: "V2_samlRequestID",
|
||||
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"user not active",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||
"loginClient",
|
||||
"applicationId",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewSessionLinkedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||
"sessionID",
|
||||
"userID",
|
||||
testNow,
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(),
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||
"userID", "org1", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
user.NewHumanAddedEvent(
|
||||
context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
user.NewUserDeactivatedEvent(
|
||||
context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t),
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
samlRequestID: "V2_samlRequestID",
|
||||
samlResponseID: "samlResponseID",
|
||||
samlResponseLifetime: time.Minute * 5,
|
||||
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "SAML-1768ZQpmcP", "Errors.User.NotActive"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"add successful",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewAddedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||
"loginClient",
|
||||
"applicationId",
|
||||
"acs",
|
||||
"relaystate",
|
||||
"request",
|
||||
"binding",
|
||||
"issuer",
|
||||
"destination",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
samlrequest.NewSessionLinkedEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate,
|
||||
"sessionID",
|
||||
"userID",
|
||||
testNow,
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(),
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||
"userID", "org1", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
user.NewHumanAddedEvent(
|
||||
context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
samlsession.NewAddedEvent(context.Background(), &samlsession.NewAggregate("V2_samlSessionID", "org1").Aggregate,
|
||||
"userID", "org1", "sessionID", "issuer", []string{"issuer"},
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, &language.Afrikaans,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
samlsession.NewSAMLResponseAddedEvent(context.Background(), &samlsession.NewAggregate("V2_samlSessionID", "org1").Aggregate, "samlResponseID", time.Minute*5),
|
||||
samlrequest.NewSucceededEvent(context.Background(), &samlrequest.NewAggregate("V2_samlRequestID", "instanceID").Aggregate),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "samlSessionID"),
|
||||
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
samlRequestID: "V2_samlRequestID",
|
||||
samlResponseID: "samlResponseID",
|
||||
samlResponseLifetime: time.Minute * 5,
|
||||
complianceCheck: mockSAMLRequestComplianceChecker(nil),
|
||||
},
|
||||
res{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
keyAlgorithm: tt.fields.keyAlgorithm,
|
||||
}
|
||||
err := c.CreateSAMLSessionFromSAMLRequest(tt.args.ctx, tt.args.samlRequestID, tt.args.complianceCheck, tt.args.samlResponseID, tt.args.samlResponseLifetime)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
41
internal/domain/saml_error_reason.go
Normal file
41
internal/domain/saml_error_reason.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/saml/pkg/provider"
|
||||
)
|
||||
|
||||
type SAMLErrorReason int32
|
||||
|
||||
const (
|
||||
SAMLErrorReasonUnspecified SAMLErrorReason = iota
|
||||
SAMLErrorReasonVersionMissmatch
|
||||
SAMLErrorReasonAuthNFailed
|
||||
SAMLErrorReasonInvalidAttrNameOrValue
|
||||
SAMLErrorReasonInvalidNameIDPolicy
|
||||
SAMLErrorReasonRequestDenied
|
||||
SAMLErrorReasonRequestUnsupported
|
||||
SAMLErrorReasonUnsupportedBinding
|
||||
)
|
||||
|
||||
func SAMLErrorReasonToString(reason SAMLErrorReason) string {
|
||||
switch reason {
|
||||
case SAMLErrorReasonUnspecified:
|
||||
return "unspecified error"
|
||||
case SAMLErrorReasonVersionMissmatch:
|
||||
return provider.StatusCodeVersionMissmatch
|
||||
case SAMLErrorReasonAuthNFailed:
|
||||
return provider.StatusCodeAuthNFailed
|
||||
case SAMLErrorReasonInvalidAttrNameOrValue:
|
||||
return provider.StatusCodeInvalidAttrNameOrValue
|
||||
case SAMLErrorReasonInvalidNameIDPolicy:
|
||||
return provider.StatusCodeInvalidNameIDPolicy
|
||||
case SAMLErrorReasonRequestDenied:
|
||||
return provider.StatusCodeRequestDenied
|
||||
case SAMLErrorReasonRequestUnsupported:
|
||||
return provider.StatusCodeRequestUnsupported
|
||||
case SAMLErrorReasonUnsupportedBinding:
|
||||
return provider.StatusCodeUnsupportedBinding
|
||||
default:
|
||||
return "unspecified error"
|
||||
}
|
||||
}
|
||||
10
internal/domain/saml_request.go
Normal file
10
internal/domain/saml_request.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package domain
|
||||
|
||||
type SAMLRequestState int
|
||||
|
||||
const (
|
||||
SAMLRequestStateUnspecified SAMLRequestState = iota
|
||||
SAMLRequestStateAdded
|
||||
SAMLRequestStateFailed
|
||||
SAMLRequestStateSucceeded
|
||||
)
|
||||
9
internal/domain/saml_session.go
Normal file
9
internal/domain/saml_session.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
type SAMLSessionState int32
|
||||
|
||||
const (
|
||||
SAMLSessionStateUnspecified SAMLSessionState = iota
|
||||
SAMLSessionStateActive
|
||||
SAMLSessionStateTerminated
|
||||
)
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
|
||||
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)
|
||||
}
|
||||
|
||||
223
internal/integration/saml.go
Normal file
223
internal/integration/saml.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
oidc_internal "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
|
||||
session_pb "github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
const spCertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIDITCCAgmgAwIBAgIUUo5urYkuUHAe7LQ9sZSL+xXAqBwwDQYJKoZIhvcNAQEL
|
||||
BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIwNDEz
|
||||
MTE1MFoXDTI1MDEwMzEzMTE1MFowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w
|
||||
bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoACwbGIh8udK
|
||||
Um1r+yQoPtfswEX6Cb6Y1KwR6WZDYgzHdMyUC5Sy8Bg1H2puUZZukDLuyu6Pqvum
|
||||
8kfnzhjUR6nNCoUlidwE+yz020w5oOBofRKgJK/FVUuWD3k6kjdP9CrBFLG0PQQ3
|
||||
N2e4wilP4czCxizKero2a0e7Eq8OjHAPf8gjM+GWFZgVAbV8uf2Mjt1O2Vfbx5PZ
|
||||
sLuBZtl5jokx3NiC7my/yj81MbGEDPcQo0emeVBz3J3nVG6Yr4kdCKkvv2dhJ26C
|
||||
5cL7NIIUY4IQomJNwYC2NaYgSpQOxJHL/HsOPusO4Ia2WtUTXEZUFkxn1u0YuoSx
|
||||
CkGehF/1OwIDAQABo1MwUTAdBgNVHQ4EFgQUr6S0wA2l3MdfnvfveWDueQtaoJMw
|
||||
HwYDVR0jBBgwFoAUr6S0wA2l3MdfnvfveWDueQtaoJMwDwYDVR0TAQH/BAUwAwEB
|
||||
/zANBgkqhkiG9w0BAQsFAAOCAQEAH3Q9obyWJaMKFuGJDkIp1RFot79RWTVcAcwA
|
||||
XTJNfCseLONRIs4MkRxOn6GQBwV2IEqs1+hFG80dcd/c6yYyJ8bziKEyNMtPWrl6
|
||||
fdVD+1WnWcD1ZYrS8hgdz0FxXxl/+GjA8Pu6icmnhKgUDTYWns6Rj/gtQtZS8ZoA
|
||||
JY+T/1mGze2+Xx6pjuArZ7+hnH6EWwo+ckcmXAKyhnkhX7xIo1UFvNY2VWaGl2wU
|
||||
K2yyJA4Lu/NNmqPnpAcRDsnGP6r4frMhjnPq/ifC3B+6FT3p8dubV9PA0y86bAy5
|
||||
0yIgNje4DyWLy/DM9EpdPfJmvUAL6hOtyb8Aa9hR+a8stu7h6g==
|
||||
-----END CERTIFICATE-----`
|
||||
const spKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCgALBsYiHy50pS
|
||||
bWv7JCg+1+zARfoJvpjUrBHpZkNiDMd0zJQLlLLwGDUfam5Rlm6QMu7K7o+q+6by
|
||||
R+fOGNRHqc0KhSWJ3AT7LPTbTDmg4Gh9EqAkr8VVS5YPeTqSN0/0KsEUsbQ9BDc3
|
||||
Z7jCKU/hzMLGLMp6ujZrR7sSrw6McA9/yCMz4ZYVmBUBtXy5/YyO3U7ZV9vHk9mw
|
||||
u4Fm2XmOiTHc2ILubL/KPzUxsYQM9xCjR6Z5UHPcnedUbpiviR0IqS+/Z2EnboLl
|
||||
wvs0ghRjghCiYk3BgLY1piBKlA7Ekcv8ew4+6w7ghrZa1RNcRlQWTGfW7Ri6hLEK
|
||||
QZ6EX/U7AgMBAAECggEAD1aRkwpDO+BdORKhP9WDACc93F647fc1+mk2XFv/yKX1
|
||||
9uXnqUaLcsW3TfgrdCnKFouzZYPCBP+TzPUErTanHumRrNj/tLwBRDzWijE/8wKg
|
||||
MaE39dxdu+P/kiMqcLrZsMvqb3vrjc/aJTcNuJsyO7Cf2VSQ4nv4XIdnUQ60A9VR
|
||||
OmUp//VULZxImnPx/R304/p5VfOhyXfzBeoxUPogBurjtzkyXVG0EG2enJMMiTix
|
||||
900fTDez0TQ8V6O59vM04fhtPXvH51OkMTW/HU1QQvlnAJuX06I7k4CaBpF3xPII
|
||||
QpEbFILq5y6yAQJWELRGWzeoxK6kn6bNfI8S0+oKqQKBgQDg2UM7ruMASpY7B4zj
|
||||
XNztGDOx9BCdYyHH1O05r+ILmltBC7jFImwIYrHbaX+dg52l0PPImZuBz852IqrC
|
||||
VAEF30yBn2gWyVzIdo7W3mw9Jgqc4LrhStaJxOuXVoT2/PAuDBF8TJMNH9oLNqiD
|
||||
aPAI0cVn9BRV7AziEsrMlDLLiQKBgQC2K4Z/caAvwx/AescsN6lp+/m7MeLUpZzQ
|
||||
myZt44bnR5LouUo3vCYl+Bk8wu6PTd41LUYW/SW26HDDFTKgkBb1zVHfk5QRApaB
|
||||
VPwZnhcUvNapPOnDp75Qoq238wpfayQlKF1xCawS3N5AWkDaEdfzuH7umFJxVss2
|
||||
1tfDsn01owKBgAYWG3nMHBzv5+0lIS0uYFSSqSOSBbkc69cq7lj3Z9kEjp/OH2xG
|
||||
qEH52fKkgm3TGDta0p6Fee4jn+UWvySPfY+ZIcsIc5raTIaonuk2EBv/oZ3pf2WF
|
||||
zxTfnbj1AJhm9GFqtjZ1JC3gxNg03I7iEk1K0FsmAj7pKtgbxh2PjWhxAoGBAKBx
|
||||
BSwJbwOh3r0vZWvUOilV+0SbUyPmGI7Blr8BvTbFGuZNCsi7tP2L3O5e4Kzl7+b1
|
||||
0N0+Z5EIdwfaC5TOUup5wroeyDGTDesqZj5JthpVltnHBDuF6WArZsS0EVaojlUL
|
||||
kACWfC7AyB31X1iwjnng7CpHjZS01JWf8rgw44XxAoGAQ5YYd4WmGYZoJJak7zhb
|
||||
xnYG7hU7nS7pBPGob1FvjYMw1x/htuJCjxLh08dlzJGM6SFlDn7HVM9ou99w5n+d
|
||||
xtqmbthw2E9VjSk3zSYb4uFc6mv0C/kRPTDUFH+9CpQTBBx/O016hmcatxlBS6JL
|
||||
VAV6oE8sEJYHtR6YdZiMWWo=
|
||||
-----END PRIVATE KEY-----`
|
||||
|
||||
func CreateSAMLSP(root string, idpMetadata *saml.EntityDescriptor, binding string) (*samlsp.Middleware, error) {
|
||||
rootURL, err := url.Parse(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPair, err := tls.X509KeyPair([]byte(spCertificate), []byte(spKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sp, err := samlsp.New(samlsp.Options{
|
||||
URL: *rootURL,
|
||||
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
|
||||
Certificate: keyPair.Leaf,
|
||||
IDPMetadata: idpMetadata,
|
||||
UseArtifactResponse: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sp.Binding = binding
|
||||
sp.ResponseBinding = binding
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
func (i *Instance) CreateSAMLClient(ctx context.Context, projectID string, m *samlsp.Middleware) (*management.AddSAMLAppResponse, error) {
|
||||
spMetadata, err := xml.MarshalIndent(m.ServiceProvider.Metadata(), "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m.ResponseBinding == saml.HTTPRedirectBinding {
|
||||
metadata := strings.Replace(string(spMetadata), saml.HTTPPostBinding, saml.HTTPRedirectBinding, 2)
|
||||
spMetadata = []byte(metadata)
|
||||
}
|
||||
|
||||
resp, err := i.Client.Mgmt.AddSAMLApp(ctx, &management.AddSAMLAppRequest{
|
||||
ProjectId: projectID,
|
||||
Name: fmt.Sprintf("app-%s", gofakeit.AppName()),
|
||||
Metadata: &management.AddSAMLAppRequest_MetadataXml{MetadataXml: spMetadata},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, await(func() error {
|
||||
_, err := i.Client.Mgmt.GetProjectByID(ctx, &management.GetProjectByIDRequest{
|
||||
Id: projectID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{
|
||||
ProjectId: projectID,
|
||||
AppId: resp.GetAppId(),
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Instance) CreateSAMLAuthRequest(m *samlsp.Middleware, loginClient string, acs saml.Endpoint, relayState string, responseBinding string) (authRequestID string, err error) {
|
||||
authReq, err := m.ServiceProvider.MakeAuthenticationRequest(acs.Location, acs.Binding, responseBinding)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := GetRequest(redirectURL.String(), map[string]string{oidc_internal.LoginClientHeader: loginClient})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get request: %w", err)
|
||||
}
|
||||
|
||||
loc, err := CheckRedirect(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("check redirect: %w", err)
|
||||
}
|
||||
|
||||
prefixWithHost := i.Issuer() + i.Config.LoginURLV2
|
||||
if !strings.HasPrefix(loc.String(), prefixWithHost) {
|
||||
return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String())
|
||||
}
|
||||
return strings.TrimPrefix(loc.String(), prefixWithHost), nil
|
||||
}
|
||||
|
||||
func (i *Instance) FailSAMLAuthRequest(ctx context.Context, id string, reason saml_pb.ErrorReason) *saml_pb.CreateResponseResponse {
|
||||
resp, err := i.Client.SAMLv2.CreateResponse(ctx, &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: id,
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Error{Error: &saml_pb.AuthorizationError{Error: reason}},
|
||||
})
|
||||
logging.OnError(err).Panic("create human user")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (i *Instance) SuccessfulSAMLAuthRequest(ctx context.Context, userId, id string) *saml_pb.CreateResponseResponse {
|
||||
respSession, err := i.Client.SessionV2.CreateSession(ctx, &session_pb.CreateSessionRequest{
|
||||
Checks: &session_pb.Checks{
|
||||
User: &session_pb.CheckUser{
|
||||
Search: &session_pb.CheckUser_UserId{
|
||||
UserId: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
logging.OnError(err).Panic("create session")
|
||||
|
||||
resp, err := i.Client.SAMLv2.CreateResponse(ctx, &saml_pb.CreateResponseRequest{
|
||||
SamlRequestId: id,
|
||||
ResponseKind: &saml_pb.CreateResponseRequest_Session{
|
||||
Session: &saml_pb.Session{
|
||||
SessionId: respSession.GetSessionId(),
|
||||
SessionToken: respSession.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
})
|
||||
logging.OnError(err).Panic("create human user")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (i *Instance) GetSAMLIDPMetadata() (*saml.EntityDescriptor, error) {
|
||||
idpEntityID := http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure) + "/saml/v2/metadata"
|
||||
resp, err := http.Get(idpEntityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entityDescriptor := new(saml.EntityDescriptor)
|
||||
if err := xml.Unmarshal(data, entityDescriptor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entityDescriptor, nil
|
||||
}
|
||||
|
||||
func (i *Instance) Issuer() string {
|
||||
return http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure)
|
||||
}
|
||||
@@ -69,6 +69,7 @@ var (
|
||||
DeviceAuthProjection *handler.Handler
|
||||
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,
|
||||
|
||||
132
internal/query/projection/saml_request.go
Normal file
132
internal/query/projection/saml_request.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
SamlRequestsProjectionTable = "projections.saml_requests"
|
||||
|
||||
SamlRequestColumnID = "id"
|
||||
SamlRequestColumnCreationDate = "creation_date"
|
||||
SamlRequestColumnChangeDate = "change_date"
|
||||
SamlRequestColumnSequence = "sequence"
|
||||
SamlRequestColumnResourceOwner = "resource_owner"
|
||||
SamlRequestColumnInstanceID = "instance_id"
|
||||
SamlRequestColumnLoginClient = "login_client"
|
||||
SamlRequestColumnIssuer = "issuer"
|
||||
SamlRequestColumnACS = "acs"
|
||||
SamlRequestColumnRelayState = "relay_state"
|
||||
SamlRequestColumnBinding = "binding"
|
||||
)
|
||||
|
||||
type samlRequestProjection struct{}
|
||||
|
||||
// Name implements handler.Projection.
|
||||
func (*samlRequestProjection) Name() string {
|
||||
return SamlRequestsProjectionTable
|
||||
}
|
||||
|
||||
func newSamlRequestProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(samlRequestProjection))
|
||||
}
|
||||
|
||||
func (*samlRequestProjection) Init() *old_handler.Check {
|
||||
return handler.NewMultiTableCheck(
|
||||
handler.NewTable([]*handler.InitColumn{
|
||||
handler.NewColumn(SamlRequestColumnID, handler.ColumnTypeText),
|
||||
handler.NewColumn(SamlRequestColumnCreationDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(SamlRequestColumnChangeDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(SamlRequestColumnSequence, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(SamlRequestColumnResourceOwner, handler.ColumnTypeText),
|
||||
handler.NewColumn(SamlRequestColumnInstanceID, handler.ColumnTypeText),
|
||||
handler.NewColumn(SamlRequestColumnLoginClient, handler.ColumnTypeText),
|
||||
handler.NewColumn(SamlRequestColumnIssuer, handler.ColumnTypeText),
|
||||
handler.NewColumn(SamlRequestColumnACS, handler.ColumnTypeText),
|
||||
handler.NewColumn(SamlRequestColumnRelayState, handler.ColumnTypeText),
|
||||
handler.NewColumn(SamlRequestColumnBinding, handler.ColumnTypeText),
|
||||
},
|
||||
handler.NewPrimaryKey(SamlRequestColumnInstanceID, SamlRequestColumnID),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *samlRequestProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: samlrequest.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: samlrequest.AddedType,
|
||||
Reduce: p.reduceSamlRequestAdded,
|
||||
},
|
||||
{
|
||||
Event: samlrequest.SucceededType,
|
||||
Reduce: p.reduceSamlRequestEnded,
|
||||
},
|
||||
{
|
||||
Event: samlrequest.FailedType,
|
||||
Reduce: p.reduceSamlRequestEnded,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Aggregate: instance.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(SamlRequestColumnInstanceID),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *samlRequestProjection) reduceSamlRequestAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*samlrequest.AddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Sfwfa", "reduce.wrong.event.type %s", samlrequest.AddedType)
|
||||
}
|
||||
|
||||
return handler.NewCreateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SamlRequestColumnID, e.Aggregate().ID),
|
||||
handler.NewCol(SamlRequestColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCol(SamlRequestColumnCreationDate, e.CreationDate()),
|
||||
handler.NewCol(SamlRequestColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SamlRequestColumnResourceOwner, e.Aggregate().ResourceOwner),
|
||||
handler.NewCol(SamlRequestColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SamlRequestColumnLoginClient, e.LoginClient),
|
||||
handler.NewCol(SamlRequestColumnIssuer, e.Issuer),
|
||||
handler.NewCol(SamlRequestColumnACS, e.ACSURL),
|
||||
handler.NewCol(SamlRequestColumnRelayState, e.RelayState),
|
||||
handler.NewCol(SamlRequestColumnBinding, e.Binding),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *samlRequestProjection) reduceSamlRequestEnded(event eventstore.Event) (*handler.Statement, error) {
|
||||
switch event.(type) {
|
||||
case *samlrequest.SucceededEvent,
|
||||
*samlrequest.FailedEvent:
|
||||
break
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ASF3h", "reduce.wrong.event.type %s", []eventstore.EventType{samlrequest.SucceededType, samlrequest.FailedType})
|
||||
}
|
||||
|
||||
return handler.NewDeleteStatement(
|
||||
event,
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SamlRequestColumnID, event.Aggregate().ID),
|
||||
handler.NewCond(SamlRequestColumnInstanceID, event.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
123
internal/query/projection/saml_request_test.go
Normal file
123
internal/query/projection/saml_request_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestSamlRequestProjection_reduces(t *testing.T) {
|
||||
type args struct {
|
||||
event func(t *testing.T) eventstore.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "reduceSamlRequestAdded",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
samlrequest.AddedType,
|
||||
samlrequest.AggregateType,
|
||||
[]byte(`{"login_client": "loginClient", "issuer": "issuer", "acs_url": "acs", "relay_state": "relayState", "binding": "binding"}`),
|
||||
), eventstore.GenericEventMapper[samlrequest.AddedEvent]),
|
||||
},
|
||||
reduce: (&samlRequestProjection{}).reduceSamlRequestAdded,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("saml_request"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.saml_requests (id, instance_id, creation_date, change_date, resource_owner, sequence, login_client, issuer, acs, relay_state, binding) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
"ro-id",
|
||||
uint64(15),
|
||||
"loginClient",
|
||||
"issuer",
|
||||
"acs",
|
||||
"relayState",
|
||||
"binding",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceSamlRequestFailed",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
samlrequest.FailedType,
|
||||
samlrequest.AggregateType,
|
||||
[]byte(`{"reason": 0}`),
|
||||
), eventstore.GenericEventMapper[samlrequest.FailedEvent]),
|
||||
},
|
||||
reduce: (&samlRequestProjection{}).reduceSamlRequestEnded,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("saml_request"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.saml_requests WHERE (id = $1) AND (instance_id = $2)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceSamlRequestSucceeded",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
samlrequest.SucceededType,
|
||||
samlrequest.AggregateType,
|
||||
nil,
|
||||
), eventstore.GenericEventMapper[samlrequest.SucceededEvent]),
|
||||
},
|
||||
reduce: (&samlRequestProjection{}).reduceSamlRequestEnded,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("saml_request"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.saml_requests WHERE (id = $1) AND (instance_id = $2)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := baseEvent(t)
|
||||
got, err := tt.reduce(event)
|
||||
if !zerrors.IsErrorInvalidArgument(err) {
|
||||
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||
}
|
||||
|
||||
event = tt.args.event(t)
|
||||
got, err = tt.reduce(event)
|
||||
assertReduce(t, got, err, SamlRequestsProjectionTable, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
81
internal/query/saml_request.go
Normal file
81
internal/query/saml_request.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SamlRequest struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
LoginClient string
|
||||
Issuer string
|
||||
ACS string
|
||||
RelayState string
|
||||
Binding string
|
||||
}
|
||||
|
||||
func (a *SamlRequest) checkLoginClient(ctx context.Context) error {
|
||||
if uid := authz.GetCtxData(ctx).UserID; uid != a.LoginClient {
|
||||
return zerrors.ThrowPermissionDenied(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed saml_request_by_id.sql
|
||||
var samlRequestByIDQuery string
|
||||
|
||||
func (q *Queries) samlRequestByIDQuery(ctx context.Context) string {
|
||||
return fmt.Sprintf(samlRequestByIDQuery, q.client.Timetravel(call.Took(ctx)))
|
||||
}
|
||||
|
||||
func (q *Queries) SamlRequestByID(ctx context.Context, shouldTriggerBulk bool, id string, checkLoginClient bool) (_ *SamlRequest, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if shouldTriggerBulk {
|
||||
_, traceSpan := tracing.NewNamedSpan(ctx, "TriggerSamlRequestProjection")
|
||||
ctx, err = projection.SamlRequestProjection.Trigger(ctx, handler.WithAwaitRunning())
|
||||
logging.OnError(err).Debug("trigger failed")
|
||||
traceSpan.EndWithError(err)
|
||||
}
|
||||
|
||||
dst := new(SamlRequest)
|
||||
err = q.client.QueryRowContext(
|
||||
ctx,
|
||||
func(row *sql.Row) error {
|
||||
return row.Scan(
|
||||
&dst.ID, &dst.CreationDate, &dst.LoginClient, &dst.Issuer, &dst.ACS, &dst.RelayState, &dst.Binding,
|
||||
)
|
||||
},
|
||||
q.samlRequestByIDQuery(ctx),
|
||||
id, authz.GetInstance(ctx).InstanceID(),
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-Thee9", "Errors.SamlRequest.NotExisting")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Ou8ue", "Errors.Internal")
|
||||
}
|
||||
|
||||
if checkLoginClient {
|
||||
if err = dst.checkLoginClient(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return dst, nil
|
||||
}
|
||||
11
internal/query/saml_request_by_id.sql
Normal file
11
internal/query/saml_request_by_id.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
select
|
||||
id,
|
||||
creation_date,
|
||||
login_client,
|
||||
issuer,
|
||||
acs,
|
||||
relay_state,
|
||||
binding
|
||||
from projections.saml_requests %s
|
||||
where id = $1 and instance_id = $2
|
||||
limit 1;
|
||||
127
internal/query/saml_request_test.go
Normal file
127
internal/query/saml_request_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestQueries_SamlRequestByID(t *testing.T) {
|
||||
expQuery := regexp.QuoteMeta(fmt.Sprintf(
|
||||
samlRequestByIDQuery,
|
||||
asOfSystemTime,
|
||||
))
|
||||
|
||||
cols := []string{
|
||||
projection.SamlRequestColumnID,
|
||||
projection.SamlRequestColumnCreationDate,
|
||||
projection.SamlRequestColumnLoginClient,
|
||||
projection.SamlRequestColumnIssuer,
|
||||
projection.SamlRequestColumnACS,
|
||||
projection.SamlRequestColumnRelayState,
|
||||
projection.SamlRequestColumnBinding,
|
||||
}
|
||||
type args struct {
|
||||
shouldTriggerBulk bool
|
||||
id string
|
||||
checkLoginClient bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expect sqlExpectation
|
||||
want *SamlRequest
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "success, all values",
|
||||
args: args{
|
||||
shouldTriggerBulk: false,
|
||||
id: "123",
|
||||
checkLoginClient: true,
|
||||
},
|
||||
expect: mockQuery(expQuery, cols, []driver.Value{
|
||||
"id",
|
||||
testNow,
|
||||
"loginClient",
|
||||
"issuer",
|
||||
"acs",
|
||||
"relayState",
|
||||
"binding",
|
||||
}, "123", "instanceID"),
|
||||
want: &SamlRequest{
|
||||
ID: "id",
|
||||
CreationDate: testNow,
|
||||
LoginClient: "loginClient",
|
||||
Issuer: "issuer",
|
||||
ACS: "acs",
|
||||
RelayState: "relayState",
|
||||
Binding: "binding",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no rows",
|
||||
args: args{
|
||||
shouldTriggerBulk: false,
|
||||
id: "123",
|
||||
},
|
||||
expect: mockQueryScanErr(expQuery, cols, nil, "123", "instanceID"),
|
||||
wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-Thee9", "Errors.SamlRequest.NotExisting"),
|
||||
},
|
||||
{
|
||||
name: "query error",
|
||||
args: args{
|
||||
shouldTriggerBulk: false,
|
||||
id: "123",
|
||||
},
|
||||
expect: mockQueryErr(expQuery, sql.ErrConnDone, "123", "instanceID"),
|
||||
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ou8ue", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "wrong login client",
|
||||
args: args{
|
||||
shouldTriggerBulk: false,
|
||||
id: "123",
|
||||
checkLoginClient: true,
|
||||
},
|
||||
expect: mockQuery(expQuery, cols, []driver.Value{
|
||||
"id",
|
||||
testNow,
|
||||
"wrongLoginClient",
|
||||
"issuer",
|
||||
"acs",
|
||||
"relayState",
|
||||
"binding",
|
||||
}, "123", "instanceID"),
|
||||
wantErr: zerrors.ThrowPermissionDeniedf(nil, "OIDCv2-aL0ag", "Errors.SamlRequest.WrongLoginClient"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
execMock(t, tt.expect, func(db *sql.DB) {
|
||||
q := &Queries{
|
||||
client: &database.DB{
|
||||
DB: db,
|
||||
Database: &prepareDB{},
|
||||
},
|
||||
}
|
||||
ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
|
||||
|
||||
got, err := q.SamlRequestByID(ctx, tt.args.shouldTriggerBulk, tt.args.id, tt.args.checkLoginClient)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
26
internal/repository/samlrequest/aggregate.go
Normal file
26
internal/repository/samlrequest/aggregate.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package samlrequest
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "saml_request"
|
||||
AggregateVersion = "v1"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, instanceID string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: instanceID,
|
||||
InstanceID: instanceID,
|
||||
},
|
||||
}
|
||||
}
|
||||
10
internal/repository/samlrequest/eventstore.go
Normal file
10
internal/repository/samlrequest/eventstore.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package samlrequest
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/eventstore"
|
||||
|
||||
func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, AddedType, eventstore.GenericEventMapper[AddedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SessionLinkedType, eventstore.GenericEventMapper[SessionLinkedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, FailedType, eventstore.GenericEventMapper[FailedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SucceededType, eventstore.GenericEventMapper[SucceededEvent])
|
||||
}
|
||||
172
internal/repository/samlrequest/saml_request.go
Normal file
172
internal/repository/samlrequest/saml_request.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package samlrequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
samlRequestEventPrefix = "saml_request."
|
||||
AddedType = samlRequestEventPrefix + "added"
|
||||
FailedType = samlRequestEventPrefix + "failed"
|
||||
SessionLinkedType = samlRequestEventPrefix + "session.linked"
|
||||
SucceededType = samlRequestEventPrefix + "succeeded"
|
||||
)
|
||||
|
||||
type AddedEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
|
||||
LoginClient string `json:"login_client,omitempty"`
|
||||
ApplicationID string `json:"application_id,omitempty"`
|
||||
ACSURL string `json:"acs_url,omitempty"`
|
||||
RelayState string `json:"relay_state,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
Binding string `json:"binding,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
Destination string `json:"destination,omitempty"`
|
||||
}
|
||||
|
||||
func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = event
|
||||
}
|
||||
|
||||
func (e *AddedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAddedEvent(ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
loginClient,
|
||||
applicationID string,
|
||||
acsURL string,
|
||||
relayState string,
|
||||
requestID string,
|
||||
binding string,
|
||||
issuer string,
|
||||
destination string,
|
||||
) *AddedEvent {
|
||||
return &AddedEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
AddedType,
|
||||
),
|
||||
LoginClient: loginClient,
|
||||
ApplicationID: applicationID,
|
||||
ACSURL: acsURL,
|
||||
RelayState: relayState,
|
||||
RequestID: requestID,
|
||||
Binding: binding,
|
||||
Issuer: issuer,
|
||||
Destination: destination,
|
||||
}
|
||||
}
|
||||
|
||||
type SessionLinkedEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
|
||||
SessionID string `json:"session_id"`
|
||||
UserID string `json:"user_id"`
|
||||
AuthTime time.Time `json:"auth_time"`
|
||||
AuthMethods []domain.UserAuthMethodType `json:"auth_methods"`
|
||||
}
|
||||
|
||||
func (e *SessionLinkedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SessionLinkedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSessionLinkedEvent(ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
sessionID,
|
||||
userID string,
|
||||
authTime time.Time,
|
||||
authMethods []domain.UserAuthMethodType,
|
||||
) *SessionLinkedEvent {
|
||||
return &SessionLinkedEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
SessionLinkedType,
|
||||
),
|
||||
SessionID: sessionID,
|
||||
UserID: userID,
|
||||
AuthTime: authTime,
|
||||
AuthMethods: authMethods,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *SessionLinkedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = event
|
||||
}
|
||||
|
||||
type FailedEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Reason domain.SAMLErrorReason `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
func (e *FailedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *FailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFailedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
reason domain.SAMLErrorReason,
|
||||
) *FailedEvent {
|
||||
return &FailedEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
FailedType,
|
||||
),
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *FailedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = event
|
||||
}
|
||||
|
||||
type SucceededEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *SucceededEvent) Payload() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *SucceededEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSucceededEvent(ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *SucceededEvent {
|
||||
return &SucceededEvent{
|
||||
BaseEvent: eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
SucceededType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *SucceededEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = event
|
||||
}
|
||||
25
internal/repository/samlsession/aggregate.go
Normal file
25
internal/repository/samlsession/aggregate.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package samlsession
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "saml_session"
|
||||
AggregateVersion = "v1"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, resourceOwner string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
12
internal/repository/samlsession/eventstore.go
Normal file
12
internal/repository/samlsession/eventstore.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package samlsession
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, AddedType, eventstore.GenericEventMapper[AddedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SAMLResponseAddedType, eventstore.GenericEventMapper[SAMLResponseAddedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SAMLResponseRevokedType, eventstore.GenericEventMapper[SAMLResponseRevokedEvent])
|
||||
|
||||
}
|
||||
139
internal/repository/samlsession/saml_session.go
Normal file
139
internal/repository/samlsession/saml_session.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package samlsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
samlSessionEventPrefix = "saml_session."
|
||||
AddedType = samlSessionEventPrefix + "added"
|
||||
SAMLResponseAddedType = samlSessionEventPrefix + "saml_response.added"
|
||||
SAMLResponseRevokedType = samlSessionEventPrefix + "saml_response.revoked"
|
||||
)
|
||||
|
||||
type AddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
UserID string `json:"userID"`
|
||||
UserResourceOwner string `json:"userResourceOwner"`
|
||||
SessionID string `json:"sessionID"`
|
||||
EntityID string `json:"entityID"`
|
||||
Audience []string `json:"audience"`
|
||||
AuthMethods []domain.UserAuthMethodType `json:"authMethods"`
|
||||
AuthTime time.Time `json:"authTime"`
|
||||
PreferredLanguage *language.Tag `json:"preferredLanguage,omitempty"`
|
||||
UserAgent *domain.UserAgent `json:"userAgent,omitempty"`
|
||||
}
|
||||
|
||||
func (e *AddedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewAddedEvent(ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
userID,
|
||||
userResourceOwner,
|
||||
sessionID,
|
||||
entityID string,
|
||||
audience []string,
|
||||
authMethods []domain.UserAuthMethodType,
|
||||
authTime time.Time,
|
||||
preferredLanguage *language.Tag,
|
||||
userAgent *domain.UserAgent,
|
||||
) *AddedEvent {
|
||||
return &AddedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
AddedType,
|
||||
),
|
||||
UserID: userID,
|
||||
UserResourceOwner: userResourceOwner,
|
||||
SessionID: sessionID,
|
||||
EntityID: entityID,
|
||||
Audience: audience,
|
||||
AuthMethods: authMethods,
|
||||
AuthTime: authTime,
|
||||
PreferredLanguage: preferredLanguage,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
type SAMLResponseAddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
ID string `json:"id,omitempty"`
|
||||
Lifetime time.Duration `json:"lifetime,omitempty"`
|
||||
}
|
||||
|
||||
func (e *SAMLResponseAddedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SAMLResponseAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *SAMLResponseAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewSAMLResponseAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
id string,
|
||||
lifetime time.Duration,
|
||||
) *SAMLResponseAddedEvent {
|
||||
return &SAMLResponseAddedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
SAMLResponseAddedType,
|
||||
),
|
||||
ID: id,
|
||||
Lifetime: lifetime,
|
||||
}
|
||||
}
|
||||
|
||||
type SAMLResponseRevokedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *SAMLResponseRevokedEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SAMLResponseRevokedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *SAMLResponseRevokedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewSAMLResponseRevokedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *SAMLResponseRevokedEvent {
|
||||
return &SAMLResponseRevokedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
SAMLResponseRevokedType,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -566,6 +566,13 @@ Errors:
|
||||
Token:
|
||||
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user