1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 jakarta.ws.rs.core.MediaType;
27 import jakarta.ws.rs.core.Response;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.util.ArrayList;
31 import java.util.List;
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
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
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
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
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
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
244 if (isOk) {
245
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
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
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 get = new HttpGet(CONSOLE_ADDRESS.equals(baseURL)
303 ? baseURL + "wicket/bookmarkable/org.apache.syncope.client.console.pages.Logout"
304 : baseURL + "wicket/bookmarkable/org.apache.syncope.client.enduser.pages.Logout");
305 httpclient.execute(get, context);
306 }
307
308 @Override
309 protected void doSelfReg(final Runnable runnable) {
310 List<SAML2SP4UIIdPTO> idps = SAML2SP4UI_IDP_SERVICE.list();
311 assertEquals(1, idps.size());
312
313 SAML2SP4UIIdPTO cas = idps.get(0);
314 cas.setCreateUnmatching(false);
315 cas.setSelfRegUnmatching(true);
316 SAML2SP4UI_IDP_SERVICE.update(cas);
317
318 try {
319 runnable.run();
320 } finally {
321 cas.setCreateUnmatching(true);
322 cas.setSelfRegUnmatching(false);
323 SAML2SP4UI_IDP_SERVICE.update(cas);
324 }
325 }
326 }