Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,51 @@ err = descopeClient.DescopeClient().Management.ThirdPartyApplication().DeleteCon

```

### Manage Cross-App Access (XAA / ID-JAG)

Cross-App Access (XAA), built on the OAuth identity-assertion authorization grant (ID-JAG), lets a tenant trust one or more external OIDC issuers so that a token minted by a trusted issuer can be exchanged for a Descope token (the RFC 7523 `jwt-bearer` grant). XAA trust is configured **per SSO configuration** of a tenant, alongside the tenant's SAML/OIDC settings, through the SSO management API. Each SSO configuration is addressed by its `ssoID` (leave it empty for the tenant's default SSO configuration). Read the effective trust config back through the same SSO management API (`LoadXAASettings` / `LoadAllXAASettings`).

Configure the trusted issuers together with the config-level shared group/role mapping. Each issuer supports just-in-time (JIT) provisioning with the same attribute mapping as the SSO login JIT:

```go
err := descopeClient.Management.SSO().ConfigureXAASettings(context.Background(), "my-tenant-id", &descope.SSOXAASettings{
Enabled: true,
Settings: &descope.XAAJWTBearerSettings{
Issuers: map[string]*descope.XAAIssuerSettings{
// The map key is the trusted issuer URL.
"https://issuer.example.com": {
JWKsURI: "https://issuer.example.com/.well-known/jwks.json",
SignAlgorithm: "RS256",
UserInfoURI: "https://issuer.example.com/userinfo",
ExternalIDFieldName: "sub", // assertion claim used as the login id
JITDisabled: false, // JIT provisioning on: create/update the user from the assertion
AttributeMapping: &descope.AttributeMapping{
Email: "email",
Name: "name",
Group: "groups", // assertion claim that carries the user's groups
},
},
},
},
// Config-level shared group/role mapping (shared across SAML / OIDC / SCIM / XAA for this ssoID).
RoleMappings: []*descope.RoleMapping{
{Groups: []string{"admins"}, Role: "Tenant Admin"},
},
DefaultSSORoles: []string{"Member"},
}, "" /* ssoID, empty = default */)

// Load the XAA settings for a single SSO configuration.
xaa, err := descopeClient.Management.SSO().LoadXAASettings(context.Background(), "my-tenant-id", "" /* ssoID */)

// Load the XAA settings for every SSO configuration of the tenant.
allXAA, err := descopeClient.Management.SSO().LoadAllXAASettings(context.Background(), "my-tenant-id")

