View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.fit.ui;
20  
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  import static org.junit.jupiter.api.Assertions.fail;
25  
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.util.ArrayList;
29  import java.util.List;
30  import javax.ws.rs.core.MediaType;
31  import javax.ws.rs.core.Response;
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.commons.lang3.tuple.Triple;
34  import org.apache.cxf.jaxrs.client.WebClient;
35  import org.apache.http.Consts;
36  import org.apache.http.HttpHeaders;
37  import org.apache.http.HttpStatus;
38  import org.apache.http.NameValuePair;
39  import org.apache.http.client.entity.UrlEncodedFormEntity;
40  import org.apache.http.client.methods.CloseableHttpResponse;
41  import org.apache.http.client.methods.HttpGet;
42  import org.apache.http.client.methods.HttpPost;
43  import org.apache.http.client.protocol.HttpClientContext;
44  import org.apache.http.impl.client.BasicCookieStore;
45  import org.apache.http.impl.client.CloseableHttpClient;
46  import org.apache.http.impl.client.HttpClients;
47  import org.apache.http.message.BasicNameValuePair;
48  import org.apache.http.util.EntityUtils;
49  import org.apache.syncope.client.ui.commons.SAML2SP4UIConstants;
50  import org.apache.syncope.common.lib.SyncopeClientException;
51  import org.apache.syncope.common.lib.SyncopeConstants;
52  import org.apache.syncope.common.lib.to.Item;
53  import org.apache.syncope.common.lib.to.SAML2SP4UIIdPTO;
54  import org.apache.syncope.common.lib.to.SAML2SPClientAppTO;
55  import org.apache.syncope.common.lib.types.ClientAppType;
56  import org.apache.syncope.common.lib.types.SAML2SPNameId;
57  import org.apache.syncope.common.rest.api.RESTHeaders;
58  import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
59  import org.junit.jupiter.api.BeforeAll;
60  import org.junit.jupiter.api.Test;
61  
62  public class SAML2SP4UIITCase extends AbstractUIITCase {
63  
64      private static void clientAppSetup(final String appName, final String entityId, final long appId) {
65          SAML2SPClientAppTO clientApp = CLIENT_APP_SERVICE.list(ClientAppType.SAML2SP).stream().
66                  filter(app -> appName.equals(app.getName())).
67                  map(SAML2SPClientAppTO.class::cast).
68                  findFirst().
69                  orElseGet(() -> {
70                      SAML2SPClientAppTO app = new SAML2SPClientAppTO();
71                      app.setName(appName);
72                      app.setRealm(SyncopeConstants.ROOT_REALM);
73                      app.setClientAppId(appId);
74                      app.setEntityId(entityId);
75                      app.setMetadataLocation(entityId + SAML2SP4UIConstants.URL_CONTEXT + "/metadata");
76  
77                      Response response = CLIENT_APP_SERVICE.create(ClientAppType.SAML2SP, app);
78                      if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
79                          fail("Could not create SAML2 Client App");
80                      }
81  
82                      return CLIENT_APP_SERVICE.read(
83                              ClientAppType.SAML2SP, response.getHeaderString(RESTHeaders.RESOURCE_KEY));
84                  });
85  
86          clientApp.setSignAssertions(true);
87          clientApp.setSignResponses(true);
88          clientApp.setRequiredNameIdFormat(SAML2SPNameId.PERSISTENT);
89          clientApp.setAuthPolicy(getAuthPolicy().getKey());
90          clientApp.setAttrReleasePolicy(getAttrReleasePolicy().getKey());
91  
92          CLIENT_APP_SERVICE.update(ClientAppType.SAML2SP, clientApp);
93          WA_CONFIG_SERVICE.pushToWA(WAConfigService.PushSubject.clientApps, List.of());
94      }
95  
96      @BeforeAll
97      public static void consoleClientAppSetup() {
98          clientAppSetup(SAML2SP4UIITCase.class.getName() + "_Console", CONSOLE_ADDRESS, 5L);
99      }
100 
101     @BeforeAll
102     public static void enduserClientAppSetup() {
103         clientAppSetup(SAML2SP4UIITCase.class.getName() + "_Enduser", ENDUSER_ADDRESS, 6L);
104     }
105 
106     @BeforeAll
107     public static void idpSetup() {
108         WebClient.client(SAML2SP4UI_IDP_SERVICE).
109                 accept(MediaType.APPLICATION_XML_TYPE).
110                 type(MediaType.APPLICATION_XML_TYPE);
111         try {
112             SAML2SP4UI_IDP_SERVICE.importFromMetadata(
113                     (InputStream) WebClient.create(WA_ADDRESS + "/idp/metadata").get().getEntity());
114         } catch (SyncopeClientException e) {
115             // nothing bad if already imported
116         } finally {
117             WebClient.client(SAML2SP4UI_IDP_SERVICE).
118                     accept(CLIENT_FACTORY.getContentType().getMediaType()).
119                     type(CLIENT_FACTORY.getContentType().getMediaType());
120         }
121 
122         List<SAML2SP4UIIdPTO> idps = SAML2SP4UI_IDP_SERVICE.list();
123         assertEquals(1, idps.size());
124 
125         SAML2SP4UIIdPTO cas = idps.get(0);
126         cas.setEntityID(WA_ADDRESS + "/saml");
127         cas.setName("CAS");
128         cas.setCreateUnmatching(true);
129         cas.setSelfRegUnmatching(false);
130 
131         cas.getItems().clear();
132 
133         Item item = new Item();
134         item.setIntAttrName("username");
135         item.setExtAttrName("NameID");
136         item.setConnObjectKey(true);
137         cas.setConnObjectKeyItem(item);
138 
139         item = new Item();
140         item.setIntAttrName("email");
141         item.setExtAttrName("email");
142         cas.add(item);
143 
144         item = new Item();
145         item.setIntAttrName("userId");
146         item.setExtAttrName("email");
147         cas.add(item);
148 
149         item = new Item();
150         item.setIntAttrName("firstname");
151         item.setExtAttrName("given_name");
152         cas.add(item);
153 
154         item = new Item();
155         item.setIntAttrName("surname");
156         item.setExtAttrName("family_name");
157         cas.add(item);
158 
159         item = new Item();
160         item.setIntAttrName("fullname");
161         item.setExtAttrName("name");
162         cas.add(item);
163 
164         SAML2SP4UI_IDP_SERVICE.update(cas);
165     }
166 
167     @Test
168     public void fetchSpMetadata() throws Exception {
169         try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
170             HttpClientContext context = HttpClientContext.create();
171             context.setCookieStore(new BasicCookieStore());
172 
173             HttpGet get = new HttpGet(WA_ADDRESS + "/sp/metadata");
174             CloseableHttpResponse response = httpclient.execute(get, context);
175             assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
176             String responseBody = EntityUtils.toString(response.getEntity());
177             assertFalse(responseBody.isEmpty());
178         }
179     }
180 
181     @Override
182     protected void sso(final String baseURL, final String username, final String password) throws IOException {
183         CloseableHttpClient httpclient = HttpClients.createDefault();
184         HttpClientContext context = HttpClientContext.create();
185         context.setCookieStore(new BasicCookieStore());
186 
187         // 1. fetch login page
188         HttpGet get = new HttpGet(baseURL);
189         try (CloseableHttpResponse response = httpclient.execute(get, context)) {
190             assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
191         }
192 
193         // 2. click on the SAML 2.0 IdP
194         get = new HttpGet(baseURL + SAML2SP4UIConstants.URL_CONTEXT
195                 + "/login?idp=https%3A//localhost%3A9443/syncope-wa/saml");
196         String responseBody;
197         try (CloseableHttpResponse response = httpclient.execute(get, context)) {
198             responseBody = EntityUtils.toString(response.getEntity());
199         }
200         Triple<String, String, String> parsed = parseSAMLRequestForm(responseBody);
201 
202         // 2a. post SAML request
203         HttpPost post = new HttpPost(parsed.getLeft());
204         post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
205         post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
206         post.setEntity(new UrlEncodedFormEntity(
207                 List.of(new BasicNameValuePair("RelayState", parsed.getMiddle()),
208                         new BasicNameValuePair("SAMLRequest", parsed.getRight())), Consts.UTF_8));
209         String location;
210         try (CloseableHttpResponse response = httpclient.execute(post, context)) {
211             assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode());
212             location = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
213         }
214 
215         // 2b. authenticate
216         post = new HttpPost(location);
217         post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
218         post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
219         post.addHeader(HttpHeaders.REFERER, get.getURI().toASCIIString());
220         try (CloseableHttpResponse response = httpclient.execute(post, context)) {
221             assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
222             responseBody = EntityUtils.toString(response.getEntity());
223         }
224         boolean isOk = false;
225         try (CloseableHttpResponse response =
226                 authenticateToWA(username, password, responseBody, httpclient, context)) {
227 
228             switch (response.getStatusLine().getStatusCode()) {
229                 case HttpStatus.SC_OK:
230                     isOk = true;
231                     responseBody = EntityUtils.toString(response.getEntity());
232                     break;
233 
234                 case HttpStatus.SC_MOVED_TEMPORARILY:
235                     location = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
236                     break;
237 
238                 default:
239                     fail();
240             }
241         }
242 
243         // 2c. WA attribute consent screen
244         if (isOk) {
245             // check attribute repository
246             assertTrue(responseBody.contains("identifier"));
247             assertTrue(responseBody.contains("[value1]"));
248 
249             String execution = extractWAExecution(responseBody);
250 
251             List<NameValuePair> form = new ArrayList<>();
252             form.add(new BasicNameValuePair("_eventId", "confirm"));
253             form.add(new BasicNameValuePair("execution", execution));
254             form.add(new BasicNameValuePair("option", "1"));
255             form.add(new BasicNameValuePair("reminder", "30"));
256             form.add(new BasicNameValuePair("reminderTimeUnit", "days"));
257 
258             post = new HttpPost(WA_ADDRESS + "/login");
259             post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
260             post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
261             post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8));
262             try (CloseableHttpResponse response = httpclient.execute(post, context)) {
263                 assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode());
264                 location = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
265             }
266         }
267 
268         if (location.startsWith("http://localhost:8080/syncope-wa")) {
269             location = WA_ADDRESS + StringUtils.substringAfter(location, "http://localhost:8080/syncope-wa");
270         }
271 
272         get = new HttpGet(location);
273         get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
274         get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
275         try (CloseableHttpResponse response = httpclient.execute(get, context)) {
276             assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
277             responseBody = EntityUtils.toString(response.getEntity());
278         }
279 
280         // 2d. post SAML response
281         parsed = parseSAMLResponseForm(responseBody);
282 
283         post = new HttpPost(parsed.getLeft());
284         post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
285         post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
286         post.setEntity(new UrlEncodedFormEntity(
287                 List.of(new BasicNameValuePair("RelayState", parsed.getMiddle()),
288                         new BasicNameValuePair("SAMLResponse", parsed.getRight())), Consts.UTF_8));
289         try (CloseableHttpResponse response = httpclient.execute(post, context)) {
290             assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode());
291             location = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
292         }
293 
294         // 3. verify that user is now authenticated
295         get = new HttpGet(baseURL + StringUtils.removeStart(location, "../"));
296         try (CloseableHttpResponse response = httpclient.execute(get, context)) {
297             assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
298             assertTrue(EntityUtils.toString(response.getEntity()).contains(username));
299         }
300     }
301 
302     @Override
303     protected void doSelfReg(final Runnable runnable) {
304         List<SAML2SP4UIIdPTO> idps = SAML2SP4UI_IDP_SERVICE.list();
305         assertEquals(1, idps.size());
306 
307         SAML2SP4UIIdPTO cas = idps.get(0);
308         cas.setCreateUnmatching(false);
309         cas.setSelfRegUnmatching(true);
310         SAML2SP4UI_IDP_SERVICE.update(cas);
311 
312         try {
313             runnable.run();
314         } finally {
315             cas.setCreateUnmatching(true);
316             cas.setSelfRegUnmatching(false);
317             SAML2SP4UI_IDP_SERVICE.update(cas);
318         }
319     }
320 }