feat: saml application configuration for login version (#9351)

# Which Problems Are Solved

OIDC applications can configure the used login version, which is
currently not possible for SAML applications.

# How the Problems Are Solved

Add the same functionality dependent on the feature-flag for SAML
applications.

# Additional Changes

None

# Additional Context

Closes #9267
Follow up issue for frontend changes #9354

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2025-02-13 17:03:05 +01:00
committed by GitHub
parent 66296db971
commit 49de5c61b2
40 changed files with 1051 additions and 240 deletions

View File

@@ -98,7 +98,11 @@ func (s *Server) AddOIDCApp(ctx context.Context, req *mgmt_pb.AddOIDCAppRequest)
}, nil
}
func (s *Server) AddSAMLApp(ctx context.Context, req *mgmt_pb.AddSAMLAppRequest) (*mgmt_pb.AddSAMLAppResponse, error) {
app, err := s.command.AddSAMLApplication(ctx, AddSAMLAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
samlApp, err := AddSAMLAppRequestToDomain(req)
if err != nil {
return nil, err
}
app, err := s.command.AddSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
@@ -150,7 +154,11 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID
}
func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAMLAppConfigRequest) (*mgmt_pb.UpdateSAMLAppConfigResponse, error) {
config, err := s.command.ChangeSAMLApplication(ctx, UpdateSAMLAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
samlApp, err := UpdateSAMLAppConfigRequestToDomain(req)
if err != nil {
return nil, err
}
config, err := s.command.ChangeSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}

View File

@@ -67,15 +67,21 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp,
}, nil
}
func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) *domain.SAMLApp {
func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) (*domain.SAMLApp, error) {
loginVersion, loginBaseURI, err := app_grpc.LoginVersionToDomain(req.GetLoginVersion())
if err != nil {
return nil, err
}
return &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: req.ProjectId,
},
AppName: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
}
AppName: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func AddAPIAppRequestToDomain(app *mgmt_pb.AddAPIAppRequest) *domain.APIApp {
@@ -125,15 +131,21 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
}, nil
}
func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) *domain.SAMLApp {
func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) (*domain.SAMLApp, error) {
loginVersion, loginBaseURI, err := app_grpc.LoginVersionToDomain(app.GetLoginVersion())
if err != nil {
return nil, err
}
return &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: app.ProjectId,
},
AppID: app.AppId,
Metadata: app.GetMetadataXml(),
MetadataURL: app.GetMetadataUrl(),
}
AppID: app.AppId,
Metadata: app.GetMetadataXml(),
MetadataURL: app.GetMetadataUrl(),
LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI,
}, nil
}
func UpdateAPIAppConfigRequestToDomain(app *mgmt_pb.UpdateAPIAppConfigRequest) *domain.APIApp {

View File

@@ -85,7 +85,8 @@ func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app_pb.Logi
func AppSAMLConfigToPb(app *query.SAMLApp) app_pb.AppConfig {
return &app_pb.App_SamlConfig{
SamlConfig: &app_pb.SAMLConfig{
Metadata: &app_pb.SAMLConfig_MetadataXml{MetadataXml: app.Metadata},
Metadata: &app_pb.SAMLConfig_MetadataXml{MetadataXml: app.Metadata},
LoginVersion: loginVersionToPb(app.LoginVersion, app.LoginBaseURI),
},
}
}

View File

@@ -0,0 +1,53 @@
package saml
import (
"strings"
"github.com/zitadel/saml/pkg/provider/serviceprovider"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
)
const (
LoginSamlRequestParam = "samlRequest"
LoginPath = "/login"
)
type ServiceProvider struct {
SP *query.SAMLServiceProvider
defaultLoginURL string
defaultLoginURLV2 string
}
func ServiceProviderFromBusiness(spQuery *query.SAMLServiceProvider, defaultLoginURL, defaultLoginURLV2 string) (*serviceprovider.ServiceProvider, error) {
sp := &ServiceProvider{
SP: spQuery,
defaultLoginURL: defaultLoginURL,
defaultLoginURLV2: defaultLoginURLV2,
}
return serviceprovider.NewServiceProvider(
spQuery.AppID,
&serviceprovider.Config{Metadata: spQuery.Metadata},
sp.LoginURL,
)
}
func (s *ServiceProvider) LoginURL(id string) string {
// if the authRequest does not have the v2 prefix, it was created for login V1
if !strings.HasPrefix(id, command.IDPrefixV2) {
return s.defaultLoginURL + id
}
// any v2 login without a specific base uri will be sent to the configured login v2 UI
// this way we're also backwards compatible
if s.SP.LoginBaseURI == nil || s.SP.LoginBaseURI.String() == "" {
return s.defaultLoginURLV2 + id
}
// for clients with a specific URI (internal or external) we only need to add the auth request id
uri := s.SP.LoginBaseURI.JoinPath(LoginPath)
q := uri.Query()
q.Set(LoginSamlRequestParam, id)
uri.RawQuery = q.Encode()
return uri.String()
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/actions/object"
"github.com/zitadel/zitadel/internal/activity"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/auth/repository"
@@ -62,22 +63,12 @@ type Storage struct {
}
func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) {
app, err := p.query.ActiveAppBySAMLEntityID(ctx, entityID)
sp, err := p.query.ActiveSAMLServiceProviderByID(ctx, entityID)
if err != nil {
return nil, err
}
return serviceprovider.NewServiceProvider(
app.ID,
&serviceprovider.Config{
Metadata: app.SAMLConfig.Metadata,
},
func(id string) string {
if strings.HasPrefix(id, command.IDPrefixV2) {
return p.defaultLoginURLv2 + id
}
return p.defaultLoginURL + id
},
)
return ServiceProviderFromBusiness(sp, p.defaultLoginURL, p.defaultLoginURLv2)
}
func (p *Storage) GetEntityIDByAppID(ctx context.Context, appID string) (string, error) {
@@ -108,11 +99,34 @@ func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequest
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
// for backwards compatibility we pass the login client if set
headers, _ := http_utils.HeadersFromCtx(ctx)
if loginClient := headers.Get(LoginClientHeader); loginClient != "" {
loginClient := headers.Get(LoginClientHeader)
// for backwards compatibility we'll use the new login if the header is set (no matter the other configs)
if loginClient != "" {
return p.createAuthRequestLoginClient(ctx, req, acsUrl, protocolBinding, relayState, applicationID, loginClient)
}
return p.createAuthRequest(ctx, req, acsUrl, protocolBinding, relayState, applicationID)
// if the instance requires the v2 login, use it no matter what the application configured
if authz.GetFeatures(ctx).LoginV2.Required {
return p.createAuthRequestLoginClient(ctx, req, acsUrl, protocolBinding, relayState, applicationID, loginClient)
}
version, err := p.query.SAMLAppLoginVersion(ctx, applicationID)
if err != nil {
return nil, err
}
switch version {
case domain.LoginVersion1:
return p.createAuthRequest(ctx, req, acsUrl, protocolBinding, relayState, applicationID)
case domain.LoginVersion2:
return p.createAuthRequestLoginClient(ctx, req, acsUrl, protocolBinding, relayState, applicationID, loginClient)
case domain.LoginVersionUnspecified:
fallthrough
default:
// since we already checked for a login header, we can fall back to the v1 login
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) {