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.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 java.io.IOException;
37  import java.io.InputStream;
38  import java.lang.invoke.MethodHandles;
39  import java.text.ParseException;
40  import java.util.ArrayList;
41  import java.util.List;
42  import java.util.Properties;
43  import java.util.Set;
44  import java.util.concurrent.TimeUnit;
45  import java.util.concurrent.TimeoutException;
46  import javax.ws.rs.core.Form;
47  import javax.ws.rs.core.HttpHeaders;
48  import javax.ws.rs.core.MediaType;
49  import javax.ws.rs.core.Response;
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                 // ignore
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         // 1. public
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         // 2. protected
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         // 2a. redirected to WA login screen
208         String responseBody = EntityUtils.toString(response.getEntity());
209         response = authenticateToWA("bellini", "password", responseBody, httpclient, context);
210 
211         // 2b. WA attribute consent screen
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         // 2c. WA scope consent screen
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         // 2d. finally get requested content
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         // 3. logout
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         // 0. access public route
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         // 1. obtain id and access tokens
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             // 1a. take and verify id_token
305             String idToken = json.get("id_token").asText();
306             assertNotNull(idToken);
307             checkJWT(idToken, true);
308         }
309 
310         // 1b. take and verify access_token
311         String accessToken = json.get("access_token").asText();
312         checkJWT(accessToken, false);
313 
314         // 2. access protected route
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 }