1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.fit.sra;
20
21 import static org.awaitility.Awaitility.await;
22 import static org.hamcrest.MatcherAssert.assertThat;
23 import static org.hamcrest.Matchers.is;
24 import static org.hamcrest.Matchers.oneOf;
25 import static org.junit.jupiter.api.Assertions.assertEquals;
26 import static org.junit.jupiter.api.Assertions.assertFalse;
27 import static org.junit.jupiter.api.Assertions.assertNotNull;
28 import static org.junit.jupiter.api.Assertions.assertTrue;
29 import static org.junit.jupiter.api.Assertions.fail;
30 import static org.junit.jupiter.api.Assumptions.assumeTrue;
31
32 import com.fasterxml.jackson.databind.JsonNode;
33 import com.fasterxml.jackson.databind.node.ObjectNode;
34 import com.nimbusds.jwt.JWTClaimsSet;
35 import com.nimbusds.jwt.SignedJWT;
36 import jakarta.ws.rs.core.Form;
37 import jakarta.ws.rs.core.HttpHeaders;
38 import jakarta.ws.rs.core.MediaType;
39 import jakarta.ws.rs.core.Response;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.lang.invoke.MethodHandles;
43 import java.text.ParseException;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Properties;
47 import java.util.Set;
48 import java.util.concurrent.TimeUnit;
49 import java.util.concurrent.TimeoutException;
50 import org.apache.cxf.jaxrs.client.WebClient;
51 import org.apache.http.Consts;
52 import org.apache.http.HttpStatus;
53 import org.apache.http.NameValuePair;
54 import org.apache.http.client.entity.UrlEncodedFormEntity;
55 import org.apache.http.client.methods.CloseableHttpResponse;
56 import org.apache.http.client.methods.HttpGet;
57 import org.apache.http.client.methods.HttpPost;
58 import org.apache.http.client.protocol.HttpClientContext;
59 import org.apache.http.impl.client.BasicCookieStore;
60 import org.apache.http.impl.client.CloseableHttpClient;
61 import org.apache.http.impl.client.HttpClients;
62 import org.apache.http.message.BasicNameValuePair;
63 import org.apache.http.util.EntityUtils;
64 import org.apache.syncope.common.lib.OIDCScopeConstants;
65 import org.apache.syncope.common.lib.SyncopeConstants;
66 import org.apache.syncope.common.lib.to.OIDCRPClientAppTO;
67 import org.apache.syncope.common.lib.types.ClientAppType;
68 import org.apache.syncope.common.lib.types.OIDCGrantType;
69 import org.apache.syncope.common.lib.types.OIDCSubjectType;
70 import org.apache.syncope.common.rest.api.RESTHeaders;
71 import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
72 import org.apereo.cas.oidc.OidcConstants;
73 import org.jsoup.Jsoup;
74 import org.junit.jupiter.api.BeforeAll;
75 import org.junit.jupiter.api.Test;
76
77 public class OIDCSRAITCase extends AbstractSRAITCase {
78
79 protected static String SRA_REGISTRATION_ID;
80
81 protected static Long CLIENT_APP_ID;
82
83 protected static String CLIENT_ID;
84
85 protected static String CLIENT_SECRET;
86
87 protected static String TOKEN_URI;
88
89 @BeforeAll
90 public static void startSRA() throws IOException, InterruptedException, TimeoutException {
91 assumeTrue(OIDCSRAITCase.class.equals(MethodHandles.lookup().lookupClass()));
92
93 doStartSRA("oidc");
94 }
95
96 protected static void oidcClientAppSetup(
97 final String appName,
98 final String sraRegistrationId,
99 final Long clientAppId,
100 final String clientId,
101 final String clientSecret) {
102
103 OIDCRPClientAppTO clientApp = CLIENT_APP_SERVICE.list(ClientAppType.OIDCRP).stream().
104 filter(app -> appName.equals(app.getName())).
105 map(OIDCRPClientAppTO.class::cast).
106 findFirst().
107 orElseGet(() -> {
108 OIDCRPClientAppTO app = new OIDCRPClientAppTO();
109 app.setName(appName);
110 app.setRealm(SyncopeConstants.ROOT_REALM);
111 app.setClientAppId(clientAppId);
112 app.setClientId(clientId);
113 app.setClientSecret(clientSecret);
114 app.setBypassApprovalPrompt(false);
115
116 Response response = CLIENT_APP_SERVICE.create(ClientAppType.OIDCRP, app);
117 if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
118 fail("Could not create OIDC Client App");
119 }
120
121 return CLIENT_APP_SERVICE.read(
122 ClientAppType.OIDCRP, response.getHeaderString(RESTHeaders.RESOURCE_KEY));
123 });
124
125 clientApp.setJwtAccessToken(true);
126 clientApp.setClientId(clientId);
127 clientApp.setClientSecret(clientSecret);
128 clientApp.setSubjectType(OIDCSubjectType.PUBLIC);
129 clientApp.getRedirectUris().clear();
130 clientApp.getRedirectUris().add(SRA_ADDRESS + "/login/oauth2/code/" + sraRegistrationId);
131 clientApp.setSignIdToken(true);
132 clientApp.setLogoutUri(SRA_ADDRESS + "/logout");
133 clientApp.setAuthPolicy(getAuthPolicy().getKey());
134 clientApp.setAttrReleasePolicy(getAttrReleasePolicy().getKey());
135 clientApp.getScopes().add(OIDCScopeConstants.OPEN_ID);
136 clientApp.getScopes().add(OIDCScopeConstants.PROFILE);
137 clientApp.getScopes().add(OIDCScopeConstants.EMAIL);
138 clientApp.getSupportedGrantTypes().add(OIDCGrantType.password);
139 clientApp.getSupportedGrantTypes().add(OIDCGrantType.authorization_code);
140
141 CLIENT_APP_SERVICE.update(ClientAppType.OIDCRP, clientApp);
142
143 await().atMost(60, TimeUnit.SECONDS).pollInterval(20, TimeUnit.SECONDS).until(() -> {
144 try {
145 String metadata = WebClient.create(
146 WA_ADDRESS + "/oidc/" + OidcConstants.WELL_KNOWN_OPENID_CONFIGURATION_URL).
147 get().readEntity(String.class);
148 if (!metadata.contains("groups")) {
149 WA_CONFIG_SERVICE.pushToWA(WAConfigService.PushSubject.conf, List.of());
150 throw new IllegalStateException();
151 }
152
153 return true;
154 } catch (Exception e) {
155
156 }
157 return false;
158 });
159 WA_CONFIG_SERVICE.pushToWA(WAConfigService.PushSubject.clientApps, List.of());
160 }
161
162 @BeforeAll
163 public static void clientAppSetup() {
164 assumeTrue(OIDCSRAITCase.class.equals(MethodHandles.lookup().lookupClass()));
165
166 Properties props = new Properties();
167 try (InputStream propStream = OIDCSRAITCase.class.getResourceAsStream("/sra-oidc.properties")) {
168 props.load(propStream);
169 } catch (Exception e) {
170 fail("Could not load /sra-oidc.properties", e);
171 }
172 SRA_REGISTRATION_ID = "OIDC";
173 CLIENT_APP_ID = 1L;
174 CLIENT_ID = props.getProperty("sra.oidc.client-id");
175 assertNotNull(CLIENT_ID);
176 CLIENT_SECRET = props.getProperty("sra.oidc.client-secret");
177 assertNotNull(CLIENT_SECRET);
178 TOKEN_URI = WA_ADDRESS + "/oidc/accessToken";
179
180 oidcClientAppSetup(
181 OIDCSRAITCase.class.getName(), SRA_REGISTRATION_ID, CLIENT_APP_ID, CLIENT_ID, CLIENT_SECRET);
182 }
183
184 @Test
185 public void web() throws IOException {
186 CloseableHttpClient httpclient = HttpClients.createDefault();
187 HttpClientContext context = HttpClientContext.create();
188 context.setCookieStore(new BasicCookieStore());
189
190
191 HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?" + QUERY_STRING);
192 get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
193 get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
194 CloseableHttpResponse response = httpclient.execute(get, context);
195
196 ObjectNode headers = checkGetResponse(response, get.getURI().toASCIIString().replace("/public", ""));
197 assertFalse(headers.has(HttpHeaders.COOKIE));
198
199
200 get = new HttpGet(SRA_ADDRESS + "/protected/get?" + QUERY_STRING);
201 String originalRequestURI = get.getURI().toASCIIString();
202 get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
203 get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
204 response = httpclient.execute(get, context);
205 assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
206
207
208 String responseBody = EntityUtils.toString(response.getEntity());
209 response = authenticateToWA("bellini", "password", responseBody, httpclient, context);
210
211
212 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
213 responseBody = EntityUtils.toString(response.getEntity());
214 String execution = extractWAExecution(responseBody);
215
216 List<NameValuePair> form = new ArrayList<>();
217 form.add(new BasicNameValuePair("_eventId", "confirm"));
218 form.add(new BasicNameValuePair("execution", execution));
219 form.add(new BasicNameValuePair("option", "1"));
220 form.add(new BasicNameValuePair("reminder", "30"));
221 form.add(new BasicNameValuePair("reminderTimeUnit", "days"));
222
223 HttpPost post = new HttpPost(WA_ADDRESS + "/login");
224 post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
225 post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
226 post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8));
227 response = httpclient.execute(post, context);
228 }
229 assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode());
230
231
232 get = new HttpGet(response.getLastHeader(HttpHeaders.LOCATION).getValue());
233 get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
234 get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
235 response = httpclient.execute(get, context);
236 assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
237
238 responseBody = EntityUtils.toString(response.getEntity());
239
240 String allow = Jsoup.parse(responseBody).body().
241 getElementsByTag("a").select("a[name=allow]").first().
242 attr("href");
243 assertNotNull(allow);
244
245
246 get = new HttpGet(allow);
247 get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML);
248 get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
249 response = httpclient.execute(get, context);
250
251 headers = checkGetResponse(response, originalRequestURI.replace("/protected", ""));
252 assertTrue(headers.get(HttpHeaders.COOKIE).asText().contains("SESSION"));
253
254
255 get = new HttpGet(SRA_ADDRESS + "/protected/logout");
256 get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE);
257 response = httpclient.execute(get, context);
258 checkLogout(response);
259 }
260
261 private void checkJWT(final String token, final boolean idToken) throws ParseException {
262 assertNotNull(token);
263 SignedJWT jwt = SignedJWT.parse(token);
264 assertNotNull(jwt);
265 JWTClaimsSet idTokenClaimsSet = jwt.getJWTClaimsSet();
266 assertEquals("verdi", idTokenClaimsSet.getSubject());
267 if (idToken) {
268 assertEquals("verdi", idTokenClaimsSet.getStringClaim("preferred_username"));
269 }
270 assertEquals("verdi@syncope.org", idTokenClaimsSet.getStringClaim("email"));
271 assertEquals("Verdi", idTokenClaimsSet.getStringClaim("family_name"));
272 assertEquals("Giuseppe", idTokenClaimsSet.getStringClaim("given_name"));
273 assertEquals("Giuseppe Verdi", idTokenClaimsSet.getStringClaim("name"));
274 assertEquals(Set.of("root", "child", "citizen"), Set.of(idTokenClaimsSet.getStringArrayClaim("groups")));
275 }
276
277 protected boolean checkIdToken() {
278 return true;
279 }
280
281 @Test
282 public void rest() throws IOException, ParseException {
283
284 WebClient client = WebClient.create(SRA_ADDRESS + "/public/post").
285 accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON);
286 Response response = client.post(null);
287 assertEquals(HttpStatus.SC_OK, response.getStatus());
288
289
290 Form form = new Form().
291 param("grant_type", "password").
292 param("client_id", CLIENT_ID).
293 param("client_secret", CLIENT_SECRET).
294 param("username", "verdi").
295 param("password", "password").
296 param("scope", "openid profile email syncope");
297 response = WebClient.create(TOKEN_URI).post(form);
298 assertEquals(HttpStatus.SC_OK, response.getStatus());
299 assertTrue(response.getHeaderString(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON));
300
301 JsonNode json = MAPPER.readTree(response.readEntity(String.class));
302
303 if (checkIdToken()) {
304
305 String idToken = json.get("id_token").asText();
306 assertNotNull(idToken);
307 checkJWT(idToken, true);
308 }
309
310
311 String accessToken = json.get("access_token").asText();
312 checkJWT(accessToken, false);
313
314
315 client = WebClient.create(SRA_ADDRESS + "/protected/post").
316 authorization("Bearer " + accessToken).
317 accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON);
318 response = client.post(null);
319
320 assertEquals(HttpStatus.SC_OK, response.getStatus());
321
322 json = MAPPER.readTree(response.readEntity(String.class));
323
324 ObjectNode headers = (ObjectNode) json.get("headers");
325 assertEquals(MediaType.APPLICATION_JSON, headers.get(HttpHeaders.ACCEPT).asText());
326 assertEquals(MediaType.APPLICATION_JSON, headers.get(HttpHeaders.CONTENT_TYPE).asText());
327 assertThat(headers.get("X-Forwarded-Host").asText(), is(oneOf("localhost:8080", "127.0.0.1:8080")));
328
329 String withHost = client.getBaseURI().toASCIIString().replace("/protected", "");
330 String withIP = withHost.replace("localhost", "127.0.0.1");
331 assertThat(json.get("url").asText(), is(oneOf(withHost, withIP)));
332 }
333 }