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("&amp;", "&");
+        
+        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;
+    }
+
+}

Reply via email to