// Delete the XAA settings of a single SSO configuration (removes its trusted issuers from the tenant).
err = descopeClient.Management.SSO().DeleteXAASettings(context.Background(), "my-tenant-id", "" /* ssoID */)
```

> Group-to-role mapping is **not** configured per issuer. Role mapping (`RoleMappings`), default SSO roles (`DefaultSSORoles`), and FGA/group grants (`FgaMappings`) passed to `ConfigureXAASettings` are the config-level shared mapping: the same mapping is shared across SAML / OIDC / SCIM / XAA for that `ssoID`. Each issuer only maps the assertion's groups claim (via `AttributeMapping.Group`); how those group names resolve to roles is defined once, per SSO configuration. On load, this shared mapping is returned as `GroupsMapping` (role references by id and name), mirroring the SAML settings load shape.

### Manage Outbound Applications

You can create, update, delete, or load outbound applications:
Expand Down
12 changes: 12 additions & 0 deletions descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ var (
ssoSAMLSettingsByMetadata: "mgmt/sso/saml/metadata",
ssoRedirectURL: "mgmt/sso/redirect",
ssoOIDCSettings: "mgmt/sso/oidc",
ssoXAASettings: "mgmt/sso/xaa/settings",
ssoXAALoadAllSettings: "mgmt/sso/xaa/settings/all",
ssoMetadata: "mgmt/sso/metadata",
ssoMapping: "mgmt/sso/mapping",
ssoRecalculateMappings: "mgmt/sso/recalculate-mappings",
Expand Down Expand Up @@ -533,6 +535,8 @@ type mgmtEndpoints struct {
ssoSAMLSettingsByMetadata string
ssoRedirectURL string
ssoOIDCSettings string
ssoXAASettings string
ssoXAALoadAllSettings string
ssoRecalculateMappings string
updateJWT string
impersonate string
Expand Down Expand Up @@ -1312,6 +1316,14 @@ func (e *endpoints) ManagementSSOOIDCSettings() string {
return path.Join(e.version, e.mgmt.ssoOIDCSettings)
}

func (e *endpoints) ManagementXAASettings() string {
return path.Join(e.version, e.mgmt.ssoXAASettings)
}

func (e *endpoints) ManagementXAALoadAllSettings() string {
return path.Join(e.version, e.mgmt.ssoXAALoadAllSettings)
}

// // Deprecated
func (e *endpoints) ManagementSSOSettings() string {
return path.Join(e.version, e.mgmt.ssoSettings)
Expand Down
107 changes: 107 additions & 0 deletions descope/internal/mgmt/sso.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,110 @@ func (s *sso) RecalculateSSOMappings(ctx context.Context, tenantID string, ssoID

return err
}

func (s *sso) ConfigureXAASettings(ctx context.Context, tenantID string, settings *descope.SSOXAASettings, ssoID string) error {
if tenantID == "" {
return utils.NewInvalidArgumentError("tenantID")
}

if settings == nil {
return utils.NewInvalidArgumentError("settings")
}

mappings := []map[string]any{}
for i := range settings.RoleMappings {
mappings = append(mappings, map[string]any{
"groups": settings.RoleMappings[i].Groups,
"roleName": settings.RoleMappings[i].Role,
})
}

req := map[string]any{
"tenantId": tenantID,
"enabled": settings.Enabled,
"settings": settings.Settings,
"roleMappings": mappings,
"defaultSSORoles": settings.DefaultSSORoles,
"groupsPriority": settings.GroupsPriority,
"groupPriorityEnabled": settings.GroupPriorityEnabled,
"allowOverrideRoles": settings.AllowOverrideRoles,
}
if len(ssoID) > 0 {
req["ssoId"] = ssoID
}
fgaMappings := parseFgaMappings(settings.FgaMappings)
if len(fgaMappings) > 0 {
req["fgaMappings"] = fgaMappings
}
if settings.ProviderID != "" {
req["providerID"] = settings.ProviderID
}

_, err := s.client.DoPostRequest(ctx, api.Routes.ManagementXAASettings(), req, nil, "")
return err
}

func (s *sso) LoadXAASettings(ctx context.Context, tenantID string, ssoID string) (*descope.SSOXAASettingsResponse, error) {
if tenantID == "" {
return nil, utils.NewInvalidArgumentError("tenantID")
}

req := &api.HTTPRequest{
QueryParams: map[string]string{"tenantId": tenantID},
}
if len(ssoID) > 0 {
req.QueryParams["ssoId"] = ssoID
}
res, err := s.client.DoGetRequest(ctx, api.Routes.ManagementXAASettings(), req, "")
if err != nil {
return nil, err
}
return unmarshalXAASettingsResponse(res)
}

func (s *sso) LoadAllXAASettings(ctx context.Context, tenantID string) ([]*descope.SSOXAASettingsResponse, error) {
if tenantID == "" {
return nil, utils.NewInvalidArgumentError("tenantID")
}

req := &api.HTTPRequest{
QueryParams: map[string]string{"tenantId": tenantID},
}
res, err := s.client.DoGetRequest(ctx, api.Routes.ManagementXAALoadAllSettings(), req, "")
if err != nil {
return nil, err
}
return unmarshalXAAAllSettingsResponse(res)
}

func (s *sso) DeleteXAASettings(ctx context.Context, tenantID string, ssoID string) error {
if tenantID == "" {
return utils.NewInvalidArgumentError("tenantID")
}
req := &api.HTTPRequest{
QueryParams: map[string]string{"tenantId": tenantID},
}
if len(ssoID) > 0 {
req.QueryParams["ssoId"] = ssoID
}
_, err := s.client.DoDeleteRequest(ctx, api.Routes.ManagementXAASettings(), req, "")
return err
}

func unmarshalXAASettingsResponse(res *api.HTTPResponse) (*descope.SSOXAASettingsResponse, error) {
var xaaSettingsRes *descope.SSOXAASettingsResponse
err := utils.Unmarshal([]byte(res.BodyStr), &xaaSettingsRes)
if err != nil {
return nil, err
}
return xaaSettingsRes, err
}

func unmarshalXAAAllSettingsResponse(res *api.HTTPResponse) ([]*descope.SSOXAASettingsResponse, error) {
var xaaAllSettingsRes *descope.SSOXAAAllSettingsResponse
err := utils.Unmarshal([]byte(res.BodyStr), &xaaAllSettingsRes)
if err != nil {
return nil, err
}
return xaaAllSettingsRes.XAASettings, err
}
Loading
Loading