This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git
commit da229662d3f139ef0a10ecb958f42f1fd0e2f68a Author: Robert Munteanu <[email protected]> AuthorDate: Mon Feb 6 23:04:46 2023 +0100 WIP - add an end to end test --- org.apache.sling.servlets.oidc-rp/pom.xml | 22 ++- .../servlets/oidc_rp/AuthorizationCodeFlowIT.java | 152 +++++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) diff --git a/org.apache.sling.servlets.oidc-rp/pom.xml b/org.apache.sling.servlets.oidc-rp/pom.xml index dd92c11a..1e9f282b 100644 --- a/org.apache.sling.servlets.oidc-rp/pom.xml +++ b/org.apache.sling.servlets.oidc-rp/pom.xml @@ -22,7 +22,7 @@ <parent> <groupId>org.apache.sling</groupId> <artifactId>sling-bundle-parent</artifactId> - <version>47</version> + <version>49</version> <relativePath/> </parent> @@ -105,5 +105,25 @@ <version>1.0.0</version> <scope>provided</scope> </dependency> + + <!-- test dependencies --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.9.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>3.24.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.testing.clients</artifactId> + <version>3.0.18</version> + <scope>test</scope> + </dependency> </dependencies> </project> diff --git a/org.apache.sling.servlets.oidc-rp/src/test/java/org/apache/sling/servlets/oidc_rp/AuthorizationCodeFlowIT.java b/org.apache.sling.servlets.oidc-rp/src/test/java/org/apache/sling/servlets/oidc_rp/AuthorizationCodeFlowIT.java new file mode 100644 index 00000000..41e11ee8 --- /dev/null +++ b/org.apache.sling.servlets.oidc-rp/src/test/java/org/apache/sling/servlets/oidc_rp/AuthorizationCodeFlowIT.java @@ -0,0 +1,152 @@ +/* + * 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.sling.servlets.oidc_rp; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.http.Header; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; +import org.apache.sling.testing.clients.ClientException; +import org.apache.sling.testing.clients.SlingClient; +import org.apache.sling.testing.clients.SlingHttpResponse; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.nimbusds.jwt.SignedJWT; + +class AuthorizationCodeFlowIT { + + @Test + void accessTokenIsPresentOnSuccessfulLogin() throws Exception { + // two parts + // - local app on port 8080 + // - keycloak on port 8081 + + // TODO + // 1. automatically start keycloak (test containers?) and import data + // 2. lookup external sling app from a env settting ( and start using maven infrastructure ) + + SlingClient sling = SlingClient.Builder.create(URI.create("http://localhost:8080"), "admin", "admin").disableRedirectHandling().build(); + + // clean up any existing tokens + String userPath = getUserPath(sling, sling.getUser()); + sling.deletePath(userPath + "/oidc-tokens/keycloak", 200); + sling.doGet(userPath + "/oidc-tokens/keycloak", 404); + + // kick off oidc auth + SlingHttpResponse entryPointResponse = sling.doGet("/system/sling/oidc/entry-point", 302); + Header locationHeader = entryPointResponse.getFirstHeader("location"); + assertThat(locationHeader.getElements()).as("Location header value from entry-point request") + .singleElement().asString().startsWith("http://localhost:8081"); + + String locationHeaderValue = locationHeader.getValue(); + + // load login form from keycloak + HttpClient keycloak = HttpClient.newHttpClient(); + HttpRequest renderLoginFormRequest = HttpRequest.newBuilder().uri(URI.create(locationHeaderValue)).build(); + HttpResponse<Stream<String>> renderLoginFormResponse = keycloak.send(renderLoginFormRequest, BodyHandlers.ofLines()); + List<String> matchingFormLines = renderLoginFormResponse.body() + .filter( line -> line.contains("id=\"kc-form-login\"")) + .collect(Collectors.toList()); + assertThat(matchingFormLines).as("lines matching form id").singleElement(); + String formLine = matchingFormLines.get(0); + int actionAttrStart = formLine.indexOf("action=\"") + "action=\"".length(); + int actionAttrEnd = formLine.indexOf('"', actionAttrStart); + + String actionAttr = formLine.substring(actionAttrStart, actionAttrEnd).replace("&", "&"); + + List<String> authFormRequestCookies = renderLoginFormResponse.headers().allValues("set-cookie"); + + Map<String, String> authData = Map.of("username", "test", "password", "test", "credentialId", ""); + String requestBody = authData.entrySet().stream() + .map( e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + + HttpRequest.Builder authenticateRequest = HttpRequest.newBuilder(URI.create(actionAttr)) + .POST(BodyPublishers.ofString(requestBody)) + .header("content-type", "application/x-www-form-urlencoded"); + authFormRequestCookies.stream().forEach( cookie -> authenticateRequest.header("cookie", cookie)); + + HttpResponse<String> authenticateResponse = keycloak.send(authenticateRequest.build(), BodyHandlers.ofString()); + System.out.println(authenticateResponse.body()); + Optional<String> authResponseLocationHeader = authenticateResponse.headers().firstValue("location"); + assertThat(authResponseLocationHeader).as("Authentication response header").isPresent(); + + URI redirectUri = URI.create(authResponseLocationHeader.get()); + System.out.println(redirectUri.getRawPath()+"?" + redirectUri.getRawQuery()); + List<NameValuePair> params = Arrays.stream(redirectUri.getRawQuery().split("&")) + .map( s -> { + var parts = s.split("="); + return (NameValuePair) new BasicNameValuePair(parts[0], parts[1]); + }) + .collect(Collectors.toList()); + sling.doGet(redirectUri.getRawPath(), params, 200); + + JsonNode keycloakToken = sling.doGetJson(userPath + "/oidc-tokens/keycloak",0, 200); + String accesToken = keycloakToken.get("access_token").asText(); + // validate that the JWT is valid; we trust what keycloak has returned but just want to ensure that + // the token was stored correctly + SignedJWT.parse(accesToken); + } + + private String getUserPath(SlingClient sling, String authorizableId) throws ClientException { + + ObjectNode usersJson = (ObjectNode) sling.doGetJson("/home/users", 2, 200); + for ( Map.Entry<String,JsonNode> user : toIterable(usersJson.fields()) ) { + JsonNode jsonNode = user.getValue().get("jcr:primaryType"); + if ( jsonNode == null ) + continue; + + if ( jsonNode.isTextual() && "rep:AuthorizableFolder".equals(jsonNode.asText())) { + ObjectNode node = (ObjectNode) user.getValue(); + for ( Map.Entry<String, JsonNode> user2 : toIterable(node.fields()) ) { + JsonNode primaryType = user2.getValue().get("jcr:primaryType"); + if ( primaryType != null && primaryType.isTextual() && primaryType.asText().equals("rep:User")) { + JsonNode authorizableIdProp = user2.getValue().get("rep:authorizableId"); + if (authorizableId.equals(authorizableIdProp.asText()) ) + return "/home/users/" + user.getKey() + "/" + user2.getKey(); + } + } + } + } + + throw new IllegalArgumentException(String.format("Unable to locate path for user with id '%s'", authorizableId)); + } + + private static <T> Iterable<T> toIterable(Iterator<T> iterator) { + return () -> iterator; + } + +}
