You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2022/05/16 17:30:42 UTC

[sling-whiteboard] branch master updated: First draft of an OIDC RP support bundle

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


The following commit(s) were added to refs/heads/master by this push:
     new c7d9dd7e First draft of an OIDC RP support bundle
c7d9dd7e is described below

commit c7d9dd7edfc5a2ed8b5fe995158e79ee3b98fd2c
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Mon May 16 16:50:01 2022 +0200

    First draft of an OIDC RP support bundle
---
 org.apache.sling.servlets.oidc-rp/README.md        |  54 ++++++++
 org.apache.sling.servlets.oidc-rp/pom.xml          | 109 +++++++++++++++
 .../servlets/oidc_rp/OidcConnectionFinder.java     |  38 ++++++
 .../sling/servlets/oidc_rp/impl/Endpoints.java     |  54 ++++++++
 .../servlets/oidc_rp/impl/OidcCallbackServlet.java | 147 +++++++++++++++++++++
 .../servlets/oidc_rp/impl/OidcConnection.java      |  27 ++++
 .../oidc_rp/impl/OidcConnectionFinderImpl.java     |  90 +++++++++++++
 .../servlets/oidc_rp/impl/OidcConnectionImpl.java  |  72 ++++++++++
 .../oidc_rp/impl/OidcConnectionPersister.java      |  24 ++++
 .../oidc_rp/impl/OidcEntryPointServlet.java        |  90 +++++++++++++
 .../sling/servlets/oidc_rp/package-info.java       |  20 +++
 11 files changed, 725 insertions(+)

