Skip to content

Commit

Permalink
SAML: Validate SSO URL matches the one in entity descriptor if both a…
Browse files Browse the repository at this point in the history
…re given (#51968)
  • Loading branch information
kopiczko authored Feb 11, 2025
1 parent e6b9412 commit 37ef1dc
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 1 deletion.
12 changes: 11 additions & 1 deletion lib/services/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,17 @@ func ValidateSAMLConnector(sc types.SAMLConnector, rg RoleGetter) error {

sc.SetIssuer(md.EntityID)
if md.IDPSSODescriptor != nil && len(md.IDPSSODescriptor.SingleSignOnServices) > 0 {
sc.SetSSO(md.IDPSSODescriptor.SingleSignOnServices[0].Location)
metadataSsoUrl := md.IDPSSODescriptor.SingleSignOnServices[0].Location
if sc.GetSSO() != "" && sc.GetSSO() != metadataSsoUrl {
log.WithFields(log.Fields{
"connector_name": sc.GetName(),
"connector_sso_url": sc.GetSSO(),
"idp_metadata_sso_url": metadataSsoUrl,
}).Warn(
"Connector has set SSO URL, but it does not match the one found in IDP metadata. Overwriting with the IDP metadata SSO URL.",
)
}
sc.SetSSO(metadataSsoUrl)
}
}

Expand Down
57 changes: 57 additions & 0 deletions lib/services/saml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,60 @@ func (rs roleSet) GetRole(ctx context.Context, name string) (types.Role, error)
}
return nil, trace.NotFound("unknown role: %s", name)
}

func Test_ValidateSAMLConnector(t *testing.T) {
t.Parallel()

const entityDescriptor = `
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2024-07-04T11:40:17.481Z" cacheDuration="PT48H" entityID="teleport.example.com/metadata">
<IDPSSODescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://first.in.the.descriptor.example.com/sso/saml"></SingleSignOnService>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://second.in.the.descriptor.example.com/sso/saml"></SingleSignOnService>
</IDPSSODescriptor>
</EntityDescriptor>`

// Create a roleSet with <nil> role values as ValidateSAMLConnector only checks if the role
// in the connector role mapping exists.
var existingRoles roleSet = map[string]types.Role{
"existing-test-role": nil,
}

testCases := []struct {
name string
connectorDesc types.SAMLConnectorSpecV2
expectedSsoUrl string
}{
{
name: "matching set SSO and entity descriptor URLs",
connectorDesc: types.SAMLConnectorSpecV2{
AssertionConsumerService: "https://example.com/acs",
EntityDescriptor: entityDescriptor,
AttributesToRoles: []types.AttributeMapping{
{Name: "groups", Value: "Everyone", Roles: []string{"existing-test-role"}},
},
},
expectedSsoUrl: "https://first.in.the.descriptor.example.com/sso/saml",
},
{
name: "if set SSO URL and entity descriptor do not match, overwrite with the one from the descriptor",
connectorDesc: types.SAMLConnectorSpecV2{
AssertionConsumerService: "https://example.com/acs",
SSO: "https://i.do.not.match.example.com/sso",
EntityDescriptor: entityDescriptor,
AttributesToRoles: []types.AttributeMapping{
{Name: "groups", Value: "Everyone", Roles: []string{"existing-test-role"}},
},
},
expectedSsoUrl: "https://first.in.the.descriptor.example.com/sso/saml",
},
}
for _, tc := range testCases {
connector, err := types.NewSAMLConnector("test-connector", tc.connectorDesc)
require.NoError(t, err)

err = ValidateSAMLConnector(connector, existingRoles)
require.NoError(t, err)

require.Equal(t, tc.expectedSsoUrl, connector.GetSSO())
}
}

0 comments on commit 37ef1dc

Please sign in to comment.