diff --git a/backend/build.gradle b/backend/build.gradle index 9dec8da20..a4ca532b1 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -195,7 +195,8 @@ dependencies { testCompile "org.springframework.boot:spring-boot-starter-test:${project.'springbootVersion'}" testCompile "org.springframework.security:spring-security-test:${project.'springSecurityVersion'}" testCompile 'org.skyscreamer:jsonassert:1.5.0' - testCompile "org.xmlunit:xmlunit-core:2.5.1" + testImplementation "org.xmlunit:xmlunit-core:2.9.0" + testImplementation "org.xmlunit:xmlunit-assertj:2.9.0" testRuntime 'cglib:cglib-nodep:3.2.5' compile "net.shibboleth.ext:spring-extensions:6.2.0" diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Audience.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Audience.java index d80b4e590..87833d3bc 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Audience.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Audience.java @@ -16,8 +16,4 @@ public class Audience extends AbstractXMLObject implements org.opensaml.saml.sam @Getter @Setter private String URI; - - public Audience(String value) { - this.setURI(value); - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java index 383d7237b..cae6d277a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java @@ -91,7 +91,9 @@ public boolean isSupportedProtocol(String s) { @Override public void addSupportedProtocol(String supportedProtocol) { - supportedProtocols.add(supportedProtocol); + if (!supportedProtocols.contains(supportedProtocol)) { + supportedProtocols.add(supportedProtocol); + } } @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/DefaultAcrValue.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/DefaultAcrValue.java index 954fa9db5..6610001aa 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/DefaultAcrValue.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/DefaultAcrValue.java @@ -11,7 +11,4 @@ @NoArgsConstructor @Audited public class DefaultAcrValue extends AbstractValueXMLObject implements net.shibboleth.oidc.saml.xmlobject.DefaultAcrValue { - public DefaultAcrValue(String value) { - this.setValue(value); - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/PostLogoutRedirectUri.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/PostLogoutRedirectUri.java index 2c66e75e3..0c326043d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/PostLogoutRedirectUri.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/PostLogoutRedirectUri.java @@ -11,7 +11,4 @@ @NoArgsConstructor @Audited public class PostLogoutRedirectUri extends AbstractValueXMLObject implements net.shibboleth.oidc.saml.xmlobject.PostLogoutRedirectUri { - public PostLogoutRedirectUri(String value) { - this.setValue(value); - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/RequestUri.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/RequestUri.java index ee9885ebd..4be5c0c60 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/RequestUri.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/RequestUri.java @@ -11,7 +11,4 @@ @NoArgsConstructor @Audited public class RequestUri extends AbstractValueXMLObject implements net.shibboleth.oidc.saml.xmlobject.RequestUri { - public RequestUri(String value) { - this.setValue(value); - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 18bb322f0..b7b034546 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -181,6 +181,9 @@ public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws Forb public EntityDescriptorRepresentation createNewEntityDescriptorFromXMLOrigin(EntityDescriptor ed) { ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); ed.setProtocol(determineEntityDescriptorProtocol(ed)); + if (ed.getProtocol() == EntityDescriptorProtocol.OIDC) { + ed.getSPSSODescriptor("").addSupportedProtocol("http://openid.net/specs/openid-connect-core-1_0.html"); + } EntityDescriptor savedEntity = entityDescriptorRepository.save(ed); return createRepresentationFromDescriptor(savedEntity); } @@ -204,8 +207,7 @@ public EntityDescriptorRepresentation updateGroupForEntityDescriptor(String reso } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) - throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java index 39f4cac1d..7a32ff156 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java @@ -4,6 +4,7 @@ import com.google.common.base.Strings; import edu.internet2.tier.shibboleth.admin.ui.domain.AssertionConsumerService; import edu.internet2.tier.shibboleth.admin.ui.domain.Audience; +import edu.internet2.tier.shibboleth.admin.ui.domain.AudienceBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson; import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPersonBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.Description; @@ -14,9 +15,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptorProtocol; import edu.internet2.tier.shibboleth.admin.ui.domain.Extensions; +import edu.internet2.tier.shibboleth.admin.ui.domain.ExtensionsBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.GivenName; import edu.internet2.tier.shibboleth.admin.ui.domain.InformationURL; import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.KeyName; import edu.internet2.tier.shibboleth.admin.ui.domain.Logo; import edu.internet2.tier.shibboleth.admin.ui.domain.NameIDFormat; import edu.internet2.tier.shibboleth.admin.ui.domain.Organization; @@ -37,11 +40,17 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.AbstractValueXMLObject; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.ClientSecret; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.ClientSecretKeyReference; import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.DefaultAcrValue; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.DefaultAcrValueBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.JwksData; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.JwksUri; import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.OAuthRPExtensions; import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.PostLogoutRedirectUri; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.PostLogoutRedirectUriBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.RequestUri; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.RequestUriBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.ValueXMLObject; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; @@ -75,16 +84,18 @@ public class EntityDescriptorConversionUtils { public static KeyDescriptor createKeyDescriptor(String name, String usageType, String value, KeyDescriptorRepresentation.ElementType elementType) { KeyDescriptor keyDescriptor = openSamlObjects.buildDefaultInstanceOfType(KeyDescriptor.class); - + KeyInfo keyInfo = openSamlObjects.buildDefaultInstanceOfType(KeyInfo.class); if (!Strings.isNullOrEmpty(name)) { keyDescriptor.setName(name); + KeyName keyName = openSamlObjects.buildDefaultInstanceOfType(KeyName.class); + keyName.setValue(name); + keyInfo.getXMLObjects().add(keyName); } if (!"both".equals(usageType)) { keyDescriptor.setUsageType(usageType); } - KeyInfo keyInfo = openSamlObjects.buildDefaultInstanceOfType(KeyInfo.class); AbstractValueXMLObject xmlObject; switch (elementType) { case X509Data: @@ -100,17 +111,17 @@ public static KeyDescriptor createKeyDescriptor(String name, String usageType, S keyInfo.getXMLObjects().add(xmlObject); break; case jwksUri: - xmlObject = openSamlObjects.buildDefaultInstanceOfType(JwksData.class); + xmlObject = openSamlObjects.buildDefaultInstanceOfType(JwksUri.class); xmlObject.setValue(value); keyInfo.getXMLObjects().add(xmlObject); break; case clientSecret: - xmlObject = openSamlObjects.buildDefaultInstanceOfType(JwksData.class); + xmlObject = openSamlObjects.buildDefaultInstanceOfType(ClientSecret.class); xmlObject.setValue(value); keyInfo.getXMLObjects().add(xmlObject); break; case clientSecretKeyReference: - xmlObject = openSamlObjects.buildDefaultInstanceOfType(JwksData.class); + xmlObject = openSamlObjects.buildDefaultInstanceOfType(ClientSecretKeyReference.class); xmlObject.setValue(value); keyInfo.getXMLObjects().add(xmlObject); break; @@ -349,55 +360,55 @@ public static void setupSPSSODescriptor(EntityDescriptor ed, EntityDescriptorRep } private static Extensions buildOAuthRPExtensionsFromRepresentation(@NonNull ServiceProviderSsoDescriptorRepresentation representation) { - Extensions result = new Extensions(); + Extensions result = new ExtensionsBuilder().buildObject(); HashMap oauthrpextMap = (HashMap) representation.getExtensions().get("OAuthRPExtensions"); OAuthRPExtensions oAuthRPExtensions = new OAuthRPExtensions(); oauthrpextMap.keySet().forEach(key -> { - try { - if ("requestUris".equals(key) || "defaultAcrValues".equals(key) || "postLogoutRedirectUris".equals(key) || "audience".equals(key)){ - Field field = oAuthRPExtensions.getClass().getDeclaredField(key); - field.setAccessible(true); - ((List) oauthrpextMap.get(key)).forEach(value -> { - switch (key) { - case "requestUris": - oAuthRPExtensions.addRequestUri(new RequestUri((value))); - break; - case "defaultAcrValues": - oAuthRPExtensions.addDefaultAcrValue(new DefaultAcrValue((value))); - break; - case "postLogoutRedirectUris": - oAuthRPExtensions.addPostLogoutRedirectUri(new PostLogoutRedirectUri((value))); - break; - case "audience": - oAuthRPExtensions.addAudience(new Audience(value)); - break; - } - }); - } - else if ("attributes".equals(key)) { - HashMap attributes = (HashMap) oauthrpextMap.get(key); - attributes.keySet().forEach(attKey -> { - try { - Field attField = oAuthRPExtensions.getClass().getDeclaredField(attKey); - attField.setAccessible(true); - if ("requireAuthTime".equals(attKey)) { - Boolean value = Boolean.valueOf(attributes.get(attKey).toString()); - attField.set(oAuthRPExtensions, value); - } else if ("defaultMaxAge".equals(attKey)) { - Integer value = Integer.valueOf(attributes.get(attKey).toString()); - attField.setInt(oAuthRPExtensions, value); - } else { - attField.set(oAuthRPExtensions, attributes.get(attKey).toString()); - } - } - catch (IllegalAccessException | NoSuchFieldException e) { - // skip it + if ("requestUris".equals(key) || "defaultAcrValues".equals(key) || "postLogoutRedirectUris".equals(key) || "audiences".equals(key)) { + ((List) oauthrpextMap.get(key)).forEach(value -> { + switch (key) { + case "requestUris": + RequestUri ru = new RequestUriBuilder().buildObject(); + ru.setValue(value); + oAuthRPExtensions.addRequestUri(ru); + break; + case "defaultAcrValues": + DefaultAcrValue dav = new DefaultAcrValueBuilder().buildObject(); + dav.setValue(value); + oAuthRPExtensions.addDefaultAcrValue(dav); + break; + case "postLogoutRedirectUris": + PostLogoutRedirectUri plru = new PostLogoutRedirectUriBuilder().buildObject(); + plru.setValue(value); + oAuthRPExtensions.addPostLogoutRedirectUri(plru); + break; + case "audiences": + Audience audience = new AudienceBuilder().buildObject(); + audience.setURI(value); + oAuthRPExtensions.addAudience(audience); + break; + } + }); + } else if ("attributes".equals(key)) { + HashMap attributes = (HashMap) oauthrpextMap.get(key); + attributes.keySet().forEach(attKey -> { + try { + Field attField = oAuthRPExtensions.getClass().getDeclaredField(attKey); + attField.setAccessible(true); + if ("requireAuthTime".equals(attKey)) { + Boolean value = Boolean.valueOf(attributes.get(attKey).toString()); + attField.set(oAuthRPExtensions, value); + } else if ("defaultMaxAge".equals(attKey)) { + Integer value = Integer.valueOf(attributes.get(attKey).toString()); + attField.setInt(oAuthRPExtensions, value); + } else { + attField.set(oAuthRPExtensions, attributes.get(attKey).toString()); } - }); - } - } - catch (NoSuchFieldException e) { - // skip it + } + catch (IllegalAccessException | NoSuchFieldException e) { + // skip it + } + }); } }); result.addUnknownXMLObject(oAuthRPExtensions); diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index c59e87c2a..a78a76c24 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -3,13 +3,12 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptorProtocol import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation -import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.security.model.Group @@ -20,10 +19,10 @@ import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionSer import edu.internet2.tier.shibboleth.admin.ui.service.EntityService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils -import groovy.json.JsonSlurper import lombok.SneakyThrows import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.io.ClassPathResource @@ -35,6 +34,7 @@ import org.springframework.web.util.NestedServletException import spock.lang.Subject import javax.persistence.EntityManager +import java.nio.charset.StandardCharsets import static org.hamcrest.CoreMatchers.containsString import static org.springframework.http.MediaType.APPLICATION_JSON @@ -47,6 +47,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { @Autowired @@ -78,7 +79,9 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { def controller EntityDescriptorVersionService versionService = Mock() - + + def shortNameToOAuth = "\$.serviceProviderSsoDescriptor.extensions.OAuthRPExtensions." + @Transactional def setup() { openSamlObjects.init() @@ -708,7 +711,7 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { } @WithMockAdmin - def "POST /EntityDescriptor OIDC descriptor"() { + def "POST /EntityDescriptor OIDC descriptor - incoming JSON"() { when: def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(fromFile("/json/SHIBUI-2380-1.json"))) @@ -719,6 +722,84 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.protocolSupportEnum").value("http://openid.net/specs/openid-connect-core-1_0.html")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.clientUri").value("https://example.org/clientUri")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.responseTypes").value("code id_token")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.sectorIdentifierUri").value("https://example.org/sectorIdentifier")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.idTokenEncryptedResponseEnc").value("A256GCM")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.applicationType").value("web")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.tokenEndpointAuthMethod").value("client_secret_basic")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.userInfoEncryptedResponseEnc").value("A192GCM")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.userInfoSignedResponseAlg").value("RS384")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.userInfoEncryptedResponseAlg").value("A192KW")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.grantTypes").value("authorization_code")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.softwareId").value("mockSoftwareId")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.requestObjectEncryptionEnc").value("A128GCM")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.initiateLoginUri").value("https://example.org/initiateLogin")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.tokenEndpointAuthMethod").value("client_secret_basic")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.requestObjectSigningAlg").value("RS256")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.scopes").value("openid profile")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.idTokenEncryptedResponseAlg").value("A256KW")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.softwareVersion").value("mockSoftwareVersion")) + .andExpect(jsonPath(shortNameToOAuth + "postLogoutRedirectUris[0]").value("https://example.org/postLogout")) + .andExpect(jsonPath(shortNameToOAuth + "requestUris[0]").value("https://example.org/request")) + .andExpect(jsonPath(shortNameToOAuth + "defaultAcrValues").isArray()) + .andExpect(jsonPath(shortNameToOAuth + "attributes.requireAuthTime").value(Boolean.FALSE)) + .andExpect(jsonPath(shortNameToOAuth + "attributes.defaultMaxAge").value(Integer.valueOf(0))) + } + + @WithMockAdmin + def 'GET /EntityDescriptor/{resourceId} existing as oidc xml'() { + given: + def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-2380.json').bytes, EntityDescriptorRepresentation) + jpaEntityDescriptorService.createNew(representation) + def edResourceId = jpaEntityDescriptorService.getAllEntityDescriptorProjectionsBasedOnUserAccess().get(0).getResourceId() + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/" + edResourceId).accept(APPLICATION_XML)) + + then: + String xmlContent = result.andReturn().getResponse().getContentAsString(); + result.andExpect(status().isOk()) + TestHelpers.generatedXmlIsTheSameAsExpectedXml(new String(fromFile("/metadata/SHIBUI-2380.xml"), StandardCharsets.UTF_8), xmlContent) + } + + @WithMockAdmin + def "POST /EntityDescriptor OIDC descriptor - incoming XML"() { + when: + def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_XML).content(fromFile("/metadata/SHIBUI-2380.xml")).param("spName", "testing")) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.entityId").value("mockSamlClientId")) + .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.protocolSupportEnum").value("http://openid.net/specs/openid-connect-core-1_0.html")) + .andExpect(jsonPath("\$.protocol").value("OIDC")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.clientUri").value("https://example.org/clientUri")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.responseTypes").value("code id_token")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.sectorIdentifierUri").value("https://example.org/sectorIdentifier")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.idTokenEncryptedResponseEnc").value("A256GCM")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.applicationType").value("web")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.tokenEndpointAuthMethod").value("client_secret_basic")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.userInfoEncryptedResponseEnc").value("A192GCM")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.userInfoSignedResponseAlg").value("RS384")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.userInfoEncryptedResponseAlg").value("A192KW")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.grantTypes").value("authorization_code")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.softwareId").value("mockSoftwareId")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.requestObjectEncryptionEnc").value("A128GCM")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.initiateLoginUri").value("https://example.org/initiateLogin")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.tokenEndpointAuthMethod").value("client_secret_basic")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.requestObjectSigningAlg").value("RS256")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.scopes").value("openid profile")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.idTokenEncryptedResponseAlg").value("A256KW")) + .andExpect(jsonPath(shortNameToOAuth + "attributes.softwareVersion").value("mockSoftwareVersion")) + .andExpect(jsonPath(shortNameToOAuth + "postLogoutRedirectUris[0]").value("https://example.org/postLogout")) + .andExpect(jsonPath(shortNameToOAuth + "requestUris[0]").value("https://example.org/request")) + .andExpect(jsonPath(shortNameToOAuth + "audiences[0]").value("http://mypeeps")) + .andExpect(jsonPath(shortNameToOAuth + "defaultAcrValues").isArray()) + .andExpect(jsonPath(shortNameToOAuth + "attributes.requireAuthTime").value(Boolean.FALSE)) + .andExpect(jsonPath(shortNameToOAuth + "attributes.defaultMaxAge").value(Integer.valueOf(0))) } @SneakyThrows diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializerForTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializerForTest.groovy index f84afdcc5..86e24419d 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializerForTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializerForTest.groovy @@ -6,7 +6,13 @@ class JPAXMLObjectProviderInitializerForTest extends AbstractXMLObjectProviderIn @Override protected String[] getConfigResources() { return new String[]{ - "/jpa-saml2-metadata-config.xml", "jpa-saml2-metadata-algorithm-config.xml", "jpa-encryption-config.xml", "jpa-signature-config.xml" + "/jpa-saml2-metadata-config.xml", + "jpa-saml2-metadata-algorithm-config.xml", + "jpa-encryption-config.xml", + "jpa-signature-config.xml", + "jpa-saml2-assertion-config.xml", + "jpa-shib-oidc-config.xml", + "modified-saml2-assertion-config.xml" } } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index 8de2ac759..14ad669c5 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -13,6 +13,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.OAuthRPExtensions import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -767,8 +768,35 @@ class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { when: def representation = new ObjectMapper().readValue(this.class.getResource('/json/SHIBUI-2380.json').bytes, EntityDescriptorRepresentation) def ed = service.createDescriptorFromRepresentation(representation) + def oauthRpExt = (OAuthRPExtensions) ed.getSPSSODescriptor("").getExtensions().getOrderedChildren().get(0) then: assert ed.getProtocol() == EntityDescriptorProtocol.OIDC + assert oauthRpExt.getDefaultAcrValues().size() == 2 + assert oauthRpExt.getPostLogoutRedirectUris().size() == 1 + assert oauthRpExt.getRequestUris().size() == 1 + assert oauthRpExt.getAudiences().size() == 1 + assert oauthRpExt.getClientUri().equals("https://example.org/clientUri") + assert oauthRpExt.getResponseTypes().equals("code id_token") + assert oauthRpExt.getSectorIdentifierUri().equals("https://example.org/sectorIdentifier") + assert oauthRpExt.getIdTokenEncryptedResponseEnc().equals("A256GCM") + assert oauthRpExt.getApplicationType().equals("web") + assert oauthRpExt.getTokenEndpointAuthMethod().equals("client_secret_basic") + assert oauthRpExt.isRequireAuthTime() == false + + assert oauthRpExt.getUserInfoEncryptedResponseEnc().equals("A192GCM") + assert oauthRpExt.getUserInfoSignedResponseAlg().equals("RS384") + assert oauthRpExt.getUserInfoEncryptedResponseAlg().equals("A192KW") + assert oauthRpExt.getGrantTypes().equals("authorization_code") + assert oauthRpExt.getSoftwareId().equals("mockSoftwareId") + assert oauthRpExt.getRequestObjectEncryptionEnc().equals("A128GCM") + assert oauthRpExt.getInitiateLoginUri().equals("https://example.org/initiateLogin") + assert oauthRpExt.getTokenEndpointAuthMethod().equals("client_secret_basic") + assert oauthRpExt.getRequestObjectSigningAlg().equals("RS256") + assert oauthRpExt.getScopes().equals("openid profile") + assert oauthRpExt.getIdTokenEncryptedResponseAlg().equals("A256KW") + assert oauthRpExt.getSoftwareVersion().equals("mockSoftwareVersion") + + assert oauthRpExt.getDefaultMaxAge() == 0 } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy index e2d67412e..ed8815127 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy @@ -6,9 +6,12 @@ import junit.framework.Assert import org.apache.commons.lang.StringUtils import org.w3c.dom.Document import org.w3c.dom.Node +import org.xmlunit.assertj.XmlAssert import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.builder.Input.Builder +import org.xmlunit.diff.DefaultNodeMatcher +import org.xmlunit.diff.ElementSelectors import javax.xml.transform.Source import javax.xml.transform.Transformer @@ -37,6 +40,11 @@ class TestHelpers { return count } + static void generatedXmlIsTheSameAsExpectedXml(String expectedXmlResource, String generatedXml) { + XmlAssert.assertThat(generatedXml).and(expectedXmlResource).ignoreWhitespace().normalizeWhitespace() + .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)).areSimilar(); + } + static void generatedXmlIsTheSameAsExpectedXml(String expectedXmlResource, Document generatedXml) { def Builder builder = Input.fromDocument(generatedXml) def Source source = builder.build() diff --git a/backend/src/test/resources/jpa-saml2-assertion-config.xml b/backend/src/test/resources/jpa-saml2-assertion-config.xml new file mode 100644 index 000000000..99cf4995e --- /dev/null +++ b/backend/src/test/resources/jpa-saml2-assertion-config.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/test/resources/jpa-shib-oidc-config.xml b/backend/src/test/resources/jpa-shib-oidc-config.xml new file mode 100644 index 000000000..7bf05eeb4 --- /dev/null +++ b/backend/src/test/resources/jpa-shib-oidc-config.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/test/resources/jpa-signature-config.xml b/backend/src/test/resources/jpa-signature-config.xml index 9a8da32e8..b2450ac71 100644 --- a/backend/src/test/resources/jpa-signature-config.xml +++ b/backend/src/test/resources/jpa-signature-config.xml @@ -16,16 +16,7 @@ - - - - + @@ -56,20 +47,20 @@ - + +--> + @@ -84,13 +75,13 @@ - + @@ -98,13 +89,13 @@ - + @@ -154,13 +145,13 @@ - + diff --git a/backend/src/test/resources/json/SHIBUI-2380.json b/backend/src/test/resources/json/SHIBUI-2380.json index 1f73d2e23..f71c6de6b 100644 --- a/backend/src/test/resources/json/SHIBUI-2380.json +++ b/backend/src/test/resources/json/SHIBUI-2380.json @@ -17,7 +17,6 @@ } ], "entityId": "mockSamlClientId", - "idOfOwner": "admingroup", "organization": {}, "securityInfo": { "authenticationRequestsSigned": false, @@ -35,26 +34,30 @@ }, { "value": "https://example.org/jwks", + "name": "mockJwksUri", "type": "both", "elementType": "jwksUri" }, { "value": "mockClientSecretValue", + "name": "mockClientSecret", "type": "both", "elementType": "clientSecret" } ] }, "serviceEnabled": false, - "serviceProviderName": "charlesTest3", + "serviceProviderName": "charlesTest", "serviceProviderSsoDescriptor": { - "protocolSupportEnum": "http://openid.net/specs/openid-connect-core-1_0.html", "nameIdFormats": [ "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "urn:mace:shibboleth:metadata:oidc:1.0:nameid-format:pairwise" ], "extensions": { "OAuthRPExtensions": { + "audiences": [ + "http://mypeeps" + ], "postLogoutRedirectUris": [ "https://example.org/postLogout" ], diff --git a/backend/src/test/resources/metadata/SHIBUI-2380.xml b/backend/src/test/resources/metadata/SHIBUI-2380.xml new file mode 100644 index 000000000..7f292cdcd --- /dev/null +++ b/backend/src/test/resources/metadata/SHIBUI-2380.xml @@ -0,0 +1,76 @@ + + + + + + + password + mfa + https://example.org/request + https://example.org/postLogout + http://mypeeps + + + + + + + MIIEQDCCAqigAwIBAgIVAIarXvdvyS47KJR7U40FlTufyD8vMA0GCSqGSIb3DQEB + + + + + + + + + MIIBKDCBzgIJAOYlspXlaqguMAoGCCqGSM49BAMCMBwxCzAJBgNVBAYTAkZJMQ0w + + + + + + + mockJwksUri + https://example.org/jwks + + + + + mockClientSecret + mockClientSecretValue + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + urn:mace:shibboleth:metadata:oidc:1.0:nameid-format:pairwise + + + + + \ No newline at end of file diff --git a/backend/src/test/resources/modified-saml2-assertion-config.xml b/backend/src/test/resources/modified-saml2-assertion-config.xml new file mode 100644 index 000000000..5dcb3688d --- /dev/null +++ b/backend/src/test/resources/modified-saml2-assertion-config.xml @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file