diff --git a/org.apache.sling.servlets.oidc-rp/README.md b/org.apache.sling.servlets.oidc-rp/README.md
new file mode 100644
index 00000000..c4694add
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/README.md
@@ -0,0 +1,54 @@
+# Apache Sling OpenID Connect Relying Party support bundle
+
+## Sling Starter Deployment
+
+```diff
+diff --git a/src/main/features/app/starter.json b/src/main/features/app/starter.json
+index 9c9231f..18c1586 100644
+--- a/src/main/features/app/starter.json
++++ b/src/main/features/app/starter.json
+@@ -3,6 +3,34 @@
+         {
+             "id":"org.apache.sling:org.apache.sling.starter.content:1.0.12",
+             "start-order":"20"
++        },
++        {
++            "id":"com.nimbusds:oauth2-oidc-sdk:9.35",
++            "start-order":"20"
++        },
++        {
++            "id":"com.nimbusds:nimbus-jose-jwt:9.22",
++            "start-order":"20"
++        },
++        {
++            "id":"com.nimbusds:content-type:2.2",
++            "start-order":"20"
++        },
++        {
++            "id":"com.nimbusds:lang-tag:1.6",
++            "start-order":"20"
++        },
++        {
++            "id":"org.apache.servicemix.bundles:org.apache.servicemix.bundles.jcip-annotations:1.0_2",
++            "start-order":"20"
++        },
++        {
++            "id":"net.minidev:json-smart:2.4.8",
++            "start-order":"20"
++        },
++        {
++            "id":"net.minidev:accessors-smart:2.4.8",
++            "start-order":"20"
+         }
+     ]
+ }
+
+```
+
+## Whiteboard graudation TODO 
+
+- bundle/package should probably be org.apache.sling.extensions.odic-rp, as the primary entry point is the Java API
+- document usage; make sure to explain this is _not_ an authentication handler
+- provide a sample content package and instructions how to use
+- review to see if we can use more of the Nimbus SDK, e.g. enpodints discovery, token parsing
+- review security best practices
diff --git a/org.apache.sling.servlets.oidc-rp/pom.xml b/org.apache.sling.servlets.oidc-rp/pom.xml
new file mode 100644
index 00000000..dd92c11a
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/pom.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling-bundle-parent</artifactId>
+        <version>47</version>
+        <relativePath/>
+    </parent>
+
+	<artifactId>org.apache.sling.servlets.oidc-rp</artifactId>
+	<version>0.1-SNAPSHOT</version>
+
+	<name>Apache Sling Servlets OIDC Relying Party</name>
+
+    <properties>
+        <sling.java.version>17</sling.java.version>
+        <project.build.outputTimestamp>1</project.build.outputTimestamp>
+        <bnd.baseline.skip>true</bnd.baseline.skip>
+	</properties>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>biz.aQute.bnd</groupId>
+				<artifactId>bnd-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+	<dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.metatype.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.annotation.versioning</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.servlets.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-api</artifactId>
+            <version>2.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.0.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>oauth2-oidc-sdk</artifactId>
+            <version>9.35</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.johnzon</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/OidcConnectionFinder.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/OidcConnectionFinder.java
new file mode 100644
index 00000000..116505bb
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/OidcConnectionFinder.java
@@ -0,0 +1,38 @@
+/*
+ * 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 java.net.URI;
+import java.util.Optional;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.ResourceResolver;
+
+// TODO - support more than one connection
+// TODO - bad name
+public interface OidcConnectionFinder {
+
+    // flow would be :
+    // 1. get oidc token
+    // 1a. found → all good
+    // 1b. not found → redirect to oidc entry point uri
+    // 2. after callback, redirect back to same page (how, cookie?)
+    // see https://github.com/panva/node-openid-client/issues/83
+    Optional<String> getOidcToken(ResourceResolver resolver);
+
+    URI getOidcEntryPointUri(SlingHttpServletRequest request, String redirectPath);
+}
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/Endpoints.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/Endpoints.java
new file mode 100644
index 00000000..f167f24c
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/Endpoints.java
@@ -0,0 +1,54 @@
+/*
+ * 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.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+record Endpoints(String authorizationEndpoint, String tokenEndpoint) {
+
+    private static final Logger logger = LoggerFactory.getLogger(Endpoints.class);
+
+    // TODO - cache endpoints, move to service?
+    static Endpoints discover(String base, HttpClient client) throws IOException, InterruptedException {
+        HttpRequest discoveryRequest = HttpRequest.newBuilder()
+                .uri(URI.create(base + "/.well-known/openid-configuration"))
+                .build();
+
+        HttpResponse<byte[]> response = client.send(discoveryRequest, BodyHandlers.ofByteArray());
+        try (JsonReader reader = Json.createReader(new ByteArrayInputStream(response.body()))) {
+            JsonObject cfg = reader.readObject();
+            String authEndpoint = cfg.getJsonString("authorization_endpoint").getString();
+            String tokenEndpoint = cfg.getJsonString("token_endpoint").getString();
+            Endpoints ep = new Endpoints(authEndpoint, tokenEndpoint);
+            logger.info("Discovered auth_endpoint {} , token_endpoint {}", ep.authorizationEndpoint(), ep.tokenEndpoint());
+            return ep;
+        }
+    }
+}
\ No newline at end of file
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcCallbackServlet.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcCallbackServlet.java
new file mode 100644
index 00000000..221cd868
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcCallbackServlet.java
@@ -0,0 +1,147 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLDecoder;
+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.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.servlets.annotations.SlingServletPaths;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
+import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
+import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
+
+@Component(service = { Servlet.class })
+@SlingServletPaths(OidcCallbackServlet.PATH)
+public class OidcCallbackServlet extends SlingAllMethodsServlet {
+
+    static final String PATH = "/system/sling/oidc/callback";
+
+    private static final long serialVersionUID = 1L;
+
+    private final OidcConnection connection;
+    private final OidcConnectionPersister persister;
+
+    static String getCallbackUri(HttpServletRequest request) {
+        return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + PATH;
+    }
+
+    @Activate
+    public OidcCallbackServlet(@Reference OidcConnection connection, @Reference OidcConnectionPersister persister) {
+        this.connection = connection;
+        this.persister = persister;
+    }
+
+    @Override
+    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
+            throws ServletException, IOException {
+        try {
+            StringBuffer requestURL = request.getRequestURL();
+            if ( request.getQueryString() != null )
+                requestURL.append('?').append(request.getQueryString());
+
+            AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(requestURL.toString()));
+            State state = (State) request.getSession().getAttribute(OidcEntryPointServlet.SESSION_ATTRIBUTE_STATE);
+            request.getSession().removeAttribute(OidcEntryPointServlet.SESSION_ATTRIBUTE_STATE);
+            if ( state == null || !authResponse.getState().equals(state))
+                throw new ServletException("Failed state check");
+
+            if ( response instanceof AuthenticationErrorResponse )
+                throw new ServletException(authResponse.toErrorResponse().getErrorObject().toString());
+
+            String authCode = authResponse.toSuccessResponse().getAuthorizationCode().getValue();
+
+            HttpClient client = HttpClient.newHttpClient();
+            Endpoints ep = Endpoints.discover(connection.baseUrl(), client);
+
+            Map<String, String> params = new HashMap<>();
+            params.put("client_id", connection.clientId());
+            params.put("client_secret", connection.clientSecret());
+            params.put("code", authCode);
+            params.put("grant_type", "authorization_code");
+            params.put("redirect_uri", getCallbackUri(request));
+
+            String payload = params.entrySet().stream()
+                    .map( e -> e.getKey() + "=" +URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
+                    .collect(Collectors.joining("&"));
+
+            HttpRequest tokenRequest = HttpRequest.newBuilder()
+                    .uri(URI.create(ep.tokenEndpoint()))
+                    .headers("Content-Type", "application/x-www-form-urlencoded")
+                    .POST(BodyPublishers.ofString(payload))
+                    .build();
+
+            HttpResponse<String> tokenResponse = client.send(tokenRequest, BodyHandlers.ofString());
+
+            String accessToken;
+            try ( JsonReader reader = Json.createReader(new StringReader(tokenResponse.body())) ) {
+                JsonObject tokenObject = reader.readObject();
+                accessToken = tokenObject.getString("access_token");
+//                int expires = tokenObject.getInt("expires_in"); // TODO - this is optional
+            }
+
+            persister.persistToken(request.getResourceResolver(), accessToken);
+
+            String encodedRedirect = (String) request.getSession().getAttribute(OidcEntryPointServlet.SESSION_ATTRIBUTE_REDIRECT);
+            if ( encodedRedirect == null ) {
+                response.setStatus(HttpServletResponse.SC_OK);
+                response.addHeader("Content-Type", "text/plain");
+                response.getWriter().write("OK");
+                response.getWriter().flush();
+                response.getWriter().close();
+            } else {
+                request.getSession().removeAttribute(OidcEntryPointServlet.SESSION_ATTRIBUTE_REDIRECT);
+                String redirect = URLDecoder.decode(encodedRedirect, StandardCharsets.UTF_8);
+                response.sendRedirect(redirect);
+            }
+        } catch (ParseException | URISyntaxException | IOException e) {
+            throw new ServletException(e);
+        } catch (InterruptedException e1) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+}
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnection.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnection.java
new file mode 100644
index 00000000..73329dfa
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnection.java
@@ -0,0 +1,27 @@
+/*
+ * 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.impl;
+
+// TODO - bad name
+public interface OidcConnection {
+
+    String name();
+    String baseUrl();
+    String clientId();
+    String clientSecret();
+    String[] scopes();
+}
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionFinderImpl.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionFinderImpl.java
new file mode 100644
index 00000000..ecc21992
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionFinderImpl.java
@@ -0,0 +1,90 @@
+/*
+ * 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.impl;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.servlets.oidc_rp.OidcConnectionFinder;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+@Component
+public class OidcConnectionFinderImpl implements OidcConnectionFinder, OidcConnectionPersister {
+
+    private final OidcConnection connection;
+
+    @Activate
+    public OidcConnectionFinderImpl(@Reference OidcConnection connection) {
+        this.connection = connection;
+    }
+
+    @Override
+    public Optional<String> getOidcToken(ResourceResolver resolver) {
+        // TODO - remove expired tokens
+        try {
+            User user = resolver.adaptTo(User.class);
+            Value[] tokenValue = user.getProperty(tokenPropertyPath());
+            if ( tokenValue == null )
+                return Optional.empty();
+
+            if ( tokenValue.length != 1)
+                throw new RuntimeException("Unexpected value count for token property : " + tokenValue.length);
+
+            return Optional.of(tokenValue[0].getString());
+        } catch (RepositoryException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void persistToken(ResourceResolver resolver, String tokenValue) {
+        try {
+            User currentUser = resolver.adaptTo(User.class);
+            Session session = resolver.adaptTo(Session.class);
+            currentUser.setProperty("oidc-tokens/" + connection.name() + "/token", session.getValueFactory().createValue(tokenValue));
+            session.save();
+        } catch (RepositoryException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public URI getOidcEntryPointUri(SlingHttpServletRequest request, String redirectPath) {
+        StringBuilder uri = new StringBuilder();
+        uri.append(request.getScheme()).append("://").append(request.getServerName()).append(":").append(request.getServerPort()).append(OidcEntryPointServlet.PATH);
+        if ( redirectPath != null )
+            uri.append("?redirect=").append(URLEncoder.encode(redirectPath, StandardCharsets.UTF_8));
+
+        return URI.create(uri.toString());
+    }
+
+    private String tokenPropertyPath() {
+        return "oidc-tokens/" + connection.name() + "/token";
+    }
+
+}
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionImpl.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionImpl.java
new file mode 100644
index 00000000..9a7ab472
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionImpl.java
@@ -0,0 +1,72 @@
+/*
+ * 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.impl;
+
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.AttributeType;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+// TODO - bad name
+@Component
+@Designate(ocd = OidcConnectionImpl.Config.class)
+public class OidcConnectionImpl implements OidcConnection{
+
+    @ObjectClassDefinition(name = "OpenID Connect connection details")
+    public @interface Config {
+        String name();
+        String baseUrl();
+        String clientId();
+        @AttributeDefinition(type = AttributeType.PASSWORD) String clientSecret();
+        String[] scopes();
+    }
+
+    private Config cfg;
+
+    @Activate
+    public OidcConnectionImpl(Config cfg) {
+        this.cfg = cfg;
+    }
+
+    @Override
+    public String baseUrl() {
+        return cfg.baseUrl();
+    }
+
+    @Override
+    public String clientId() {
+        return cfg.clientId();
+    }
+
+    @Override
+    public String clientSecret() {
+        return cfg.clientSecret();
+    }
+
+    @Override
+    public String[] scopes() {
+        return cfg.scopes();
+    }
+
+    @Override
+    public String name() {
+        return cfg.name();
+    }
+
+}
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionPersister.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionPersister.java
new file mode 100644
index 00000000..9693f393
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcConnectionPersister.java
@@ -0,0 +1,24 @@
+/*
+ * 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.impl;
+
+import org.apache.sling.api.resource.ResourceResolver;
+
+public interface OidcConnectionPersister {
+
+    void persistToken(ResourceResolver resourceResolver, String tokenValue);
+}
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcEntryPointServlet.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcEntryPointServlet.java
new file mode 100644
index 00000000..c8d92f53
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/impl/OidcEntryPointServlet.java
@@ -0,0 +1,90 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.servlets.annotations.SlingServletPaths;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+import com.nimbusds.openid.connect.sdk.Nonce;
+
+@Component(service = { Servlet.class })
+@SlingServletPaths(OidcEntryPointServlet.PATH)
+public class OidcEntryPointServlet extends SlingAllMethodsServlet {
+    private static final long serialVersionUID = 1L;
+
+    static final String SESSION_ATTRIBUTE_STATE = OidcEntryPointServlet.class.getName() + ".state";
+    static final String SESSION_ATTRIBUTE_REDIRECT = OidcEntryPointServlet.class.getName() + ".redirect";
+    static final String PATH = "/system/sling/oidc/entry-point"; // NOSONAR
+    private final OidcConnection connection;
+
+    @Activate
+    public OidcEntryPointServlet(@Reference OidcConnection connection) {
+        this.connection = connection;
+    }
+    @Override
+    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
+            throws ServletException, IOException {
+
+        try {
+
+            Endpoints ep = Endpoints.discover(connection.baseUrl(), HttpClient.newHttpClient());
+
+            // The client ID provisioned by the OpenID provider when
+            // the client was registered
+            ClientID clientID = new ClientID(connection.clientId());
+
+            // The client callback URL
+            URI callback = new URI(OidcCallbackServlet.getCallbackUri(request));
+            // Generate random state string to securely pair the callback to this request
+            State state = new State();
+            request.getSession().setAttribute(SESSION_ATTRIBUTE_STATE, state);
+            if ( request.getParameter("redirect") != null )
+                request.getSession().setAttribute(SESSION_ATTRIBUTE_REDIRECT, request.getParameter("redirect"));
+
+            // Generate nonce for the ID token
+            Nonce nonce = new Nonce();
+
+            // Compose the OpenID authentication request (for the code flow)
+            AuthenticationRequest authRequest = new AuthenticationRequest.Builder(new ResponseType("code"), new Scope(connection.scopes()),
+                    clientID, callback).endpointURI(new URI(ep.authorizationEndpoint())).state(state).nonce(nonce).build();
+
+            response.sendRedirect(authRequest.toURI().toString());
+        } catch (URISyntaxException e) {
+            throw new ServletException(e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+}
diff --git a/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/package-info.java b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/package-info.java
new file mode 100644
index 00000000..632d6dfd
--- /dev/null
+++ b/org.apache.sling.servlets.oidc-rp/src/main/java/org/apache/sling/servlets/oidc_rp/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+@Version("0.1.0")
+package org.apache.sling.servlets.oidc_rp;
+
+import org.osgi.annotation.versioning.Version;