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-org-apache-sling-auth-oauth-client.git
The following commit(s) were added to refs/heads/master by this push:
new 3cad956 feat: add support for all methods in the
OAuthEnabledSlingServlet (#9)
3cad956 is described below
commit 3cad956b413142e8fa0c85c10c7c53f7a719c1e9
Author: Robert Munteanu <[email protected]>
AuthorDate: Thu Dec 12 17:21:26 2024 +0100
feat: add support for all methods in the OAuthEnabledSlingServlet (#9)
---
.../support/OAuthEnabledSlingServlet.java | 122 ++++++++++--
.../support/OAuthEnabledSlingServletTest.java | 213 +++++++++++++++++++++
2 files changed, 314 insertions(+), 21 deletions(-)
diff --git
a/src/main/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServlet.java
b/src/main/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServlet.java
index 273551e..72d57a3 100644
---
a/src/main/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServlet.java
+++
b/src/main/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServlet.java
@@ -16,6 +16,11 @@
*/
package org.apache.sling.auth.oauth_client.support;
+import static org.apache.sling.api.servlets.HttpConstants.METHOD_DELETE;
+import static org.apache.sling.api.servlets.HttpConstants.METHOD_GET;
+import static org.apache.sling.api.servlets.HttpConstants.METHOD_POST;
+import static org.apache.sling.api.servlets.HttpConstants.METHOD_PUT;
+
import java.io.IOException;
import java.util.Objects;
@@ -24,7 +29,7 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
-import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.auth.oauth_client.ClientConnection;
import org.apache.sling.auth.oauth_client.OAuthTokenAccess;
import org.apache.sling.auth.oauth_client.OAuthTokenResponse;
@@ -33,7 +38,18 @@ import org.apache.sling.auth.oauth_client.impl.TokenState;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public abstract class OAuthEnabledSlingServlet extends SlingSafeMethodsServlet
{
+/**
+ * Support class for implementing OAuth-enabled servlets
+ *
+ * <p>Features:</p>
+ *
+ * <ul>
+ * <li>Handles OAuth token retrieval and refresh</li>
+ * <li>Starts the authentication flow if no token is available</li>
+ * <li>Handles invalid access tokens ( {@link
#isInvalidAccessTokenException(Exception)} )</li>
+ * </ul>
+ */
+public abstract class OAuthEnabledSlingServlet extends SlingAllMethodsServlet {
private static final long serialVersionUID = 1L;
@@ -47,12 +63,41 @@ public abstract class OAuthEnabledSlingServlet extends
SlingSafeMethodsServlet {
this.connection = Objects.requireNonNull(connection, "connection may
not null");
this.tokenAccess = Objects.requireNonNull(tokenAccess, "tokenAccess
may not null");
}
+
+ @Override
+ protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull
SlingHttpServletResponse response)
+ throws ServletException, IOException {
+ handleRequestWithToken(request, response, METHOD_GET);
+ }
- @Override
- protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull
SlingHttpServletResponse response)
+ @Override
+ protected void doPost(@NotNull SlingHttpServletRequest request, @NotNull
SlingHttpServletResponse response)
+ throws ServletException, IOException {
+ handleRequestWithToken(request, response, METHOD_POST);
+ }
+
+ @Override
+ protected void doDelete(@NotNull SlingHttpServletRequest request, @NotNull
SlingHttpServletResponse response)
+ throws ServletException, IOException {
+ handleRequestWithToken(request, response, METHOD_DELETE);
+ }
+
+ @Override
+ protected void doPut(@NotNull SlingHttpServletRequest request, @NotNull
SlingHttpServletResponse response)
+ throws ServletException, IOException {
+ handleRequestWithToken(request, response, METHOD_PUT);
+ }
+
+ @Override
+ protected void doGeneric(@NotNull SlingHttpServletRequest request,
@NotNull SlingHttpServletResponse response)
+ throws ServletException, IOException {
+ handleRequestWithToken(request, response, request.getMethod());
+ }
+
+ private void handleRequestWithToken(@NotNull SlingHttpServletRequest
request, @NotNull SlingHttpServletResponse response, String method)
throws ServletException, IOException {
- if ( request.getUserPrincipal() == null ) {
+ if ( request.getRemoteUser() == null ) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User is
not authenticated");
return;
}
@@ -64,33 +109,68 @@ public abstract class OAuthEnabledSlingServlet extends
SlingSafeMethodsServlet {
OAuthTokenResponse tokenResponse =
tokenAccess.getAccessToken(connection, request, redirectPath);
if (tokenResponse.hasValidToken() ) {
- doGetWithPossiblyInvalidToken(request, response, new
OAuthToken(TokenState.VALID, tokenResponse.getTokenValue()), redirectPath);
- } else {
-
response.sendRedirect(tokenResponse.getRedirectUri().toString());
- }
- }
-
- private void doGetWithPossiblyInvalidToken(@NotNull
SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response,
OAuthToken token, String redirectPath) throws ServletException, IOException {
- try {
- doGetWithToken(request, response, token.getValue());
- } catch (ServletException | IOException e) {
+ OAuthToken token = new OAuthToken(TokenState.VALID,
tokenResponse.getTokenValue());
+ try {
+ switch ( method ) {
+ case METHOD_GET:
+ doGetWithToken(request, response, token.getValue());
+ break;
+ case METHOD_POST:
+ doPostWithToken(request, response, token.getValue());
+ break;
+ case METHOD_PUT:
+ doPutWithToken(request, response, token.getValue());
+ break;
+ case METHOD_DELETE:
+ doDeleteWithToken(request, response, token.getValue());
+ break;
+ default:
+ doGenericWithToken(request, response, token.getValue());
+ break;
+ }
+ } catch (IOException | ServletException e) {
if (isInvalidAccessTokenException(e)) {
logger.warn("Invalid access token, clearing exiting token and
restarting OAuth flow", e);
- OAuthTokenResponse tokenResponse =
tokenAccess.clearAccessToken(connection, request, getRedirectPath(request));
-
response.sendRedirect(tokenResponse.getRedirectUri().toString());
+ OAuthTokenResponse newTokenResponse =
tokenAccess.clearAccessToken(connection, request, redirectPath);
+
response.sendRedirect(newTokenResponse.getRedirectUri().toString());
} else {
throw e;
}
}
- }
-
+ } else {
+
response.sendRedirect(tokenResponse.getRedirectUri().toString());
+ }
+ }
+
// TODO - do we need this as a protected method?
protected @NotNull String getRedirectPath(@NotNull
SlingHttpServletRequest request) {
return request.getRequestURI();
}
- protected abstract void doGetWithToken(@NotNull SlingHttpServletRequest
request, @NotNull SlingHttpServletResponse response, String accessToken)
- throws ServletException, IOException;
+ protected void doGetWithToken(@NotNull SlingHttpServletRequest request,
@NotNull SlingHttpServletResponse response, String accessToken)
+ throws IOException, ServletException {
+ handleMethodNotImplemented(request, response);
+ }
+
+ protected void doPostWithToken(@NotNull SlingHttpServletRequest
request, @NotNull SlingHttpServletResponse response, String accessToken)
+ throws IOException, ServletException {
+ handleMethodNotImplemented(request, response);
+ }
+
+ protected void doPutWithToken(@NotNull SlingHttpServletRequest request,
@NotNull SlingHttpServletResponse response, String accessToken)
+ throws IOException, ServletException {
+ handleMethodNotImplemented(request, response);
+ }
+
+ protected void doDeleteWithToken(@NotNull SlingHttpServletRequest
request, @NotNull SlingHttpServletResponse response, String accessToken)
+ throws IOException, ServletException {
+ handleMethodNotImplemented(request, response);
+ }
+
+ protected void doGenericWithToken(@NotNull SlingHttpServletRequest
request, @NotNull SlingHttpServletResponse response, String accessToken)
+ throws IOException, ServletException {
+ handleMethodNotImplemented(request, response);
+ }
protected boolean isInvalidAccessTokenException(Exception e) {
return false;
diff --git
a/src/test/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServletTest.java
b/src/test/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServletTest.java
new file mode 100644
index 0000000..69f8d07
--- /dev/null
+++
b/src/test/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServletTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.auth.oauth_client.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.auth.oauth_client.ClientConnection;
+import org.apache.sling.auth.oauth_client.InMemoryOAuthTokenStore;
+import org.apache.sling.auth.oauth_client.OAuthTokenAccess;
+import org.apache.sling.auth.oauth_client.impl.MockOidcConnection;
+import org.apache.sling.auth.oauth_client.impl.OAuthException;
+import org.apache.sling.auth.oauth_client.impl.OAuthTokenRefresher;
+import org.apache.sling.auth.oauth_client.impl.OAuthTokenStore;
+import org.apache.sling.auth.oauth_client.impl.OAuthTokens;
+import org.apache.sling.auth.oauth_client.impl.TokenAccessImpl;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(SlingContextExtension.class)
+class OAuthEnabledSlingServletTest {
+
+ private SlingContext context = new SlingContext();
+ private TokenAccessImpl tokenAccess;
+ private InMemoryOAuthTokenStore tokenStore;
+
+ @BeforeEach
+ void initServices() {
+ tokenStore = (InMemoryOAuthTokenStore)
context.registerService(OAuthTokenStore.class, new InMemoryOAuthTokenStore());
+ context.registerService(OAuthTokenRefresher.class, new
OAuthTokenRefresher() {
+ @Override
+ public OAuthTokens refreshTokens(ClientConnection connection,
String refreshToken) throws OAuthException {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+ });
+
+ tokenAccess =
context.registerInjectActivateService(TokenAccessImpl.class);
+ }
+
+ @Test
+ void errorWhenUserIsNotLoggedIn() throws ServletException, IOException {
+
+ OAuthEnabledSlingServletTestImpl servlet = new
OAuthEnabledSlingServletTestImpl(MockOidcConnection.DEFAULT_CONNECTION,
tokenAccess);
+
+ servlet.service(context.request(), context.response());
+
+ assertThat(context.response().getStatus()).as("response status
code").isEqualTo(401);
+ }
+
+
+ @Test
+ void redirectWhenNoTokenIsFound() throws ServletException, IOException {
+
+ context.request().setRemoteUser("user");
+
+ OAuthEnabledSlingServletTestImpl servlet = new
OAuthEnabledSlingServletTestImpl(MockOidcConnection.DEFAULT_CONNECTION,
tokenAccess);
+
+ servlet.service(context.request(), context.response());
+
+ assertThat(context.response().getStatus()).as("response status
code").isEqualTo(302);
+ assertThat(context.response().getHeader("location")).as("redirect
location").startsWith("http://localhost/system/sling/oauth/entry-point");
+ }
+
+ @Test
+ void doGetInvokedWhenTokenIsFound() throws ServletException, IOException {
+
+ doInvokeWithToken("GET", "Hello World. GET. TOKEN: ACCESS_TOKEN");
+ }
+
+ @Test
+ void doPostInvokedWhenTokenIsFound() throws ServletException, IOException {
+
+ doInvokeWithToken("POST", "Hello World. POST. TOKEN: ACCESS_TOKEN");
+ }
+
+ @Test
+ void doPutInvokedWhenTokenIsFound() throws ServletException, IOException {
+
+ doInvokeWithToken("PUT", "Hello World. PUT. TOKEN: ACCESS_TOKEN");
+ }
+
+ @Test
+ void doDeleteInvokedWhenTokenIsFound() throws ServletException,
IOException {
+
+ doInvokeWithToken("DELETE", "Hello World. DELETE. TOKEN:
ACCESS_TOKEN");
+ }
+
+ private void doInvokeWithToken(String method, String expectedBody) throws
ServletException, IOException {
+ context.request().setRemoteUser("user");
+
+ tokenStore.persistTokens(MockOidcConnection.DEFAULT_CONNECTION,
context.resourceResolver(), new OAuthTokens("ACCESS_TOKEN", 0, null));
+
+ OAuthEnabledSlingServletTestImpl servlet = new
OAuthEnabledSlingServletTestImpl(MockOidcConnection.DEFAULT_CONNECTION,
tokenAccess);
+
+ context.request().setMethod(method);
+ servlet.service(context.request(), context.response());
+
+ assertThat(context.response().getStatus()).as("response status
code").isEqualTo(200);
+ assertThat(context.response().getOutputAsString()).as("response
body").isEqualTo(expectedBody);
+ }
+
+ @Test
+ void doGenericInvokedWhenTokenIsFound() throws ServletException,
IOException {
+
+ doInvokeWithToken("PATCH", "Hello World. PATCH. TOKEN: ACCESS_TOKEN");
+ }
+
+ @Test
+ void exceptionClearedAndRedirectIssueWhenTokenIsInvalid() throws
ServletException, IOException {
+
+ context.request().setRemoteUser("user");
+
+ tokenStore.persistTokens(MockOidcConnection.DEFAULT_CONNECTION,
context.resourceResolver(), new OAuthTokens("ACCESS_TOKEN", 0, null));
+
+ OAuthEnabledSlingServletTestImpl servlet = new
OAuthEnabledSlingServletTestImpl(MockOidcConnection.DEFAULT_CONNECTION,
tokenAccess);
+
+ context.request().setMethod("ERROR_TOKEN");
+ servlet.service(context.request(), context.response());
+
+ assertThat(context.response().getStatus()).as("response status
code").isEqualTo(302);
+ assertThat(context.response().getHeader("location")).as("redirect
location").startsWith("http://localhost/system/sling/oauth/entry-point");
+ assertThat(tokenStore.allTokens()).as("all tokens").isEmpty();
+ }
+
+ @Test
+ void exceptionPropagated() {
+
+ context.request().setRemoteUser("user");
+
+ tokenStore.persistTokens(MockOidcConnection.DEFAULT_CONNECTION,
context.resourceResolver(), new OAuthTokens("ACCESS_TOKEN", 0, null));
+
+ OAuthEnabledSlingServletTestImpl servlet = new
OAuthEnabledSlingServletTestImpl(MockOidcConnection.DEFAULT_CONNECTION,
tokenAccess);
+
+ context.request().setMethod("ERROR_GENERIC");
+
+ assertThatThrownBy(() -> servlet.service(context.request(),
context.response()))
+ .isInstanceOf(ServletException.class);
+ }
+
+ static class OAuthEnabledSlingServletTestImpl extends
OAuthEnabledSlingServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ public OAuthEnabledSlingServletTestImpl(ClientConnection connection,
OAuthTokenAccess tokenAccess) {
+ super(connection, tokenAccess);
+ }
+
+ @Override
+ protected void doGetWithToken(@NotNull SlingHttpServletRequest request,
+ @NotNull SlingHttpServletResponse response, String
accessToken) throws ServletException, IOException {
+ response.getWriter().write("Hello World. GET. TOKEN: " +
accessToken);
+ }
+
+ @Override
+ protected void doPostWithToken(@NotNull SlingHttpServletRequest
request, @NotNull SlingHttpServletResponse response, String accessToken)
+ throws ServletException, IOException {
+ response.getWriter().write("Hello World. POST. TOKEN: " +
accessToken);
+ }
+ @Override
+ protected void doPutWithToken(@NotNull SlingHttpServletRequest request,
+ @NotNull SlingHttpServletResponse response, String
accessToken) throws IOException, ServletException {
+ response.getWriter().write("Hello World. PUT. TOKEN: " +
accessToken);
+ }
+
+ @Override
+ protected void doDeleteWithToken(@NotNull SlingHttpServletRequest
request,
+ @NotNull SlingHttpServletResponse response, String
accessToken) throws IOException, ServletException {
+ response.getWriter().write("Hello World. DELETE. TOKEN: " +
accessToken);
+ }
+ @Override
+ protected void doGenericWithToken(@NotNull SlingHttpServletRequest
request,
+ @NotNull SlingHttpServletResponse response, String
accessToken) throws IOException, ServletException {
+
+ if (request.getMethod().equals("ERROR_TOKEN") )
+ throw new ServletException("CLEAR_TOKEN");
+
+ if (request.getMethod().equals("ERROR_GENERIC"))
+ throw new ServletException();
+
+ response.getWriter().write("Hello World. " + request.getMethod() +
". TOKEN: " + accessToken);
+ }
+
+ @Override
+ protected boolean isInvalidAccessTokenException(Exception e) {
+ return "CLEAR_TOKEN".equals(e.getMessage());
+ }
+ }
+
+}