This is an automated email from the ASF dual-hosted git repository.
ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/master by this push:
new 80cf524 [SYNCOPE-1579] Adding SAML 2.0 support via pac4j-saml (#208)
80cf524 is described below
commit 80cf524e9db6008c047c1b5668d6e5cc0cd58bbb
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Mon Jul 27 15:26:29 2020 +0200
[SYNCOPE-1579] Adding SAML 2.0 support via pac4j-saml (#208)
---
.../main/resources/archetype-resources/wa/pom.xml | 4 -
.../api/service/wa/WASAML2IdPMetadataService.java | 4 +-
.../core/persistence/jpa/dao/JPAGroupDAO.java | 11 +-
.../provisioning/java/MappingManagerImplTest.java | 2 +-
docker/wa/pom.xml | 2 -
docker/wa/src/main/resources/wa.properties | 6 +-
ext/saml2sp/pom.xml | 68 ++++-
fit/wa-reference/pom.xml | 9 +-
.../src/main/resources/idp-metadata.xml | 144 -----------
fit/wa-reference/src/main/resources/wa.properties | 3 +-
.../org/apache/syncope/fit/sra/AbstractITCase.java | 182 +++++++------
.../apache/syncope/fit/sra/OAUTH2SRAITCase.java | 4 -
.../org/apache/syncope/fit/sra/OIDCSRAITCase.java | 116 ++++-----
.../org/apache/syncope/fit/sra/SAML2SRAITCase.java | 282 +++++++++++++++++++++
.../test/resources/application-saml2.properties | 26 +-
pom.xml | 112 +++-----
sra/pom.xml | 22 +-
.../org/apache/syncope/sra/SecurityConfig.java | 73 +++++-
.../AbstractServerLogoutSuccessHandler.java | 77 ++++++
.../{ => oauth2}/OAuth2SecurityConfigUtils.java | 13 +-
...cClientInitiatedServerLogoutSuccessHandler.java | 74 +-----
.../sra/security/pac4j/ServerHttpContext.java | 226 +++++++++++++++++
.../sra/security/pac4j/ServerHttpSessionStore.java | 68 +++++
.../security/saml2/SAML2AnonymousWebFilter.java | 57 +++++
.../security/saml2/SAML2AuthenticationToken.java | 49 ++++
.../sra/security/saml2/SAML2BindingType.java | 36 +++
.../sra/security/saml2/SAML2MetadataEndpoint.java | 54 ++++
.../sra/security/saml2/SAML2RequestGenerator.java | 65 +++++
.../security/saml2/SAML2SecurityConfigUtils.java | 96 +++++++
.../security/saml2/SAML2ServerLogoutHandler.java | 56 ++++
.../saml2/SAML2ServerLogoutSuccessHandler.java | 33 +++
.../SAML2WebSsoAuthenticationRequestWebFilter.java | 67 +++++
.../saml2/SAML2WebSsoAuthenticationWebFilter.java | 117 +++++++++
...DoNothingIfCommittedServerRedirectStrategy.java | 34 +++
.../resources/debug/application-debug.properties | 32 ++-
sra/src/test/resources/debug/saml.keystore.jks | Bin 0 -> 9263 bytes
wa/pom.xml | 9 -
.../metadata/RestfulSamlIdPMetadataGenerator.java | 9 +-
.../metadata/RestfulSamlIdPMetadataLocator.java | 43 ++--
wa/starter/src/main/resources/wa.properties | 6 +-
wa/starter/src/test/resources/debug/wa.properties | 3 +-
41 files changed, 1779 insertions(+), 515 deletions(-)
diff --git a/archetype/src/main/resources/archetype-resources/wa/pom.xml
b/archetype/src/main/resources/archetype-resources/wa/pom.xml
index 044806d..a1c919d 100644
--- a/archetype/src/main/resources/archetype-resources/wa/pom.xml
+++ b/archetype/src/main/resources/archetype-resources/wa/pom.xml
@@ -32,10 +32,6 @@ under the License.
<artifactId>${artifactId}</artifactId>
<packaging>war</packaging>
- <properties>
- <opensaml.version>3.4.5</opensaml.version>
- </properties>
-
<dependencies>
<dependency>
<groupId>${groupId}</groupId>
diff --git
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/WASAML2IdPMetadataService.java
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/WASAML2IdPMetadataService.java
index 219abb0..f5b73d4 100644
---
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/WASAML2IdPMetadataService.java
+++
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/WASAML2IdPMetadataService.java
@@ -51,6 +51,8 @@ import
org.apache.syncope.common.rest.api.service.JAXRSService;
@Path("wa/saml2idp/metadata")
public interface WASAML2IdPMetadataService extends JAXRSService {
+ String DEFAULT_OWNER = "Syncope";
+
/**
* Returns a document outlining keys and metadata of Syncope as SAML 2.0
IdP.
*
@@ -60,7 +62,7 @@ public interface WASAML2IdPMetadataService extends
JAXRSService {
*/
@GET
@Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML,
MediaType.APPLICATION_XML })
- SAML2IdPMetadataTO getByOwner(@QueryParam("appliesTo")
@DefaultValue("Syncope") String appliesTo);
+ SAML2IdPMetadataTO getByOwner(@QueryParam("appliesTo")
@DefaultValue(DEFAULT_OWNER) String appliesTo);
/**
* Returns the SAML 2.0 IdP metadata matching the given key.
diff --git
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java
index a8ea10f..19d0901 100644
---
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java
+++
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java
@@ -20,7 +20,6 @@ package org.apache.syncope.core.persistence.jpa.dao;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -255,11 +254,10 @@ public class JPAGroupDAO extends AbstractAnyDAO<Group>
implements GroupDAO {
clearUDynMembers(merged);
if (merged.getUDynMembership() != null) {
SearchCond cond =
buildDynMembershipCond(merged.getUDynMembership().getFIQLCond(),
merged.getRealm());
- int count = searchDAO.count(
-
Collections.<String>singleton(merged.getRealm().getFullPath()), cond,
AnyTypeKind.USER);
+ int count =
searchDAO.count(Set.of(merged.getRealm().getFullPath()), cond,
AnyTypeKind.USER);
for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
page++) {
List<User> matching = searchDAO.search(
-
Collections.<String>singleton(merged.getRealm().getFullPath()),
+ Set.of(merged.getRealm().getFullPath()),
cond,
page,
AnyDAO.DEFAULT_PAGE_SIZE,
@@ -279,11 +277,10 @@ public class JPAGroupDAO extends AbstractAnyDAO<Group>
implements GroupDAO {
clearADynMembers(merged);
merged.getADynMemberships().forEach(memb -> {
SearchCond cond = buildDynMembershipCond(memb.getFIQLCond(),
merged.getRealm());
- int count = searchDAO.count(
-
Collections.<String>singleton(merged.getRealm().getFullPath()), cond,
AnyTypeKind.ANY_OBJECT);
+ int count =
searchDAO.count(Set.of(merged.getRealm().getFullPath()), cond,
AnyTypeKind.ANY_OBJECT);
for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
page++) {
List<AnyObject> matching = searchDAO.search(
-
Collections.<String>singleton(merged.getRealm().getFullPath()),
+ Set.of(merged.getRealm().getFullPath()),
cond,
page,
AnyDAO.DEFAULT_PAGE_SIZE,
diff --git
a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MappingManagerImplTest.java
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MappingManagerImplTest.java
index b8137f4..81bd345 100644
---
a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MappingManagerImplTest.java
+++
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MappingManagerImplTest.java
@@ -264,7 +264,7 @@ public class MappingManagerImplTest extends AbstractTest {
// 2. verify that dynamic membership is in place
assertTrue(userDAO.findAllGroupKeys(user).contains(group.getKey()));
- // 3.
+ // 3. check propagation attrs
ExternalResource csv = resourceDAO.find("resource-csv");
Provision provision = csv.getProvision(AnyTypeKind.USER.name()).get();
assertNotNull(provision);
diff --git a/docker/wa/pom.xml b/docker/wa/pom.xml
index 428ae14..d7531e2 100644
--- a/docker/wa/pom.xml
+++ b/docker/wa/pom.xml
@@ -34,8 +34,6 @@ under the License.
<packaging>war</packaging>
<properties>
- <opensaml.version>4.0.0</opensaml.version>
-
<rootpom.basedir>${basedir}/../..</rootpom.basedir>
</properties>
diff --git a/docker/wa/src/main/resources/wa.properties
b/docker/wa/src/main/resources/wa.properties
index eb69a87..01534fd 100644
--- a/docker/wa/src/main/resources/wa.properties
+++ b/docker/wa/src/main/resources/wa.properties
@@ -27,9 +27,13 @@ cas.server.name=http://localhost:8080
cas.server.prefix=${cas.server.name}/syncope-wa
cas.server.scope=syncope.org
+cas.tgc.secure=false
cas.logout.follow-service-redirects=true
-cas.authn.saml-idp.entity-id=https://syncope.apache.org/saml
+cas.authn.saml-idp.entity-id=http://localhost:8080/saml
+cas.authn.saml-idp.metadata.metadata-backup-location=file:${conf.directory}/saml
+
+cas.authn.oidc.issuer=http://localhost:8080/syncope-wa/oidc/
# Disable access to the login endpoint
# if no target application is specified.
diff --git a/ext/saml2sp/pom.xml b/ext/saml2sp/pom.xml
index d7a23b7..68cacbd 100644
--- a/ext/saml2sp/pom.xml
+++ b/ext/saml2sp/pom.xml
@@ -34,9 +34,75 @@ under the License.
<packaging>pom</packaging>
<properties>
+ <opensaml.version>3.3.1</opensaml.version>
<rootpom.basedir>${basedir}/../..</rootpom.basedir>
</properties>
-
+
+ <dependencyManagement>
+ <dependencies>
+ <!-- OpenSAML -->
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-saml-api</artifactId>
+ <version>${opensaml.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-storage-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-messaging-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-saml-impl</artifactId>
+ <version>${opensaml.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-soap-impl</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-storage-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-messaging-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <!-- /OpenSAML -->
+ </dependencies>
+ </dependencyManagement>
+
<modules>
<module>common-lib</module>
<module>persistence-api</module>
diff --git a/fit/wa-reference/pom.xml b/fit/wa-reference/pom.xml
index b609cbd..9bf1300 100644
--- a/fit/wa-reference/pom.xml
+++ b/fit/wa-reference/pom.xml
@@ -34,8 +34,6 @@ under the License.
<packaging>war</packaging>
<properties>
- <opensaml.version>4.0.0</opensaml.version>
-
<ianal.skip>true</ianal.skip>
<rootpom.basedir>${basedir}/../..</rootpom.basedir>
@@ -243,6 +241,13 @@ under the License.
<directory>src/test/resources</directory>
<filtering>true</filtering>
</testResource>
+ <testResource>
+ <directory>${basedir}/../../sra/src/test/resources/debug</directory>
+ <filtering>false</filtering>
+ <includes>
+ <include>saml.keystore.jks</include>
+ </includes>
+ </testResource>
</testResources>
</build>
diff --git a/fit/wa-reference/src/main/resources/idp-metadata.xml
b/fit/wa-reference/src/main/resources/idp-metadata.xml
deleted file mode 100644
index 2e0162b..0000000
--- a/fit/wa-reference/src/main/resources/idp-metadata.xml
+++ /dev/null
@@ -1,144 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- ~ Licensed to the Apache Software Foundation (ASF) under one
- ~ or more contributor license agreements. See the NOTICE file
- ~ distributed with this work for additional information
- ~ regarding copyright ownership. The ASF licenses this file
- ~ to you under the Apache License, Version 2.0 (the
- ~ "License"); you may not use this file except in compliance
- ~ with the License. You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing,
- ~ software distributed under the License is distributed on an
- ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- ~ KIND, either express or implied. See the License for the
- ~ specific language governing permissions and limitations
- ~ under the License.
- -->
-<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
xmlns:xml="http://www.w3.org/XML/1998/namespace"
xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
entityID="https://syncope.apache.org/idp">
- <IDPSSODescriptor
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol
urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
- <Extensions>
- <shibmd:Scope regexp="false">example.net</shibmd:Scope>
- </Extensions>
- <KeyDescriptor use="signing">
- <ds:KeyInfo>
- <ds:X509Data>
-
<ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
-
BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
-
NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
-
ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
-
R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
-
BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
-
I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
-
EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
-
ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
-
9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
-
HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
-
L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
-
sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
-
K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
-
+Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
-
9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
-
i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
- zg==</ds:X509Certificate>
- </ds:X509Data>
- </ds:KeyInfo>
- </KeyDescriptor>
- <KeyDescriptor use="encryption">
- <ds:KeyInfo>
- <ds:X509Data>
-
<ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUKymtgciRE6pWwDfrsI58qL9pQMgwDQYJKoZIhvcNAQEL
-
BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
-
NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
-
ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKrT805sny3vlaYjTn
-
+m6m3VbUoStvnacgwHH+orNFhHoV1HP2ndoH5BsEDB9tQYuyRbGUm/nYVOSHayzB
-
G3bzMGdU7woO6rsCqpHuxUyEvojd/y/N/r9jMzeBOCu0KDBTrn3BJhnGSwSTfhOS
-
3r20JFmDuTkHmabRs7ro0BvDaQ29jh38ro1iwB4E/4mqb1zYP13NI3ooErN/o6pl
-
XKpnFY37bDDOyOuocjN9tfPNIANNFKah0HjWOP0Nso0D1g6jHOSzmOw/Yxg61vBk
-
qOD4aKhLYPAxsXRl80nDrwTnm3/9xLQj9D3uLAtDLnn9pSqn3jCLxsxsHfKL/zkB
-
IKEBAgMBAAGjYzBhMB0GA1UdDgQWBBSrPjAgCJIHYmsofDcDIPzEhnYxmTBABgNV
-
HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
-
L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAI8MlofbE0tbq8ez2d0Lq
-
Syhp4Q/shMEwjqcDarOwR+ACB9McOannUpAG7TCDp8Ch5E/V1B0Uo/5DF2tAzB1y
-
7sgAmy2mY9/mFhMYpOqTCagufwewaMkn9n7ETzC/6vQEjYrjiNyNR0F3UQQz2bhe
-
ROM3YuKctuOnMthc+ZE7vn+AXCGumRHBhyCaYdzfeUh7id+yrd9B51+o3iF4eu6w
-
zJi5z7FMCS6I4PSc/uWYDw1ahzoPONjazWSEWGUibZaJYM3pJHkuwqyWKOFGVknH
-
J1Qv4WCfSPb6eva94TZX0lkLM01C7NZObnfxY3fvJGcyFl8wlRTUYvuqM8md5CEp
- LA==</ds:X509Certificate>
- </ds:X509Data>
- </ds:KeyInfo>
- </KeyDescriptor>
-
- <!--
- <ArtifactResolutionService
Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
-
Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/ArtifactResolution"
index="1"/>
- -->
-
- <SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SLO"/>
- <SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SLO" />
-
- <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
-
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
-
- <SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SSO"/>
- <SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST-SimpleSign/SSO"/>
- <SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SSO"/>
- <SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/ECP"/>
- </IDPSSODescriptor>
-
- <!--
- <AttributeAuthorityDescriptor
protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol
urn:oasis:names:tc:SAML:2.0:protocol">
- <Extensions>
- <shibmd:Scope regexp="false">example.net</shibmd:Scope>
- </Extensions>
- <KeyDescriptor use="signing">
- <ds:KeyInfo>
- <ds:X509Data>
-
<ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
-BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
-NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
-ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
-R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
-BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
-I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
-EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
-ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
-9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
-HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
-L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
-sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
-K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
-+Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
-9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
-i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
-zg==</ds:X509Certificate>
- </ds:X509Data>
- </ds:KeyInfo>
- </KeyDescriptor>
- <AttributeService
Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/AttributeQuery"/>
- <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/AttributeQuery"/>
- </AttributeAuthorityDescriptor>
- -->
-
- <!--
- <Organization>
- <OrganizationName xml:lang="en">Institution Name</OrganizationName>
- <OrganizationDisplayName xml:lang="en">Institution
DisplayName</OrganizationDisplayName>
- <OrganizationURL xml:lang="en">URL</OrganizationURL>
- </Organization>
- <ContactPerson contactType="administrative">
- <GivenName>John Smith</GivenName>
- <EmailAddress>[email protected]</EmailAddress>
- </ContactPerson>
- <ContactPerson contactType="technical">
- <GivenName>John Smith</GivenName>
- <EmailAddress>[email protected]</EmailAddress>
- </ContactPerson>
- <ContactPerson contactType="support">
- <GivenName>IT Services Support</GivenName>
- <EmailAddress>[email protected]</EmailAddress>
- </ContactPerson>
- -->
-</EntityDescriptor>
diff --git a/fit/wa-reference/src/main/resources/wa.properties
b/fit/wa-reference/src/main/resources/wa.properties
index fc6e2ca..8c94d91 100644
--- a/fit/wa-reference/src/main/resources/wa.properties
+++ b/fit/wa-reference/src/main/resources/wa.properties
@@ -31,7 +31,8 @@ cas.authn.syncope.url=http://localhost:9080/syncope/rest/
cas.tgc.secure=false
cas.logout.follow-service-redirects=true
-cas.authn.saml-idp.entity-id=https://syncope.apache.org/saml
+cas.authn.saml-idp.entity-id=http://localhost:9080/saml
+cas.authn.saml-idp.metadata.metadata-backup-location=file:${conf.directory}/saml
cas.authn.oidc.issuer=http://localhost:9080/syncope-wa/oidc/
cas.authn.oidc.id-token-signing-alg-values-supported=RS256,RS384,RS512,PS256,PS384,PS512,ES256,ES384,ES512,HS256,HS384,HS512
diff --git
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java
index 3a76fee..db6e6a0 100644
---
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java
+++
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java
@@ -19,33 +19,47 @@
package org.apache.syncope.fit.sra;
import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.http.Consts;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
import org.apache.syncope.client.lib.SyncopeClient;
import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
-import org.apache.syncope.common.lib.auth.SyncopeAuthModuleConf;
import org.apache.syncope.common.lib.policy.AuthPolicyTO;
import org.apache.syncope.common.lib.policy.DefaultAuthPolicyConf;
-import org.apache.syncope.common.lib.to.AuthModuleTO;
import org.apache.syncope.common.lib.to.SRARouteTO;
-import org.apache.syncope.common.lib.to.client.OIDCRPTO;
-import org.apache.syncope.common.lib.types.ClientAppType;
-import org.apache.syncope.common.lib.types.OIDCSubjectType;
import org.apache.syncope.common.lib.types.PolicyType;
import org.apache.syncope.common.lib.types.SRARouteFilter;
import org.apache.syncope.common.lib.types.SRARouteFilterFactory;
@@ -53,7 +67,6 @@ import org.apache.syncope.common.lib.types.SRARoutePredicate;
import org.apache.syncope.common.lib.types.SRARoutePredicateFactory;
import org.apache.syncope.common.lib.types.SRARouteType;
import org.apache.syncope.common.rest.api.RESTHeaders;
-import org.apache.syncope.common.rest.api.service.AuthModuleService;
import org.apache.syncope.common.rest.api.service.ClientAppService;
import org.apache.syncope.common.rest.api.service.PolicyService;
import org.apache.syncope.common.rest.api.service.SRARouteService;
@@ -89,8 +102,6 @@ public abstract class AbstractITCase {
protected static SyncopeClient adminClient;
- protected static AuthModuleService authModuleService;
-
protected static PolicyService policyService;
protected static ClientAppService clientAppService;
@@ -104,11 +115,9 @@ public abstract class AbstractITCase {
clientFactory = new
SyncopeClientFactoryBean().setAddress(CORE_ADDRESS);
adminClient = clientFactory.create(ADMIN_UNAME, ADMIN_PWD);
- authModuleService = adminClient.getService(AuthModuleService.class);
policyService = adminClient.getService(PolicyService.class);
clientAppService = adminClient.getService(ClientAppService.class);
sraRouteService = adminClient.getService(SRARouteService.class);
-
}
@BeforeAll
@@ -193,70 +202,6 @@ public abstract class AbstractITCase {
}
}
- protected static void oidcClientAppSetup(
- final String appName,
- final String sraRegistrationId,
- final Long clientAppId,
- final String clientId,
- final String clientSecret) {
-
- AuthModuleTO syncopeAuthModule = authModuleService.list().stream().
- filter(module -> module.getConf() instanceof
SyncopeAuthModuleConf).
- findFirst().orElseThrow(() -> new
IllegalArgumentException("Could not find Syncope Auth Module"));
-
- AuthPolicyTO syncopeAuthPolicy =
policyService.list(PolicyType.AUTH).stream().
- map(AuthPolicyTO.class::cast).
- filter(policy -> policy.getConf() instanceof
DefaultAuthPolicyConf
- && ((DefaultAuthPolicyConf)
policy.getConf()).getAuthModules().contains(syncopeAuthModule.getKey())).
- findFirst().
- orElseGet(() -> {
- DefaultAuthPolicyConf policyConf = new
DefaultAuthPolicyConf();
-
policyConf.getAuthModules().add(syncopeAuthModule.getKey());
-
- AuthPolicyTO policy = new AuthPolicyTO();
- policy.setDescription("Syncope authentication");
- policy.setConf(policyConf);
-
- Response response = policyService.create(PolicyType.AUTH,
policy);
- if (response.getStatusInfo().getStatusCode() !=
Response.Status.CREATED.getStatusCode()) {
- fail("Could not create Syncope Auth Policy");
- }
-
- return policyService.read(PolicyType.AUTH,
response.getHeaderString(RESTHeaders.RESOURCE_KEY));
- });
-
- OIDCRPTO clientApp =
clientAppService.list(ClientAppType.OIDCRP).stream().
- filter(app -> appName.equals(app.getName())).
- map(OIDCRPTO.class::cast).
- findFirst().
- orElseGet(() -> {
- OIDCRPTO app = new OIDCRPTO();
- app.setName(appName);
- app.setClientAppId(clientAppId);
- app.setClientId(clientId);
- app.setClientSecret(clientSecret);
-
- Response response =
clientAppService.create(ClientAppType.OIDCRP, app);
- if (response.getStatusInfo().getStatusCode() !=
Response.Status.CREATED.getStatusCode()) {
- fail("Could not create OIDC Client App");
- }
-
- return clientAppService.read(
- ClientAppType.OIDCRP,
response.getHeaderString(RESTHeaders.RESOURCE_KEY));
- });
-
- clientApp.setClientId(clientId);
- clientApp.setClientSecret(clientSecret);
- clientApp.setSubjectType(OIDCSubjectType.PUBLIC);
- clientApp.getRedirectUris().add(SRA_ADDRESS + "/login/oauth2/code/" +
sraRegistrationId);
- clientApp.setAuthPolicy(syncopeAuthPolicy.getKey());
- clientApp.setSignIdToken(true);
- clientApp.setLogoutUri(SRA_ADDRESS + "/logout");
-
- clientAppService.update(ClientAppType.OIDCRP, clientApp);
- clientAppService.pushToWA();
- }
-
protected static void doStartSRA(final String activeProfile)
throws IOException, InterruptedException, TimeoutException {
@@ -304,11 +249,36 @@ public abstract class AbstractITCase {
}
return connected;
});
- assertTrue(WebClient.create(SRA_ADDRESS).get().getStatus() < 400);
+ assertDoesNotThrow(() ->
WebClient.create(SRA_ADDRESS).get().getStatus());
sraRouteService.pushToSRA();
}
+ protected static AuthPolicyTO getAuthPolicy() {
+ String authModule = "DefaultSyncopeAuthModule";
+
+ return policyService.list(PolicyType.AUTH).stream().
+ map(AuthPolicyTO.class::cast).
+ filter(policy -> policy.getConf() instanceof
DefaultAuthPolicyConf
+ && ((DefaultAuthPolicyConf)
policy.getConf()).getAuthModules().contains(authModule)).
+ findFirst().
+ orElseGet(() -> {
+ DefaultAuthPolicyConf policyConf = new
DefaultAuthPolicyConf();
+ policyConf.getAuthModules().add(authModule);
+
+ AuthPolicyTO policy = new AuthPolicyTO();
+ policy.setDescription("Syncope authentication");
+ policy.setConf(policyConf);
+
+ Response response = policyService.create(PolicyType.AUTH,
policy);
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.CREATED.getStatusCode()) {
+ fail("Could not create Syncope Auth Policy");
+ }
+
+ return policyService.read(PolicyType.AUTH,
response.getHeaderString(RESTHeaders.RESOURCE_KEY));
+ });
+ }
+
@AfterAll
public static void stopSRA() throws InterruptedException {
if (SRA != null) {
@@ -316,4 +286,64 @@ public abstract class AbstractITCase {
SRA.waitFor();
}
}
+
+ protected static String extractCASExecution(final String responseBody) {
+ int begin = responseBody.indexOf("name=\"execution\" value=\"");
+ assertNotEquals(-1, begin);
+ int end = responseBody.indexOf("\"/><input type=\"hidden\"
name=\"_eventId\"");
+ assertNotEquals(-1, end);
+
+ String execution = responseBody.substring(begin + 24, end);
+ assertNotNull(execution);
+ return execution;
+ }
+
+ protected static CloseableHttpResponse authenticateToCas(
+ final String responseBody, final CloseableHttpClient httpclient,
final HttpClientContext context)
+ throws IOException {
+
+ List<NameValuePair> form = new ArrayList<>();
+ form.add(new BasicNameValuePair("_eventId", "submit"));
+ form.add(new BasicNameValuePair("execution",
extractCASExecution(responseBody)));
+ form.add(new BasicNameValuePair("username", "bellini"));
+ form.add(new BasicNameValuePair("password", "password"));
+ form.add(new BasicNameValuePair("geolocation", ""));
+
+ HttpPost post = new HttpPost(WA_ADDRESS + "/login");
+ post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8));
+ return httpclient.execute(post, context);
+ }
+
+ protected static ObjectNode checkGetResponse(
+ final CloseableHttpResponse response, final String
originalRequestURI) throws IOException {
+
+ assertEquals(HttpStatus.SC_OK,
response.getStatusLine().getStatusCode());
+
+ assertEquals(MediaType.APPLICATION_JSON,
response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue());
+
+ JsonNode json =
OBJECT_MAPPER.readTree(EntityUtils.toString(response.getEntity()));
+
+ ObjectNode args = (ObjectNode) json.get("args");
+ assertEquals("value1", args.get("key1").asText());
+
+ ArrayNode key2 = (ArrayNode) args.get("key2");
+ assertEquals("value2", key2.get(0).asText());
+ assertEquals("value3", key2.get(1).asText());
+
+ ObjectNode headers = (ObjectNode) json.get("headers");
+ assertEquals(MediaType.TEXT_HTML,
headers.get(HttpHeaders.ACCEPT).asText());
+ assertEquals(EN_LANGUAGE,
headers.get(HttpHeaders.ACCEPT_LANGUAGE).asText());
+ assertEquals("localhost:" + PORT,
headers.get("X-Forwarded-Host").asText());
+
+ assertEquals(originalRequestURI, json.get("url").asText());
+
+ return headers;
+ }
+
+ protected void checkLogout(final CloseableHttpResponse response) throws
IOException {
+ assertEquals(HttpStatus.SC_NO_CONTENT,
response.getStatusLine().getStatusCode());
+ assertEquals("true",
response.getFirstHeader(LOGGED_OUT_HEADER).getValue());
+ }
}
diff --git
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
index a1288a4..430c1ac 100644
---
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
+++
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
@@ -18,10 +18,6 @@
*/
package org.apache.syncope.fit.sra;
-import static org.apache.syncope.fit.sra.AbstractITCase.doStartSRA;
-import static org.apache.syncope.fit.sra.AbstractITCase.oidcClientAppSetup;
-import static org.apache.syncope.fit.sra.OIDCSRAITCase.CLIENT_ID;
-import static org.apache.syncope.fit.sra.OIDCSRAITCase.CLIENT_SECRET;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
diff --git
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java
index 079a13b..191cd32 100644
---
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java
+++
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java
@@ -27,7 +27,6 @@ import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
@@ -58,6 +57,10 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
+import org.apache.syncope.common.lib.to.client.OIDCRPTO;
+import org.apache.syncope.common.lib.types.ClientAppType;
+import org.apache.syncope.common.lib.types.OIDCSubjectType;
+import org.apache.syncope.common.rest.api.RESTHeaders;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -76,6 +79,45 @@ public class OIDCSRAITCase extends AbstractITCase {
doStartSRA("oidc");
}
+ protected static void oidcClientAppSetup(
+ final String appName,
+ final String sraRegistrationId,
+ final Long clientAppId,
+ final String clientId,
+ final String clientSecret) {
+
+ OIDCRPTO clientApp =
clientAppService.list(ClientAppType.OIDCRP).stream().
+ filter(app -> appName.equals(app.getName())).
+ map(OIDCRPTO.class::cast).
+ findFirst().
+ orElseGet(() -> {
+ OIDCRPTO app = new OIDCRPTO();
+ app.setName(appName);
+ app.setClientAppId(clientAppId);
+ app.setClientId(clientId);
+ app.setClientSecret(clientSecret);
+
+ Response response =
clientAppService.create(ClientAppType.OIDCRP, app);
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.CREATED.getStatusCode()) {
+ fail("Could not create OIDC Client App");
+ }
+
+ return clientAppService.read(
+ ClientAppType.OIDCRP,
response.getHeaderString(RESTHeaders.RESOURCE_KEY));
+ });
+
+ clientApp.setClientId(clientId);
+ clientApp.setClientSecret(clientSecret);
+ clientApp.setSubjectType(OIDCSubjectType.PUBLIC);
+ clientApp.getRedirectUris().add(SRA_ADDRESS + "/login/oauth2/code/" +
sraRegistrationId);
+ clientApp.setAuthPolicy(getAuthPolicy().getKey());
+ clientApp.setSignIdToken(true);
+ clientApp.setLogoutUri(SRA_ADDRESS + "/logout");
+
+ clientAppService.update(ClientAppType.OIDCRP, clientApp);
+ clientAppService.pushToWA();
+ }
+
@BeforeAll
public static void clientAppSetup() {
assumeTrue(OIDCSRAITCase.class.equals(MethodHandles.lookup().lookupClass()));
@@ -95,37 +137,6 @@ public class OIDCSRAITCase extends AbstractITCase {
oidcClientAppSetup(OIDCSRAITCase.class.getName(), "OIDC", 1L,
CLIENT_ID, CLIENT_SECRET);
}
- private ObjectNode checkResponse(final CloseableHttpResponse response,
final String originalRequestURI)
- throws IOException {
-
- assertEquals(HttpStatus.SC_OK,
response.getStatusLine().getStatusCode());
-
- assertEquals(MediaType.APPLICATION_JSON,
response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue());
-
- JsonNode json =
OBJECT_MAPPER.readTree(EntityUtils.toString(response.getEntity()));
-
- ObjectNode args = (ObjectNode) json.get("args");
- assertEquals("value1", args.get("key1").asText());
-
- ArrayNode key2 = (ArrayNode) args.get("key2");
- assertEquals("value2", key2.get(0).asText());
- assertEquals("value3", key2.get(1).asText());
-
- ObjectNode headers = (ObjectNode) json.get("headers");
- assertEquals(MediaType.TEXT_HTML,
headers.get(HttpHeaders.ACCEPT).asText());
- assertEquals(EN_LANGUAGE,
headers.get(HttpHeaders.ACCEPT_LANGUAGE).asText());
- assertEquals("localhost:" + PORT,
headers.get("X-Forwarded-Host").asText());
-
- assertEquals(originalRequestURI, json.get("url").asText());
-
- return headers;
- }
-
- protected void checkLogout(final CloseableHttpResponse response) {
- assertEquals(HttpStatus.SC_NO_CONTENT,
response.getStatusLine().getStatusCode());
- assertEquals("true",
response.getFirstHeader(LOGGED_OUT_HEADER).getValue());
- }
-
@Test
public void web() throws IOException {
CloseableHttpClient httpclient = HttpClients.createDefault();
@@ -138,7 +149,7 @@ public class OIDCSRAITCase extends AbstractITCase {
get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
CloseableHttpResponse response = httpclient.execute(get, context);
- ObjectNode headers = checkResponse(response,
get.getURI().toASCIIString().replace("/public", ""));
+ ObjectNode headers = checkGetResponse(response,
get.getURI().toASCIIString().replace("/public", ""));
assertFalse(headers.has(HttpHeaders.COOKIE));
// 2. protected
@@ -151,46 +162,21 @@ public class OIDCSRAITCase extends AbstractITCase {
// 2a. redirected to WA login screen
String responseBody = EntityUtils.toString(response.getEntity());
- int begin = responseBody.indexOf("name=\"execution\" value=\"");
- assertNotEquals(-1, begin);
- int end = responseBody.indexOf("\"/><input type=\"hidden\"
name=\"_eventId\"");
- assertNotEquals(-1, end);
-
- String execution = responseBody.substring(begin + 24, end);
- assertNotNull(execution);
-
- List<NameValuePair> form = new ArrayList<>();
- form.add(new BasicNameValuePair("_eventId", "submit"));
- form.add(new BasicNameValuePair("execution", execution));
- form.add(new BasicNameValuePair("username", "bellini"));
- form.add(new BasicNameValuePair("password", "password"));
- form.add(new BasicNameValuePair("geolocation", ""));
-
- HttpPost post = new HttpPost(WA_ADDRESS + "/login");
- post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
- post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
- post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8));
- response = httpclient.execute(post, context);
+ response = authenticateToCas(responseBody, httpclient, context);
// 2b. WA attribute consent screen
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
responseBody = EntityUtils.toString(response.getEntity());
- begin = responseBody.indexOf("name=\"execution\" value=\"");
- assertNotEquals(-1, begin);
- end = responseBody.indexOf("\"/><input type=\"hidden\"
name=\"_eventId\"");
- assertNotEquals(-1, end);
-
- execution = responseBody.substring(begin + 24, end);
- assertNotNull(execution);
+ String execution = extractCASExecution(responseBody);
- form = new ArrayList<>();
+ List<NameValuePair> form = new ArrayList<>();
form.add(new BasicNameValuePair("_eventId", "confirm"));
form.add(new BasicNameValuePair("execution", execution));
form.add(new BasicNameValuePair("option", "1"));
form.add(new BasicNameValuePair("reminder", "30"));
form.add(new BasicNameValuePair("reminderTimeUnit", "days"));
- post = new HttpPost(WA_ADDRESS + "/login");
+ HttpPost post = new HttpPost(WA_ADDRESS + "/login");
post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8));
@@ -207,11 +193,11 @@ public class OIDCSRAITCase extends AbstractITCase {
responseBody = EntityUtils.toString(response.getEntity());
- begin = responseBody.indexOf("name=\"allow\"");
+ int begin = responseBody.indexOf("name=\"allow\"");
assertNotEquals(-1, begin);
begin = responseBody.indexOf("href=\"", begin);
assertNotEquals(-1, begin);
- end = responseBody.indexOf("\">", begin);
+ int end = responseBody.indexOf("\">", begin);
assertNotEquals(-1, end);
String allow = responseBody.substring(begin + 6, end).replace("&",
"&");
@@ -223,7 +209,7 @@ public class OIDCSRAITCase extends AbstractITCase {
get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
response = httpclient.execute(get, context);
- headers = checkResponse(response,
originalRequestURI.replace("/protected", ""));
+ headers = checkGetResponse(response,
originalRequestURI.replace("/protected", ""));
assertTrue(headers.get(HttpHeaders.COOKIE).asText().contains("pac4jCsrfToken"));
// 3. logout
diff --git
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java
new file mode 100644
index 0000000..762f2b4
--- /dev/null
+++
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.fit.sra;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.commons.lang3.tuple.Triple;
+import org.apache.commons.text.StringEscapeUtils;
+import org.apache.http.Consts;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.apache.syncope.common.lib.to.client.SAML2SPTO;
+import org.apache.syncope.common.lib.types.ClientAppType;
+import org.apache.syncope.common.lib.types.SAML2SPNameId;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class SAML2SRAITCase extends AbstractITCase {
+
+ @BeforeAll
+ public static void startSRA() throws IOException, InterruptedException,
TimeoutException {
+
assumeTrue(SAML2SRAITCase.class.equals(MethodHandles.lookup().lookupClass()));
+
+ doStartSRA("saml2");
+ }
+
+ @BeforeAll
+ public static void clientAppSetup() {
+ String appName = SAML2SRAITCase.class.getName();
+ SAML2SPTO clientApp =
clientAppService.list(ClientAppType.SAML2SP).stream().
+ filter(app -> appName.equals(app.getName())).
+ map(SAML2SPTO.class::cast).
+ findFirst().
+ orElseGet(() -> {
+ SAML2SPTO app = new SAML2SPTO();
+ app.setName(appName);
+ app.setClientAppId(3L);
+ app.setEntityId("http://localhost:8080");
+
app.setMetadataLocation("http://localhost:8080/saml2/metadata");
+
+ Response response =
clientAppService.create(ClientAppType.SAML2SP, app);
+ if (response.getStatusInfo().getStatusCode() !=
Response.Status.CREATED.getStatusCode()) {
+ fail("Could not create SAML2 Client App");
+ }
+
+ return clientAppService.read(
+ ClientAppType.SAML2SP,
response.getHeaderString(RESTHeaders.RESOURCE_KEY));
+ });
+
+ clientApp.setSignAssertions(true);
+ clientApp.setSignResponses(true);
+ clientApp.setRequiredNameIdFormat(SAML2SPNameId.PERSISTENT);
+ clientApp.setAuthPolicy(getAuthPolicy().getKey());
+
+ clientAppService.update(ClientAppType.SAML2SP, clientApp);
+ clientAppService.pushToWA();
+ }
+
+ private Triple<String, String, String> parseSAMLRequestForm(final String
responseBody) {
+ int begin = responseBody.indexOf("name=\"RelayState\" value=\"");
+ assertNotEquals(-1, begin);
+ int end = responseBody.indexOf("\"/>", begin);
+ assertNotEquals(-1, end);
+ String relayState = responseBody.substring(begin + 25, end);
+ assertNotNull(relayState);
+
+ begin = responseBody.indexOf("name=\"SAMLRequest\" value=\"");
+ assertNotEquals(-1, begin);
+ end = responseBody.indexOf("\"/>", begin);
+ assertNotEquals(-1, end);
+ String samlRequest = responseBody.substring(begin + 26, end);
+ assertNotNull(samlRequest);
+
+ begin = responseBody.indexOf("<form action=\"");
+ assertNotEquals(-1, begin);
+ end = responseBody.indexOf("\" method=\"post\">");
+ assertNotEquals(-1, end);
+ String action =
StringEscapeUtils.unescapeXml(responseBody.substring(begin + 14, end));
+ assertNotNull(action);
+
+ return Triple.of(action, relayState, samlRequest);
+ }
+
+ private Triple<String, String, String> parseSAMLResponseForm(final String
responseBody) {
+ int begin = responseBody.indexOf("name=\"RelayState\" value=\"");
+ assertNotEquals(-1, begin);
+ int end = responseBody.indexOf("\"/>");
+ assertNotEquals(-1, end);
+ String relayState = responseBody.substring(begin + 26, end);
+ assertNotNull(relayState);
+
+ begin = responseBody.indexOf("name=\"SAMLResponse\" value=\"");
+ assertNotEquals(-1, begin);
+ end = responseBody.indexOf("\"/>", begin);
+ assertNotEquals(-1, end);
+ String samlResponse = responseBody.substring(begin + 27, end);
+ assertNotNull(samlResponse);
+
+ begin = responseBody.indexOf("<form action=\"");
+ assertNotEquals(-1, begin);
+ end = responseBody.indexOf("\" method=\"post\">");
+ assertNotEquals(-1, end);
+ String action =
StringEscapeUtils.unescapeXml(responseBody.substring(begin + 14, end));
+ assertNotNull(action);
+
+ return Triple.of(action, relayState, samlResponse);
+ }
+
+ @Test
+ public void web() throws IOException {
+ CloseableHttpClient httpclient = HttpClients.createDefault();
+ HttpClientContext context = HttpClientContext.create();
+ context.setCookieStore(new BasicCookieStore());
+
+ // 1. public
+ HttpGet get = new HttpGet(SRA_ADDRESS +
"/public/get?key1=value1&key2=value2&key2=value3");
+ get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ CloseableHttpResponse response = httpclient.execute(get, context);
+
+ ObjectNode headers = checkGetResponse(response,
get.getURI().toASCIIString().replace("/public", ""));
+ assertFalse(headers.has(HttpHeaders.COOKIE));
+
+ // 2. protected
+ get = new HttpGet(SRA_ADDRESS +
"/protected/get?key1=value1&key2=value2&key2=value3");
+ String originalRequestURI = get.getURI().toASCIIString();
+ get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ response = httpclient.execute(get, context);
+ assertEquals(HttpStatus.SC_OK,
response.getStatusLine().getStatusCode());
+
+ // 2a. post SAML request
+ String responseBody = EntityUtils.toString(response.getEntity());
+ Triple<String, String, String> parsed =
parseSAMLRequestForm(responseBody);
+
+ HttpPost post = new HttpPost(parsed.getLeft());
+ post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ post.setEntity(new UrlEncodedFormEntity(
+ List.of(new BasicNameValuePair("RelayState",
parsed.getMiddle()),
+ new BasicNameValuePair("SAMLRequest",
parsed.getRight())), Consts.UTF_8));
+ response = httpclient.execute(post, context);
+ assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,
response.getStatusLine().getStatusCode());
+
+ // 2b. authenticate
+ get = new
HttpGet(response.getFirstHeader(HttpHeaders.LOCATION).getValue());
+ get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ response = httpclient.execute(get, context);
+ assertEquals(HttpStatus.SC_OK,
response.getStatusLine().getStatusCode());
+
+ responseBody = EntityUtils.toString(response.getEntity());
+ response = authenticateToCas(responseBody, httpclient, context);
+
+ // 2c. WA attribute consent screen
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ responseBody = EntityUtils.toString(response.getEntity());
+ String execution = extractCASExecution(responseBody);
+
+ List<NameValuePair> form = new ArrayList<>();
+ form.add(new BasicNameValuePair("_eventId", "confirm"));
+ form.add(new BasicNameValuePair("execution", execution));
+ form.add(new BasicNameValuePair("option", "1"));
+ form.add(new BasicNameValuePair("reminder", "30"));
+ form.add(new BasicNameValuePair("reminderTimeUnit", "days"));
+
+ post = new HttpPost(WA_ADDRESS + "/login");
+ post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8));
+ response = httpclient.execute(post, context);
+ }
+ assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,
response.getStatusLine().getStatusCode());
+
+ get = new
HttpGet(response.getFirstHeader(HttpHeaders.LOCATION).getValue());
+ get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ response = httpclient.execute(get, context);
+ assertEquals(HttpStatus.SC_OK,
response.getStatusLine().getStatusCode());
+
+ // 2d. post SAML response
+ responseBody = EntityUtils.toString(response.getEntity());
+ parsed = parseSAMLResponseForm(responseBody);
+
+ post = new HttpPost(parsed.getLeft());
+ post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ post.setEntity(new UrlEncodedFormEntity(
+ List.of(new BasicNameValuePair("RelayState",
parsed.getMiddle()),
+ new BasicNameValuePair("SAMLResponse",
parsed.getRight())), Consts.UTF_8));
+ response = httpclient.execute(post, context);
+ assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,
response.getStatusLine().getStatusCode());
+
+ // 2e. finally get requested content
+ get = new
HttpGet(response.getFirstHeader(HttpHeaders.LOCATION).getValue());
+ get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ response = httpclient.execute(get, context);
+
+ headers = checkGetResponse(response,
originalRequestURI.replace("/protected", ""));
+ assertFalse(headers.get(HttpHeaders.COOKIE).asText().isBlank());
+
+ // 3. logout
+ get = new HttpGet(SRA_ADDRESS + "/protected/logout");
+ get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ response = httpclient.execute(get, context);
+
+ // 3a. post SAML request
+ responseBody = EntityUtils.toString(response.getEntity());
+ parsed = parseSAMLRequestForm(responseBody);
+
+ post = new HttpPost(parsed.getLeft());
+ post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ post.setEntity(new UrlEncodedFormEntity(
+ List.of(new BasicNameValuePair("RelayState",
parsed.getMiddle()),
+ new BasicNameValuePair("SAMLRequest",
parsed.getRight())), Consts.UTF_8));
+ response = httpclient.execute(post, context);
+ assertEquals(HttpStatus.SC_MOVED_TEMPORARILY,
response.getStatusLine().getStatusCode());
+
+ get = new
HttpGet(response.getFirstHeader(HttpHeaders.LOCATION).getValue());
+ get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
+ get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
+ response = httpclient.execute(get, context);
+
+ // 3b. post SAML response
+ // this is missing as currently WA does not responde with form for
SP's SingleLogoutService
+ checkLogout(response);
+ }
+
+ @Override
+ protected void checkLogout(final CloseableHttpResponse response) throws
IOException {
+ assertEquals(HttpStatus.SC_OK,
response.getStatusLine().getStatusCode());
+
+ String responseBody = EntityUtils.toString(response.getEntity());
+ assertTrue(responseBody.contains("Logout successful"));
+ assertTrue(responseBody.contains("have successfully logged out of the
Central Authentication Service"));
+ }
+}
diff --git a/sra/src/test/resources/debug/application-debug.properties
b/fit/wa-reference/src/test/resources/application-saml2.properties
similarity index 57%
copy from sra/src/test/resources/debug/application-debug.properties
copy to fit/wa-reference/src/test/resources/application-saml2.properties
index 5958ee8..fa9a8e4 100644
--- a/sra/src/test/resources/debug/application-debug.properties
+++ b/fit/wa-reference/src/test/resources/application-saml2.properties
@@ -14,20 +14,16 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-#am.type=OIDC
-#am.oidc.configuration=http://localhost:9080/syncope-wa/oidc/
-#am.oidc.client.id=oidcTestClientId
-#am.oidc.client.secret=oidcTestClientSecret
-
-am.type=OAUTH2
-am.oauth2.tokenUri=http://localhost:9080/syncope-wa/oauth2.0/accessToken
-am.oauth2.authorizationUri=http://localhost:9080/syncope-wa/oauth2.0/authorize
-am.oauth2.userInfoUri=http://localhost:9080/syncope-wa/oauth2.0/profile
-am.oauth2.userNameAttributeName=id
-am.oauth2.scopes=
-am.oauth2.jwkSetUri=
-am.oauth2.issuer=http://localhost:9080/syncope-wa
-am.oauth2.client.id=oauth2TestClientId
-am.oauth2.client.secret=oauth2TestClientSecret
+am.type=SAML2
+am.saml2.sp.authnrequest.binding=POST
+am.saml2.sp.logout.request.binding=POST
+am.saml2.sp.logout.response.binding=POST
+am.saml2.sp.entityId=http://localhost:8080
+am.saml2.sp.skew=300
+am.saml2.idp=http://localhost:9080/syncope-wa/idp/metadata
+am.saml2.keystore=classpath:/saml.keystore.jks
+am.saml2.keystore.type=jks
+am.saml2.keystore.storepass=changeit
+am.saml2.keystore.keypass=changeit
global.postLogout=http://localhost:8080/logout
diff --git a/pom.xml b/pom.xml
index b5959f9..26de67f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -431,7 +431,6 @@ under the License.
<camel.version>3.4.2</camel.version>
<slf4j.version>2.0.0-alpha1</slf4j.version>
- <opensaml.version>3.3.1</opensaml.version>
<elasticsearch.version>7.8.0</elasticsearch.version>
@@ -446,6 +445,10 @@ under the License.
<commons-text.version>1.9</commons-text.version>
<commons-logging.version>1.1.3</commons-logging.version>
+ <modernizer-maven.version>2.1.0</modernizer-maven.version>
+
+ <pac4j.version>4.0.3</pac4j.version>
+
<cas.version>6.3.0-SNAPSHOT</cas.version>
<h2.version>1.4.200</h2.version>
@@ -771,67 +774,6 @@ under the License.
</dependency>
<!-- /Camel -->
- <!-- OpenSAML -->
- <dependency>
- <groupId>org.opensaml</groupId>
- <artifactId>opensaml-saml-api</artifactId>
- <version>${opensaml.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.opensaml</groupId>
- <artifactId>opensaml-storage-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.opensaml</groupId>
- <artifactId>opensaml-messaging-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.apache.velocity</groupId>
- <artifactId>velocity</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.apache.httpcomponents</groupId>
- <artifactId>httpclient</artifactId>
- </exclusion>
- <exclusion>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.opensaml</groupId>
- <artifactId>opensaml-saml-impl</artifactId>
- <version>${opensaml.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.opensaml</groupId>
- <artifactId>opensaml-soap-impl</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.opensaml</groupId>
- <artifactId>opensaml-storage-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.opensaml</groupId>
- <artifactId>opensaml-messaging-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.apache.velocity</groupId>
- <artifactId>velocity</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.apache.httpcomponents</groupId>
- <artifactId>httpclient</artifactId>
- </exclusion>
- <exclusion>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <!-- /OpenSAML -->
-
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
@@ -1189,6 +1131,12 @@ under the License.
</dependency>
<dependency>
+ <groupId>org.gaul</groupId>
+ <artifactId>modernizer-maven-annotations</artifactId>
+ <version>${modernizer-maven.version}</version>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>1.24.1</version>
@@ -1421,6 +1369,19 @@ under the License.
</dependency>
<!-- /Flowable -->
+ <!-- PAC4J -->
+ <dependency>
+ <groupId>org.pac4j</groupId>
+ <artifactId>pac4j-saml</artifactId>
+ <version>${pac4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.pac4j</groupId>
+ <artifactId>pac4j-oidc</artifactId>
+ <version>${pac4j.version}</version>
+ </dependency>
+ <!-- PAC4J -->
+
<!-- CAS -->
<dependency>
<groupId>org.apereo.cas</groupId>
@@ -2080,6 +2041,13 @@ under the License.
<enabled>true</enabled>
</snapshots>
</repository>
+ <repository>
+ <id>shibboleth</id>
+ <url>https://build.shibboleth.net/nexus/content/groups/public</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ </repository>
</repositories>
<pluginRepositories>
@@ -2427,7 +2395,7 @@ under the License.
<plugin>
<groupId>org.gaul</groupId>
<artifactId>modernizer-maven-plugin</artifactId>
- <version>2.1.0</version>
+ <version>${modernizer-maven.version}</version>
<configuration>
<javaVersion>${targetJdk}</javaVersion>
<ignorePackages>
@@ -2691,15 +2659,15 @@ under the License.
<link>http://www.slf4j.org/api/</link>
<link>http://connid.tirasa.net/apidocs/1.5/</link>
<link>http://cxf.apache.org/javadoc/latest/</link>
- <link>http://fasterxml.github.io/jackson-core/javadoc/2.10/</link>
-
<link>http://fasterxml.github.io/jackson-databind/javadoc/2.10/</link>
-
<link>http://fasterxml.github.io/jackson-annotations/javadoc/2.10/</link>
-
<link>http://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/</link>
-
<link>http://fasterxml.github.io/jackson-dataformats-text/javadoc/yaml/2.10/</link>
-
<link>http://fasterxml.github.io/jackson-dataformats-text/javadoc/csv/2.10/</link>
+ <link>http://fasterxml.github.io/jackson-core/javadoc/2.11/</link>
+
<link>http://fasterxml.github.io/jackson-databind/javadoc/2.11/</link>
+
<link>http://fasterxml.github.io/jackson-annotations/javadoc/2.11/</link>
+
<link>http://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.11/</link>
+
<link>http://fasterxml.github.io/jackson-dataformats-text/javadoc/yaml/2.11/</link>
+
<link>http://fasterxml.github.io/jackson-dataformats-text/javadoc/csv/2.11/</link>
<link>https://www.javadoc.io/doc/org.apache.camel/camel-core/latest/</link>
<link>https://www.javadoc.io/doc/org.apache.camel/camel-spring/latest/</link>
- <link>https://ci.apache.org/projects/wicket/apidocs/8.x/</link>
+ <link>https://ci.apache.org/projects/wicket/apidocs/9.x/</link>
<link>https://commons.apache.org/proper/commons-lang/javadocs/api-release/</link>
<link>https://commons.apache.org/proper/commons-jexl/apidocs/</link>
<link>https://tika.apache.org/1.24/api/</link>
@@ -2707,8 +2675,8 @@ under the License.
<link>https://docs.spring.io/spring/docs/current/javadoc-api/</link>
<link>https://docs.spring.io/spring-security/site/docs/current/api/</link>
<link>http://www.flowable.org/docs/javadocs/</link>
-
<link>https://build.shibboleth.net/nexus/content/sites/site/java-opensaml/3.3.1/apidocs/</link>
- <link>http://docs.swagger.io/swagger-core/v2.1.1/apidocs/</link>
+
<link>https://build.shibboleth.net/nexus/content/sites/site/java-opensaml/4.0.1/apidocs/</link>
+ <link>http://docs.swagger.io/swagger-core/v2.1.3/apidocs/</link>
</links>
</configuration>
<reportSets>
diff --git a/sra/pom.xml b/sra/pom.xml
index ec6e970..51a114e 100644
--- a/sra/pom.xml
+++ b/sra/pom.xml
@@ -77,8 +77,13 @@ under the License.
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.pac4j</groupId>
+ <artifactId>pac4j-saml</artifactId>
+ </dependency>
+
<dependency>
- <groupId>org.springframework.session</groupId>
+ <groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
@@ -110,6 +115,11 @@ under the License.
</dependency>
<dependency>
+ <groupId>org.gaul</groupId>
+ <artifactId>modernizer-maven-annotations</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
@@ -276,6 +286,16 @@ under the License.
<resource>
<directory>${basedir}/src/test/resources/debug</directory>
<filtering>true</filtering>
+ <excludes>
+ <exclude>saml.keystore.jks</exclude>
+ </excludes>
+ </resource>
+ <resource>
+ <directory>${basedir}/src/test/resources/debug</directory>
+ <filtering>false</filtering>
+ <includes>
+ <include>saml.keystore.jks</include>
+ </includes>
</resource>
</resources>
</build>
diff --git a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
index 6347edc..89646f3 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
+++ b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
@@ -19,15 +19,21 @@
package org.apache.syncope.sra;
import java.text.ParseException;
-import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
import org.apache.syncope.sra.security.CsrfRouteMatcher;
import org.apache.syncope.sra.security.LogoutRouteMatcher;
-import org.apache.syncope.sra.security.OAuth2SecurityConfigUtils;
+import org.apache.syncope.sra.security.oauth2.OAuth2SecurityConfigUtils;
import org.apache.syncope.sra.security.PublicRouteMatcher;
+import org.apache.syncope.sra.security.saml2.SAML2BindingType;
+import org.apache.syncope.sra.security.saml2.SAML2MetadataEndpoint;
+import org.apache.syncope.sra.security.saml2.SAML2SecurityConfigUtils;
+import
org.apache.syncope.sra.security.saml2.SAML2WebSsoAuthenticationWebFilter;
+import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.config.SAML2Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -39,6 +45,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.Environment;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.http.HttpMethod;
import
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import
org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
@@ -57,27 +65,43 @@ import
org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import
org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import reactor.core.publisher.Mono;
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {
- private static final String AM_TYPE = "am.type";
+ public static final String AM_TYPE = "am.type";
public enum AMType {
OIDC,
OAUTH2,
SAML2,
- WA
+ CAS
}
@Autowired
+ private ResourcePatternResolver resourceResolver;
+
+ @Autowired
private Environment env;
@Bean
@Order(0)
+ @ConditionalOnProperty(name = AM_TYPE, havingValue = "SAML2")
+ public SecurityWebFilterChain saml2SecurityFilterChain(final
ServerHttpSecurity http) {
+ ServerWebExchangeMatcher metadataMatcher =
+ ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET,
SAML2MetadataEndpoint.METADATA_URL);
+ return http.securityMatcher(metadataMatcher).
+ authorizeExchange().anyExchange().permitAll().
+ and().csrf().requireCsrfProtectionMatcher(new
NegatedServerWebExchangeMatcher(metadataMatcher)).
+ and().build();
+ }
+
+ @Bean
+ @Order(1)
public SecurityWebFilterChain actuatorSecurityFilterChain(final
ServerHttpSecurity http) {
ServerWebExchangeMatcher actuatorMatcher =
EndpointRequest.toAnyEndpoint();
return http.securityMatcher(actuatorMatcher).
@@ -118,7 +142,7 @@ public class SecurityConfig {
@Bean
@ConditionalOnMissingBean
public Converter<Map<String, Object>, Map<String, Object>>
jwtClaimSetConverter() {
- return MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
+ return MappedJwtClaimSetConverter.withDefaults(Map.of());
}
@Bean
@@ -183,7 +207,39 @@ public class SecurityConfig {
}
@Bean
- @Order(1)
+ @ConditionalOnProperty(name = AM_TYPE, havingValue = "SAML2")
+ public SAML2Client saml2Client() {
+ SAML2Configuration cfg = new SAML2Configuration(
+
resourceResolver.getResource(env.getProperty("am.saml2.keystore")),
+ env.getProperty("am.saml2.keystore.storepass"),
+ env.getProperty("am.saml2.keystore.keypass"),
+ resourceResolver.getResource(env.getProperty("am.saml2.idp")));
+
cfg.setIdentityProviderMetadataResource(resourceResolver.getResource(env.getProperty("am.saml2.idp")));
+ cfg.setAuthnRequestBindingType(
+
SAML2BindingType.valueOf(env.getProperty("am.saml2.sp.authnrequest.binding")).getUri());
+ cfg.setResponseBindingType(SAML2BindingType.POST.getUri());
+ cfg.setSpLogoutRequestBindingType(
+
SAML2BindingType.valueOf(env.getProperty("am.saml2.sp.logout.request.binding")).getUri());
+ cfg.setSpLogoutResponseBindingType(
+
SAML2BindingType.valueOf(env.getProperty("am.saml2.sp.logout.response.binding")).getUri());
+
cfg.setServiceProviderEntityId(env.getProperty("am.saml2.sp.entityId"));
+ cfg.setWantsAssertionsSigned(true);
+ cfg.setAuthnRequestSigned(true);
+ cfg.setSpLogoutRequestSigned(true);
+ cfg.setAcceptedSkew(env.getProperty("am.saml2.sp.skew", int.class));
+
+ SAML2Client saml2Client = new SAML2Client(cfg);
+ saml2Client.setName(AMType.SAML2.name());
+ saml2Client.setCallbackUrl(env.getProperty("am.saml2.sp.entityId")
+ +
SAML2WebSsoAuthenticationWebFilter.DEFAULT_FILTER_PROCESSES_URI);
+ saml2Client.setCallbackUrlResolver(new
NoParameterCallbackUrlResolver());
+ saml2Client.init();
+
+ return saml2Client;
+ }
+
+ @Bean
+ @Order(2)
@ConditionalOnProperty(name = AM_TYPE)
public SecurityWebFilterChain routesSecurityFilterChain(
final ServerHttpSecurity http,
@@ -208,9 +264,12 @@ public class SecurityConfig {
break;
case SAML2:
+ SAML2Client saml2Client = saml2Client();
+ SAML2SecurityConfigUtils.forLogin(http, saml2Client,
publicRouteMatcher);
+ SAML2SecurityConfigUtils.forLogout(builder, saml2Client,
logoutRouteMatcher, ctx);
break;
- case WA:
+ case CAS:
default:
}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/AbstractServerLogoutSuccessHandler.java
b/sra/src/main/java/org/apache/syncope/sra/security/AbstractServerLogoutSuccessHandler.java
new file mode 100644
index 0000000..3133d39
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/AbstractServerLogoutSuccessHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.to.SRARouteTO;
+import org.apache.syncope.sra.RouteProvider;
+import
org.apache.syncope.sra.security.web.server.DoNothingIfCommittedServerRedirectStrategy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.context.ApplicationListener;
+import org.springframework.security.web.server.ServerRedirectStrategy;
+import org.springframework.security.web.server.WebFilterExchange;
+import
org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
+
+public abstract class AbstractServerLogoutSuccessHandler
+ implements ServerLogoutSuccessHandler,
ApplicationListener<RefreshRoutesEvent> {
+
+ private static final Map<String, Optional<URI>> CACHE = new
ConcurrentHashMap<>();
+
+ protected final ServerRedirectStrategy redirectStrategy = new
DoNothingIfCommittedServerRedirectStrategy();
+
+ @Autowired
+ private RouteProvider routeProvider;
+
+ @Value("${global.postLogout}")
+ private URI globalPostLogout;
+
+ @Override
+ public void onApplicationEvent(final RefreshRoutesEvent event) {
+ CACHE.clear();
+ }
+
+ protected URI getPostLogout(final WebFilterExchange exchange) {
+ URI postLogout = globalPostLogout;
+ String routeId =
exchange.getExchange().getAttribute(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
+ if (StringUtils.isNotBlank(routeId)) {
+ Optional<URI> routePostLogout =
Optional.ofNullable(CACHE.get(routeId)).orElseGet(() -> {
+ URI uri = null;
+ Optional<SRARouteTO> route =
routeProvider.getRouteTOs().stream().
+ filter(r -> routeId.equals(r.getKey())).findFirst();
+ if (route.isPresent()) {
+ uri = route.get().getPostLogout();
+ }
+
+ CACHE.put(routeId, Optional.ofNullable(uri));
+ return CACHE.get(routeId);
+ });
+ if (routePostLogout.isPresent()) {
+ postLogout = routePostLogout.get();
+ }
+ }
+ return postLogout;
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/OAuth2SecurityConfigUtils.java
b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SecurityConfigUtils.java
similarity index 96%
rename from
sra/src/main/java/org/apache/syncope/sra/security/OAuth2SecurityConfigUtils.java
rename to
sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SecurityConfigUtils.java
index 5be5e52..cf6bbb4 100644
---
a/sra/src/main/java/org/apache/syncope/sra/security/OAuth2SecurityConfigUtils.java
+++
b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SecurityConfigUtils.java
@@ -16,11 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.syncope.sra.security;
+package org.apache.syncope.sra.security.oauth2;
-import java.util.Collections;
+import java.util.Set;
import org.apache.syncope.sra.ApplicationContextUtils;
import org.apache.syncope.sra.SecurityConfig.AMType;
+import org.apache.syncope.sra.security.LogoutRouteMatcher;
+import org.apache.syncope.sra.security.SessionRemovalServerLogoutHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
@@ -89,6 +91,7 @@ public final class OAuth2SecurityConfigUtils {
OAuth2AuthorizationRequestRedirectWebFilter authRequestRedirectFilter =
new
OAuth2AuthorizationRequestRedirectWebFilter(clientRegistrationRepository);
+ http.addFilterAt(authRequestRedirectFilter,
SecurityWebFiltersOrder.HTTP_BASIC);
AuthenticationWebFilter authenticationFilter =
new
OAuth2LoginAuthenticationWebFilter(authenticationManager(amType),
authorizedClientRepository);
@@ -99,15 +102,13 @@ public final class OAuth2SecurityConfigUtils {
authenticationFilter.setAuthenticationSuccessHandler(new
RedirectServerAuthenticationSuccessHandler());
authenticationFilter.setAuthenticationFailureHandler((exchange, ex) ->
Mono.error(ex));
authenticationFilter.setSecurityContextRepository(new
WebSessionServerSecurityContextRepository());
+ http.addFilterAt(authenticationFilter,
SecurityWebFiltersOrder.AUTHENTICATION);
MediaTypeServerWebExchangeMatcher htmlMatcher = new
MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
- htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
+ htmlMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
ServerAuthenticationEntryPoint entrypoint =
new
RedirectServerAuthenticationEntryPoint("/oauth2/authorization/" +
amType.name());
http.exceptionHandling().authenticationEntryPoint(new
DelegateEntry(htmlMatcher, entrypoint).getEntryPoint());
-
- http.addFilterAt(authRequestRedirectFilter,
SecurityWebFiltersOrder.HTTP_BASIC);
- http.addFilterAt(authenticationFilter,
SecurityWebFiltersOrder.AUTHENTICATION);
}
public static void forLogout(
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/OidcClientInitiatedServerLogoutSuccessHandler.java
b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OidcClientInitiatedServerLogoutSuccessHandler.java
similarity index 65%
rename from
sra/src/main/java/org/apache/syncope/sra/security/OidcClientInitiatedServerLogoutSuccessHandler.java
rename to
sra/src/main/java/org/apache/syncope/sra/security/oauth2/OidcClientInitiatedServerLogoutSuccessHandler.java
index 5ea8498..175ebd6 100644
---
a/sra/src/main/java/org/apache/syncope/sra/security/OidcClientInitiatedServerLogoutSuccessHandler.java
+++
b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OidcClientInitiatedServerLogoutSuccessHandler.java
@@ -16,33 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.syncope.sra.security;
+package org.apache.syncope.sra.security.oauth2;
import java.net.URI;
import java.nio.charset.StandardCharsets;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Resource;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.common.lib.to.SRARouteTO;
-import org.apache.syncope.sra.RouteProvider;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
-import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
-import org.springframework.context.ApplicationListener;
+import org.apache.syncope.sra.security.AbstractServerLogoutSuccessHandler;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import
org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import
org.springframework.security.oauth2.client.registration.ClientRegistration;
import
org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
-import org.springframework.security.web.server.DefaultServerRedirectStrategy;
-import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterExchange;
import
org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
-import
org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
@@ -52,28 +39,22 @@ import org.springframework.web.util.UriComponentsBuilder;
* @see <a
href="https://openid.net/specs/openid-connect-session-1_0.html#RPLogout">RP-Initiated
Logout</a>
* @see
org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler
*/
-public class OidcClientInitiatedServerLogoutSuccessHandler
- implements ServerLogoutSuccessHandler,
ApplicationListener<RefreshRoutesEvent> {
-
- private static final Map<String, Optional<URI>> CACHE = new
ConcurrentHashMap<>();
-
- private final ServerRedirectStrategy redirectStrategy = new
DefaultServerRedirectStrategy();
-
- private final RedirectServerLogoutSuccessHandler
serverLogoutSuccessHandler =
- new RedirectServerLogoutSuccessHandler();
+public class OidcClientInitiatedServerLogoutSuccessHandler extends
AbstractServerLogoutSuccessHandler {
@Resource(name = "oidcClientRegistrationRepository")
private ReactiveClientRegistrationRepository clientRegistrationRepository;
- @Autowired
- private RouteProvider routeProvider;
-
- @Value("${global.postLogout}")
- private URI globalPostLogout;
+ protected final RedirectServerLogoutSuccessHandler
serverLogoutSuccessHandler =
+ new RedirectServerLogoutSuccessHandler();
- @Override
- public void onApplicationEvent(final RefreshRoutesEvent event) {
- CACHE.clear();
+ /**
+ * The URL to redirect to after successfully logging out when not
originally an OIDC login
+ *
+ * @param logoutSuccessUrl the url to redirect to. Default is
"/login?logout".
+ */
+ public void setLogoutSuccessUrl(final URI logoutSuccessUrl) {
+ Assert.notNull(logoutSuccessUrl, "logoutSuccessUrl cannot be null");
+ this.serverLogoutSuccessHandler.setLogoutSuccessUrl(logoutSuccessUrl);
}
@Override
@@ -106,24 +87,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler
UriComponentsBuilder builder =
UriComponentsBuilder.fromUri(endSessionEndpoint);
builder.queryParam("id_token_hint", idToken(authentication));
- URI postLogout = globalPostLogout;
- String routeId =
exchange.getExchange().getAttribute(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
- if (StringUtils.isNotBlank(routeId)) {
- Optional<URI> routePostLogout =
Optional.ofNullable(CACHE.get(routeId)).orElseGet(() -> {
- URI uri = null;
- Optional<SRARouteTO> route =
routeProvider.getRouteTOs().stream().
- filter(r -> routeId.equals(r.getKey())).findFirst();
- if (route.isPresent()) {
- uri = route.get().getPostLogout();
- }
-
- CACHE.put(routeId, Optional.ofNullable(uri));
- return CACHE.get(routeId);
- });
- if (routePostLogout.isPresent()) {
- postLogout = routePostLogout.get();
- }
- }
+ URI postLogout = getPostLogout(exchange);
builder.queryParam("post_logout_redirect_uri", postLogout);
return builder.encode(StandardCharsets.UTF_8).build().toUri();
@@ -132,14 +96,4 @@ public class OidcClientInitiatedServerLogoutSuccessHandler
private String idToken(final Authentication authentication) {
return ((OidcUser)
authentication.getPrincipal()).getIdToken().getTokenValue();
}
-
- /**
- * The URL to redirect to after successfully logging out when not
originally an OIDC login
- *
- * @param logoutSuccessUrl the url to redirect to. Default is
"/login?logout".
- */
- public void setLogoutSuccessUrl(final URI logoutSuccessUrl) {
- Assert.notNull(logoutSuccessUrl, "logoutSuccessUrl cannot be null");
- this.serverLogoutSuccessHandler.setLogoutSuccessUrl(logoutSuccessUrl);
- }
}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpContext.java
b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpContext.java
new file mode 100644
index 0000000..1e0b3ab
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpContext.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.pac4j;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.commons.lang3.ArrayUtils;
+import org.pac4j.core.context.Cookie;
+import org.pac4j.core.context.WebContext;
+import org.pac4j.core.context.session.SessionStore;
+import org.pac4j.core.util.CommonHelper;
+import org.springframework.http.HttpCookie;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseCookie;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebSession;
+import org.springframework.web.util.UriComponentsBuilder;
+
+public class ServerHttpContext implements WebContext {
+
+ private final ServerWebExchange exchange;
+
+ private ServerHttpSessionStore sessionStore;
+
+ private MultiValueMap<String, String> form;
+
+ private String body;
+
+ /**
+ * Build a WebFlux context from the current exchange and web session.
+ *
+ * @param exchange the current exchange
+ * @param webSession the current web session
+ */
+ public ServerHttpContext(final ServerWebExchange exchange, final
WebSession webSession) {
+ this(exchange, new ServerHttpSessionStore(webSession));
+ }
+
+ /**
+ * Build a WebFlux context from the current exhange and from a session
store.
+ *
+ * @param exchange the current exchange
+ * @param sessionStore the session store to use
+ */
+ public ServerHttpContext(
+ final ServerWebExchange exchange,
+ final ServerHttpSessionStore sessionStore) {
+
+ CommonHelper.assertNotNull("exchange", exchange);
+ CommonHelper.assertNotNull("sessionStore", sessionStore);
+ this.exchange = exchange;
+ this.sessionStore = sessionStore;
+ }
+
+ public ServerHttpSessionStore getNativeSessionStore() {
+ return this.sessionStore;
+ }
+
+ @Override
+ public SessionStore<ServerHttpContext> getSessionStore() {
+ return this.sessionStore;
+ }
+
+ @Override
+ public Optional<String> getRequestAttribute(final String name) {
+ return Optional.ofNullable(exchange.getAttribute(name));
+ }
+
+ @Override
+ public void setRequestAttribute(final String name, final Object value) {
+ exchange.getAttributes().put(name, value);
+ }
+
+ @Override
+ public Optional<String> getRequestParameter(final String name) {
+ Map<String, String[]> params = getRequestParameters();
+ if (params.containsKey(name)) {
+ String[] values = params.get(name);
+ if (!ArrayUtils.isEmpty(values)) {
+ return Optional.of(values[0]);
+ }
+ }
+ return Optional.empty();
+ }
+
+ public ServerHttpContext setForm(final MultiValueMap<String, String> form)
{
+ this.form = form;
+ return this;
+ }
+
+ @Override
+ public Map<String, String[]> getRequestParameters() {
+ Map<String, String[]> params = new HashMap<>();
+
+ this.exchange.getRequest().getQueryParams().
+ forEach((key, value) -> params.put(key, new String[] {
value.toString() }));
+
+ if (this.form != null) {
+ form.forEach((key, values) -> params.put(key, values.toArray(new
String[0])));
+ }
+
+ return params;
+ }
+
+ @Override
+ public Optional<String> getRequestHeader(final String name) {
+ return
Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(name));
+ }
+
+ @Override
+ public String getRequestMethod() {
+ return this.exchange.getRequest().getMethodValue();
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return this.exchange.getRequest().getRemoteAddress().getHostString();
+ }
+
+ /**
+ * Return the native exchange.
+ *
+ * @return the native exchange
+ */
+ public ServerWebExchange getNative() {
+ return this.exchange;
+ }
+
+ @Override
+ public void setResponseHeader(final String name, final String value) {
+ this.exchange.getResponse().getHeaders().set(name, value);
+ }
+
+ @Override
+ public void setResponseContentType(final String content) {
+ this.exchange.getResponse().getHeaders().set(HttpHeaders.CONTENT_TYPE,
content);
+ }
+
+ @Override
+ public String getProtocol() {
+ return isSecure() ? "https" : "http";
+ }
+
+ @Override
+ public String getServerName() {
+ return
UriComponentsBuilder.fromHttpRequest(exchange.getRequest()).build().getHost();
+ }
+
+ @Override
+ public int getServerPort() {
+ return
UriComponentsBuilder.fromHttpRequest(exchange.getRequest()).build().getPort();
+ }
+
+ @Override
+ public String getScheme() {
+ return
UriComponentsBuilder.fromHttpRequest(exchange.getRequest()).build().getScheme();
+ }
+
+ @Override
+ public boolean isSecure() {
+ return this.exchange.getRequest().getSslInfo() != null;
+ }
+
+ @Override
+ public String getFullRequestURL() {
+ return
UriComponentsBuilder.fromHttpRequest(exchange.getRequest()).build().toUriString();
+ }
+
+ @Override
+ public Collection<Cookie> getRequestCookies() {
+ MultiValueMap<String, HttpCookie> cookies =
this.exchange.getRequest().getCookies();
+
+ Collection<Cookie> pac4jCookies = new LinkedHashSet<>();
+ cookies.toSingleValueMap().values().forEach(c -> {
+ Cookie cookie = new Cookie(c.getName(), c.getValue());
+ pac4jCookies.add(cookie);
+ });
+ return pac4jCookies;
+ }
+
+ @Override
+ public void addResponseCookie(final Cookie cookie) {
+ ResponseCookie.ResponseCookieBuilder c =
ResponseCookie.from(cookie.getName(), cookie.getValue());
+ c.secure(cookie.isSecure());
+ c.path(cookie.getPath());
+ c.maxAge(cookie.getMaxAge());
+ c.httpOnly(cookie.isHttpOnly());
+ c.domain(cookie.getDomain());
+ this.exchange.getResponse().addCookie(c.build());
+ }
+
+ @Override
+ public String getPath() {
+ return exchange.getRequest().getPath().value();
+ }
+
+ public ServerHttpContext setBody(final String body) {
+ this.body = body;
+ return this;
+ }
+
+ @Override
+ public String getRequestContent() {
+ return body;
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpSessionStore.java
b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpSessionStore.java
new file mode 100644
index 0000000..622a92c
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpSessionStore.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.pac4j;
+
+import java.util.Optional;
+import org.pac4j.core.context.session.SessionStore;
+import org.springframework.web.server.WebSession;
+
+public class ServerHttpSessionStore implements SessionStore<ServerHttpContext>
{
+
+ private final WebSession webSession;
+
+ public ServerHttpSessionStore(final WebSession webSession) {
+ this.webSession = webSession;
+ }
+
+ @Override
+ public String getOrCreateSessionId(final ServerHttpContext context) {
+ return this.webSession.getId();
+ }
+
+ @Override
+ public Optional<Object> get(final ServerHttpContext context, final String
key) {
+ return Optional.ofNullable(this.webSession.getAttribute(key));
+ }
+
+ @Override
+ public void set(final ServerHttpContext context, final String key, final
Object value) {
+ }
+
+ @Override
+ public boolean destroySession(final ServerHttpContext context) {
+ return false;
+ }
+
+ @Override
+ public Optional<WebSession> getTrackableSession(final ServerHttpContext
context) {
+ return Optional.ofNullable(this.webSession);
+ }
+
+ @Override
+ public Optional<SessionStore<ServerHttpContext>> buildFromTrackableSession(
+ final ServerHttpContext context, final Object trackableSession) {
+
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean renewSession(final ServerHttpContext context) {
+ return false;
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AnonymousWebFilter.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AnonymousWebFilter.java
new file mode 100644
index 0000000..29d0fa5
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AnonymousWebFilter.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import java.net.URI;
+import org.apache.syncope.sra.security.PublicRouteMatcher;
+import org.springframework.http.HttpStatus;
+import
org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+public class SAML2AnonymousWebFilter implements WebFilter {
+
+ public static final String INITIAL_REQUEST_URI = "INITIAL_REQUEST_URI";
+
+ private final PublicRouteMatcher publicRouteMatcher;
+
+ public SAML2AnonymousWebFilter(final PublicRouteMatcher
publicRouteMatcher) {
+ this.publicRouteMatcher = publicRouteMatcher;
+ }
+
+ @Override
+ public Mono<Void> filter(final ServerWebExchange exchange, final
WebFilterChain chain) {
+ return publicRouteMatcher.matches(exchange).
+ filter(matchResult -> !matchResult.isMatch()).
+ flatMap(r -> exchange.getSession()).flatMap(r ->
exchange.getSession()).
+ filter(s -> !s.getAttributes().containsKey(
+
WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME)).
+ switchIfEmpty(chain.filter(exchange).then(Mono.empty())).
+ flatMap(session -> {
+ session.getAttributes().put(INITIAL_REQUEST_URI,
exchange.getRequest().getURI());
+
+ exchange.getResponse().setStatusCode(HttpStatus.SEE_OTHER);
+ exchange.getResponse().getHeaders().
+
setLocation(URI.create(SAML2WebSsoAuthenticationRequestWebFilter.AUTHENTICATE_URL));
+ return exchange.getResponse().setComplete();
+ });
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AuthenticationToken.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AuthenticationToken.java
new file mode 100644
index 0000000..74e712e
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AuthenticationToken.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.pac4j.saml.credentials.SAML2Credentials;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+public class SAML2AuthenticationToken extends AbstractAuthenticationToken {
+
+ private static final long serialVersionUID = 8322987617416135717L;
+
+ private final SAML2Credentials credentials;
+
+ public SAML2AuthenticationToken(final SAML2Credentials credentials) {
+ super(credentials.getUserProfile().getRoles().stream().
+ map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
+ this.credentials = credentials;
+ this.setAuthenticated(true);
+ }
+
+ @Override
+ public Object getCredentials() {
+ return StringUtils.EMPTY;
+ }
+
+ @Override
+ public SAML2Credentials getPrincipal() {
+ return credentials;
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2BindingType.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2BindingType.java
new file mode 100644
index 0000000..bf7895d
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2BindingType.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import org.opensaml.saml.common.xml.SAMLConstants;
+
+public enum SAML2BindingType {
+ POST(SAMLConstants.SAML2_POST_BINDING_URI),
+ REDIRECT(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
+
+ private final String uri;
+
+ SAML2BindingType(final String uri) {
+ this.uri = uri;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2MetadataEndpoint.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2MetadataEndpoint.java
new file mode 100644
index 0000000..229d232
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2MetadataEndpoint.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import com.google.common.net.HttpHeaders;
+import org.apache.syncope.sra.SecurityConfig;
+import org.pac4j.saml.client.SAML2Client;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping(SAML2MetadataEndpoint.METADATA_URL)
+@ConditionalOnProperty(name = SecurityConfig.AM_TYPE, havingValue = "SAML2")
+public class SAML2MetadataEndpoint {
+
+ public static final String METADATA_URL = "/saml2/metadata";
+
+ private final SAML2Client saml2Client;
+
+ public SAML2MetadataEndpoint(final SAML2Client saml2Client) {
+ this.saml2Client = saml2Client;
+ }
+
+ @GetMapping(produces = { MediaType.APPLICATION_XML_VALUE })
+ @ResponseBody
+ public Mono<ResponseEntity<String>> metadata(final ServerHttpRequest
request) {
+ return Mono.just(ResponseEntity.ok().
+ header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE).
+
body(saml2Client.getServiceProviderMetadataResolver().getMetadata()));
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java
new file mode 100644
index 0000000..fd2a924
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import org.apache.syncope.sra.security.pac4j.ServerHttpContext;
+import java.net.URI;
+import org.pac4j.core.exception.http.RedirectionAction;
+import org.pac4j.core.exception.http.WithContentAction;
+import org.pac4j.core.exception.http.WithLocationAction;
+import org.pac4j.saml.client.SAML2Client;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import reactor.core.publisher.Mono;
+
+abstract class SAML2RequestGenerator {
+
+ protected final SAML2Client saml2Client;
+
+ protected SAML2RequestGenerator(final SAML2Client saml2Client) {
+ this.saml2Client = saml2Client;
+ }
+
+ protected Mono<Void> handle(
+ final RedirectionAction action,
+ final ServerHttpContext shc) {
+
+ if (action instanceof WithLocationAction) {
+ WithLocationAction withLocationAction = (WithLocationAction)
action;
+ shc.getNative().getResponse().setStatusCode(HttpStatus.FOUND);
+
shc.getNative().getResponse().getHeaders().setLocation(URI.create(withLocationAction.getLocation()));
+ return shc.getNative().getResponse().setComplete();
+ } else if (action instanceof WithContentAction) {
+ WithContentAction withContentAction = (WithContentAction) action;
+ String content = withContentAction.getContent();
+
+ if (content == null) {
+ throw new IllegalArgumentException("No content set for POST
AuthnRequest");
+ }
+
+ return Mono.defer(() -> {
+
shc.getNative().getResponse().getHeaders().setContentType(MediaType.TEXT_HTML);
+ return shc.getNative().getResponse().
+
writeWith(Mono.just(shc.getNative().getResponse().bufferFactory().wrap(content.getBytes())));
+ });
+ } else {
+ throw new IllegalArgumentException("Unsupported Action: " +
action.getClass().getName());
+ }
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java
new file mode 100644
index 0000000..4b71109
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import org.apache.syncope.sra.ApplicationContextUtils;
+import org.apache.syncope.sra.security.LogoutRouteMatcher;
+import org.apache.syncope.sra.security.PublicRouteMatcher;
+import org.pac4j.saml.client.SAML2Client;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import
org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.Authentication;
+import
org.springframework.security.web.server.authentication.AuthenticationWebFilter;
+import
org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
+import
org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
+import org.springframework.web.server.WebFilter;
+import reactor.core.publisher.Mono;
+
+public final class SAML2SecurityConfigUtils {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SAML2SecurityConfigUtils.class);
+
+ private static ReactiveAuthenticationManager authenticationManager() {
+ return authentication -> Mono.just(authentication).
+ filter(Authentication::isAuthenticated);
+ }
+
+ public static void forLogin(
+ final ServerHttpSecurity http,
+ final SAML2Client saml2Client,
+ final PublicRouteMatcher publicRouteMatcher) {
+
+ ReactiveAuthenticationManager authenticationManager =
authenticationManager();
+
+ SAML2WebSsoAuthenticationRequestWebFilter authRequestFilter =
+ new SAML2WebSsoAuthenticationRequestWebFilter(saml2Client);
+ http.addFilterAt(authRequestFilter,
SecurityWebFiltersOrder.HTTP_BASIC);
+
+ AuthenticationWebFilter authenticationFilter =
+ new SAML2WebSsoAuthenticationWebFilter(authenticationManager,
saml2Client);
+ authenticationFilter.setAuthenticationFailureHandler((exchange, ex) ->
Mono.error(ex));
+ authenticationFilter.setSecurityContextRepository(new
WebSessionServerSecurityContextRepository());
+ http.addFilterAt(authenticationFilter,
SecurityWebFiltersOrder.AUTHENTICATION);
+
+ WebFilter anonymousRedirectFilter = new
SAML2AnonymousWebFilter(publicRouteMatcher);
+ http.addFilterAt(anonymousRedirectFilter,
SecurityWebFiltersOrder.AUTHENTICATION);
+ }
+
+ public static void forLogout(
+ final ServerHttpSecurity.AuthorizeExchangeSpec builder,
+ final SAML2Client saml2Client,
+ final LogoutRouteMatcher logoutRouteMatcher,
+ final ConfigurableApplicationContext ctx) {
+
+ LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
+ logoutWebFilter.setRequiresLogoutMatcher(logoutRouteMatcher);
+
+ SAML2ServerLogoutHandler logoutHandler = new
SAML2ServerLogoutHandler(saml2Client);
+ logoutWebFilter.setLogoutHandler(logoutHandler);
+
+ try {
+ SAML2ServerLogoutSuccessHandler handler =
ApplicationContextUtils.getOrCreateBean(ctx,
+ SAML2ServerLogoutSuccessHandler.class.getName(),
+ SAML2ServerLogoutSuccessHandler.class);
+ logoutWebFilter.setLogoutSuccessHandler(handler);
+ } catch (ClassNotFoundException e) {
+ LOG.error("While creating instance of {}",
+ SAML2ServerLogoutSuccessHandler.class.getName(), e);
+ }
+
+ builder.and().addFilterAt(logoutWebFilter,
SecurityWebFiltersOrder.LOGOUT);
+ }
+
+ private SAML2SecurityConfigUtils() {
+ // private constructor for static utility class
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java
new file mode 100644
index 0000000..0bcba31
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import org.apache.syncope.sra.security.pac4j.ServerHttpContext;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.credentials.SAML2Credentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.WebFilterExchange;
+import
org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
+import reactor.core.publisher.Mono;
+
+public class SAML2ServerLogoutHandler extends SAML2RequestGenerator implements
ServerLogoutHandler {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SAML2ServerLogoutHandler.class);
+
+ public SAML2ServerLogoutHandler(final SAML2Client saml2Client) {
+ super(saml2Client);
+ }
+
+ @Override
+ public Mono<Void> logout(final WebFilterExchange exchange, final
Authentication authentication) {
+ return exchange.getExchange().getSession().
+ flatMap(session -> {
+ SAML2Credentials credentials = (SAML2Credentials)
authentication.getPrincipal();
+
+ LOG.debug("Creating SAML2 SP Logout Request for IDP[{}]
and Profile[{}]",
+ saml2Client.getIdentityProviderResolvedEntityId(),
credentials.getUserProfile());
+
+ ServerHttpContext shc = new
ServerHttpContext(exchange.getExchange(), session);
+
+ return session.invalidate().then(
+ saml2Client.getLogoutAction(shc,
credentials.getUserProfile(), null).
+ map(action -> handle(action, shc)).
+ orElseThrow(() -> new
IllegalStateException("No action generated")));
+ }).onErrorResume(Mono::error);
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java
new file mode 100644
index 0000000..3557eb5
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import org.apache.syncope.sra.security.AbstractServerLogoutSuccessHandler;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.WebFilterExchange;
+import reactor.core.publisher.Mono;
+
+public class SAML2ServerLogoutSuccessHandler extends
AbstractServerLogoutSuccessHandler {
+
+ @Override
+ public Mono<Void> onLogoutSuccess(final WebFilterExchange exchange, final
Authentication authentication) {
+ return Mono.just(authentication).
+ flatMap(auth ->
redirectStrategy.sendRedirect(exchange.getExchange(), getPostLogout(exchange)));
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java
new file mode 100644
index 0000000..e57faad
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import org.apache.syncope.sra.security.pac4j.ServerHttpContext;
+import org.pac4j.saml.client.SAML2Client;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+public class SAML2WebSsoAuthenticationRequestWebFilter extends
SAML2RequestGenerator implements WebFilter {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(SAML2WebSsoAuthenticationRequestWebFilter.class);
+
+ public static final String AUTHENTICATE_URL = "/saml2/authenticate";
+
+ private ServerWebExchangeMatcher redirectMatcher =
ServerWebExchangeMatchers.pathMatchers(AUTHENTICATE_URL);
+
+ public SAML2WebSsoAuthenticationRequestWebFilter(final SAML2Client
saml2Client) {
+ super(saml2Client);
+ }
+
+ public void setRedirectMatcher(final ServerWebExchangeMatcher
redirectMatcher) {
+ Assert.notNull(redirectMatcher, "redirectMatcher cannot be null");
+ this.redirectMatcher = redirectMatcher;
+ }
+
+ @Override
+ public Mono<Void> filter(final ServerWebExchange exchange, final
WebFilterChain chain) {
+ return redirectMatcher.matches(exchange).
+ filter(matchResult -> matchResult.isMatch()).
+ switchIfEmpty(chain.filter(exchange).then(Mono.empty())).
+ flatMap(matchResult -> exchange.getSession()).
+ flatMap(session -> {
+ LOG.debug("Creating SAML2 SP Authentication Request for
IDP[{}]",
+ saml2Client.getIdentityProviderResolvedEntityId());
+
+ ServerHttpContext shc = new ServerHttpContext(exchange,
session);
+
+ return saml2Client.getRedirectionAction(shc).
+ map(action -> handle(action, shc)).
+ orElseThrow(() -> new IllegalStateException("No
action generated"));
+ }).onErrorResume(Mono::error);
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java
new file mode 100644
index 0000000..79ef94b
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.saml2;
+
+import org.apache.syncope.sra.security.pac4j.ServerHttpContext;
+import java.net.URI;
+import
org.apache.syncope.sra.security.web.server.DoNothingIfCommittedServerRedirectStrategy;
+import org.pac4j.core.util.Pac4jConstants;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.credentials.SAML2Credentials;
+import
org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.ServerRedirectStrategy;
+import org.springframework.security.web.server.WebFilterExchange;
+import
org.springframework.security.web.server.authentication.AuthenticationWebFilter;
+import
org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
+import
org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
+import
org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
+import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+public class SAML2WebSsoAuthenticationWebFilter extends
AuthenticationWebFilter {
+
+ public static final String DEFAULT_FILTER_PROCESSES_URI =
"/login/saml2/sso";
+
+ private final SAML2Client saml2Client;
+
+ private ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
+
ServerWebExchangeMatchers.pathMatchers(DEFAULT_FILTER_PROCESSES_URI),
+ exchange -> exchange.getRequest().getQueryParams().
+ containsKey(Pac4jConstants.LOGOUT_ENDPOINT_PARAMETER)
+ ? ServerWebExchangeMatcher.MatchResult.notMatch()
+ : ServerWebExchangeMatcher.MatchResult.match());
+
+ public SAML2WebSsoAuthenticationWebFilter(
+ final ReactiveAuthenticationManager authenticationManager,
+ final SAML2Client saml2Client) {
+
+ super(authenticationManager);
+
+ this.saml2Client = saml2Client;
+
+ setRequiresAuthenticationMatcher(matchSamlResponse());
+
+ setServerAuthenticationConverter(convertSamlResponse());
+
+ setAuthenticationSuccessHandler(redirectToInitialRequestURI());
+ }
+
+ public void setMatcher(final ServerWebExchangeMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ @Override
+ public Mono<Void> filter(final ServerWebExchange exchange, final
WebFilterChain chain) {
+ return super.filter(exchange,
chain).then(Mono.defer(exchange.getResponse()::setComplete));
+ }
+
+ private ServerWebExchangeMatcher matchSamlResponse() {
+ return exchange -> exchange.getFormData().
+ filter(form -> form.containsKey("SAMLResponse")).
+ flatMap(form -> ServerWebExchangeMatcher.MatchResult.match()).
+ switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
+ }
+
+ private ServerAuthenticationConverter convertSamlResponse() {
+ return exchange -> exchange.getFormData().
+ flatMap(form -> this.matcher.matches(exchange).
+ flatMap(matchResult -> exchange.getSession()).
+ flatMap(session -> {
+ ServerHttpContext shc = new ServerHttpContext(exchange,
session).setForm(form);
+
+ SAML2Credentials credentials =
saml2Client.getCredentialsExtractor().extract(shc).
+ orElseThrow(() -> new IllegalStateException("No
AuthnResponse found"));
+
+ saml2Client.getAuthenticator().validate(credentials, shc);
+
+ return Mono.just(new
SAML2AuthenticationToken(credentials));
+ }));
+ }
+
+ private ServerAuthenticationSuccessHandler redirectToInitialRequestURI() {
+ return new ServerAuthenticationSuccessHandler() {
+
+ private final ServerRedirectStrategy redirectStrategy = new
DoNothingIfCommittedServerRedirectStrategy();
+
+ @Override
+ public Mono<Void> onAuthenticationSuccess(
+ final WebFilterExchange webFilterExchange, final
Authentication authentication) {
+
+ return webFilterExchange.getExchange().getSession().
+ flatMap(session -> this.redirectStrategy.sendRedirect(
+ webFilterExchange.getExchange(),
+ (URI)
session.getRequiredAttribute(SAML2AnonymousWebFilter.INITIAL_REQUEST_URI)));
+ }
+ };
+ }
+}
diff --git
a/sra/src/main/java/org/apache/syncope/sra/security/web/server/DoNothingIfCommittedServerRedirectStrategy.java
b/sra/src/main/java/org/apache/syncope/sra/security/web/server/DoNothingIfCommittedServerRedirectStrategy.java
new file mode 100644
index 0000000..c78dc15
--- /dev/null
+++
b/sra/src/main/java/org/apache/syncope/sra/security/web/server/DoNothingIfCommittedServerRedirectStrategy.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra.security.web.server;
+
+import java.net.URI;
+import org.springframework.security.web.server.DefaultServerRedirectStrategy;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+public class DoNothingIfCommittedServerRedirectStrategy extends
DefaultServerRedirectStrategy {
+
+ @Override
+ public Mono<Void> sendRedirect(final ServerWebExchange exchange, final URI
location) {
+ return exchange.getResponse().isCommitted()
+ ? Mono.empty()
+ : super.sendRedirect(exchange, location);
+ }
+}
diff --git a/sra/src/test/resources/debug/application-debug.properties
b/sra/src/test/resources/debug/application-debug.properties
index 5958ee8..5af528f 100644
--- a/sra/src/test/resources/debug/application-debug.properties
+++ b/sra/src/test/resources/debug/application-debug.properties
@@ -19,15 +19,27 @@
#am.oidc.client.id=oidcTestClientId
#am.oidc.client.secret=oidcTestClientSecret
-am.type=OAUTH2
-am.oauth2.tokenUri=http://localhost:9080/syncope-wa/oauth2.0/accessToken
-am.oauth2.authorizationUri=http://localhost:9080/syncope-wa/oauth2.0/authorize
-am.oauth2.userInfoUri=http://localhost:9080/syncope-wa/oauth2.0/profile
-am.oauth2.userNameAttributeName=id
-am.oauth2.scopes=
-am.oauth2.jwkSetUri=
-am.oauth2.issuer=http://localhost:9080/syncope-wa
-am.oauth2.client.id=oauth2TestClientId
-am.oauth2.client.secret=oauth2TestClientSecret
+#am.type=OAUTH2
+#am.oauth2.tokenUri=http://localhost:9080/syncope-wa/oauth2.0/accessToken
+#am.oauth2.authorizationUri=http://localhost:9080/syncope-wa/oauth2.0/authorize
+#am.oauth2.userInfoUri=http://localhost:9080/syncope-wa/oauth2.0/profile
+#am.oauth2.userNameAttributeName=id
+#am.oauth2.scopes=
+#am.oauth2.jwkSetUri=
+#am.oauth2.issuer=http://localhost:9080/syncope-wa
+#am.oauth2.client.id=oauth2TestClientId
+#am.oauth2.client.secret=oauth2TestClientSecret
+
+am.type=SAML2
+am.saml2.sp.authnrequest.binding=POST
+am.saml2.sp.logout.request.binding=POST
+am.saml2.sp.logout.response.binding=POST
+am.saml2.sp.entityId=http://localhost:8080
+am.saml2.sp.skew=300
+am.saml2.idp=http://localhost:9080/syncope-wa/idp/metadata
+am.saml2.keystore=classpath:/saml.keystore.jks
+am.saml2.keystore.type=jks
+am.saml2.keystore.storepass=changeit
+am.saml2.keystore.keypass=changeit
global.postLogout=http://localhost:8080/logout
diff --git a/sra/src/test/resources/debug/saml.keystore.jks
b/sra/src/test/resources/debug/saml.keystore.jks
new file mode 100644
index 0000000..e2a5238
Binary files /dev/null and b/sra/src/test/resources/debug/saml.keystore.jks
differ
diff --git a/wa/pom.xml b/wa/pom.xml
index b0c22c7..0dd676a 100644
--- a/wa/pom.xml
+++ b/wa/pom.xml
@@ -33,8 +33,6 @@ under the License.
<packaging>pom</packaging>
<properties>
- <opensaml.version>4.0.0</opensaml.version>
-
<rootpom.basedir>${basedir}/..</rootpom.basedir>
</properties>
@@ -77,13 +75,6 @@ under the License.
</profile>
</profiles>
- <repositories>
- <repository>
- <id>shibboleth</id>
- <url>https://build.shibboleth.net/nexus/content/groups/public</url>
- </repository>
- </repositories>
-
<modules>
<module>bootstrap</module>
<module>starter</module>
diff --git
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
index 9719676..6323bf0 100644
---
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
+++
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
@@ -37,8 +37,6 @@ public class RestfulSamlIdPMetadataGenerator extends
BaseSamlIdPMetadataGenerato
private static final Logger LOG =
LoggerFactory.getLogger(RestfulSamlIdPMetadataGenerator.class);
- public static final String DEFAULT_APPLIES_FOR = "Syncope";
-
private final WARestClient restClient;
public RestfulSamlIdPMetadataGenerator(
@@ -55,7 +53,7 @@ public class RestfulSamlIdPMetadataGenerator extends
BaseSamlIdPMetadataGenerato
final Optional<SamlRegisteredService> registeredService) {
LOG.info("Generating new SAML2 IdP metadata document");
- doc.setAppliesTo(DEFAULT_APPLIES_FOR);
+ doc.setAppliesTo(WASAML2IdPMetadataService.DEFAULT_OWNER);
SAML2IdPMetadataTO metadataTO = new SAML2IdPMetadataTO.Builder().
metadata(doc.getMetadata()).
encryptionKey(doc.getEncryptionKey()).
@@ -69,8 +67,8 @@ public class RestfulSamlIdPMetadataGenerator extends
BaseSamlIdPMetadataGenerato
Response response = null;
try {
response =
client.getService(WASAML2IdPMetadataService.class).set(metadataTO);
- } catch (Exception ex) {
- LOG.warn("While generating SAML2 IdP metadata document", ex);
+ } catch (Exception e) {
+ LOG.warn("While generating SAML2 IdP metadata document", e);
}
return response != null &&
HttpStatus.valueOf(response.getStatus()).is2xxSuccessful() ? doc : null;
@@ -93,5 +91,4 @@ public class RestfulSamlIdPMetadataGenerator extends
BaseSamlIdPMetadataGenerato
}
return restClient.getSyncopeClient();
}
-
}
diff --git
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
index f00bba5..0e12872 100644
---
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
+++
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
@@ -46,24 +46,13 @@ public class RestfulSamlIdPMetadataLocator extends
AbstractSamlIdPMetadataLocato
this.restClient = restClient;
}
- private static String getAppliesToFor(final
Optional<SamlRegisteredService> result) {
- if (result.isPresent()) {
- SamlRegisteredService registeredService = result.get();
- return registeredService.getName() + '-' +
registeredService.getId();
- }
- return RestfulSamlIdPMetadataGenerator.DEFAULT_APPLIES_FOR;
- }
-
@Override
public SamlIdPMetadataDocument fetchInternal(final
Optional<SamlRegisteredService> registeredService) {
try {
LOG.info("Locating SAML2 IdP metadata document");
- SAML2IdPMetadataTO saml2IdPMetadataTO =
getSyncopeClient().getService(WASAML2IdPMetadataService.class).
- getByOwner(getAppliesToFor(registeredService));
- if (saml2IdPMetadataTO == null) {
- LOG.warn("No SAML2 IdP metadata document obtained from core");
- } else {
+ SAML2IdPMetadataTO saml2IdPMetadataTO =
fetchFromCore(registeredService);
+ if (saml2IdPMetadataTO != null) {
SamlIdPMetadataDocument document = new
SamlIdPMetadataDocument();
document.setMetadata(saml2IdPMetadataTO.getMetadata());
document.setEncryptionCertificate(saml2IdPMetadataTO.getEncryptionCertificate());
@@ -75,18 +64,36 @@ public class RestfulSamlIdPMetadataLocator extends
AbstractSamlIdPMetadataLocato
LOG.debug("Found SAML2 IdP metadata document: {}",
document.getId());
return document;
}
- LOG.warn("Not a valid SAML2 IdP metadata document");
}
+ LOG.warn("Not a valid SAML2 IdP metadata document");
return null;
- } catch (SyncopeClientException ex) {
- if (ex.getType() == ClientExceptionType.NotFound) {
- LOG.debug("No SAML2 IdP metadata document is available");
- }
+ } catch (SyncopeClientException e) {
+ LOG.error("While fetching SAML2 IdP metadata", e);
}
+
return null;
}
+ private SAML2IdPMetadataTO fetchFromCore(final
Optional<SamlRegisteredService> registeredService) {
+ SAML2IdPMetadataTO result = null;
+
+ String appliesToFor =
registeredService.map(SamlRegisteredService::getName).
+ orElse(WASAML2IdPMetadataService.DEFAULT_OWNER);
+ try {
+ result =
getSyncopeClient().getService(WASAML2IdPMetadataService.class).getByOwner(appliesToFor);
+ } catch (SyncopeClientException e) {
+ if (e.getType() == ClientExceptionType.NotFound &&
registeredService.isPresent()) {
+ result =
getSyncopeClient().getService(WASAML2IdPMetadataService.class).
+ getByOwner(WASAML2IdPMetadataService.DEFAULT_OWNER);
+ } else {
+ throw e;
+ }
+ }
+
+ return result;
+ }
+
private SyncopeClient getSyncopeClient() {
if (!WARestClient.isReady()) {
LOG.info("Syncope client is not yet ready");
diff --git a/wa/starter/src/main/resources/wa.properties
b/wa/starter/src/main/resources/wa.properties
index eb69a87..01534fd 100644
--- a/wa/starter/src/main/resources/wa.properties
+++ b/wa/starter/src/main/resources/wa.properties
@@ -27,9 +27,13 @@ cas.server.name=http://localhost:8080
cas.server.prefix=${cas.server.name}/syncope-wa
cas.server.scope=syncope.org
+cas.tgc.secure=false
cas.logout.follow-service-redirects=true
-cas.authn.saml-idp.entity-id=https://syncope.apache.org/saml
+cas.authn.saml-idp.entity-id=http://localhost:8080/saml
+cas.authn.saml-idp.metadata.metadata-backup-location=file:${conf.directory}/saml
+
+cas.authn.oidc.issuer=http://localhost:8080/syncope-wa/oidc/
# Disable access to the login endpoint
# if no target application is specified.
diff --git a/wa/starter/src/test/resources/debug/wa.properties
b/wa/starter/src/test/resources/debug/wa.properties
index c44903d..29f53c3 100644
--- a/wa/starter/src/test/resources/debug/wa.properties
+++ b/wa/starter/src/test/resources/debug/wa.properties
@@ -31,7 +31,8 @@ cas.authn.syncope.url=http://localhost:9080/syncope/rest/
cas.tgc.secure=false
cas.logout.follow-service-redirects=true
-cas.authn.saml-idp.entity-id=https://syncope.apache.org/saml
+cas.authn.saml-idp.entity-id=http://localhost:8080/saml
+cas.authn.saml-idp.metadata.metadata-backup-location=file:${conf.directory}/saml
cas.authn.oidc.issuer=http://localhost:8080/syncope-wa/oidc/