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 2024/12/10 14:18:41 UTC
(sling-org-apache-sling-auth-oauth-client) branch master updated: feat: introduce a high-level API for retrieving access tokens (#5)
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 7808fdb feat: introduce a high-level API for retrieving access tokens (#5)
7808fdb is described below
commit 7808fdbeac0411aab4262f0dbbe61cfd5784c9e6
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Dec 10 15:18:37 2024 +0100
feat: introduce a high-level API for retrieving access tokens (#5)
---
README.md | 98 ++++++++++++--
.../sling/auth/oauth_client/OAuthTokenAccess.java | 69 ++++++++++
.../auth/oauth_client/OAuthTokenResponse.java | 83 ++++++++++++
.../sling/auth/oauth_client/TokenAccessImpl.java | 100 ++++++++++++++
.../support/OAuthEnabledSlingServlet.java | 46 ++-----
.../auth/oauth_client/TokenAccessImplTest.java | 149 +++++++++++++++++++++
6 files changed, 498 insertions(+), 47 deletions(-)
diff --git a/README.md b/README.md
index 8de530e..997eb5b 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,48 @@ the authentication code flow based on OIDC and OAuth 2.0 .
## Usage
-### High-level APIs
+### Models and other Java APIs
+
+The `OAuthTokenAccess` OSGi service exposes methods to retrieve and clear access tokens. These methods encapsulate
+persistence concerns and handle refresh tokens transparently, if present.
+
+```java
+@Model(adaptables = SlingHttpServletRequest.class)
+public class MyModel {
+
+ @SlingObject private SlingHttpServletRequest request;
+
+ @OSGiService(filter = "(name=foo)") private ClientConnection connection;
+
+ @OSGiService private OAuthTokenAccess tokenAccess;
+
+ private OAuthTokenResponse tokenResponse;
+
+ @PostConstruct
+ public void initToken() {
+ tokenResponse = tokenAccess.getAccessToken(connection, request, request.getRequestURI());
+ }
+
+ public MyView getResponse() {
+ if ( tokenResponse.hasValidToken() ) {
+ return doQuery(tokenResponse.getTokenValue());
+ }
+
+ return null;
+ }
+
+ public String getRedirectLink() {
+ if ( !tokenResponse.hasValidToken() ) {
+ return tokenResponse.getRedirectUri().toString();
+ }
+
+ return null;
+ }
+}
+
+```
+
+### Servlets
The bundle exposes an abstract `OAuthEnabledSlingServlet` that contains the boilerplate code needed
to obtain a valid OAuth 2 access token.
@@ -36,10 +77,9 @@ public class MySlingServlet extends OAuthEnabledSlingServlet {
@Activate
public MySlingServlet(@Reference OidcConnection connection,
- @Reference OAuthTokenStore tokenStore,
- @Reference OAuthTokenRefresher oidcClient,
+ @Reference OAuthTokenAccess tokenAccess,
@Reference MyRemoteService svc) {
- super(connection, tokenStore, oidcClient);
+ super(connection, tokenAccess);
this.svc = svc;
}
@@ -52,9 +92,6 @@ public class MySlingServlet extends OAuthEnabledSlingServlet {
}
```
-### Low-level APIs
-
-TODO
### Clearing access tokens
@@ -65,24 +102,57 @@ invalidated out of band.
The client will need to determine if the access token is invalid as this is a provider-specific
check.
-To remove invalid access tokens there is a low-level API
+#### When the request and response are available
+
+This method is generally recommended as it permits the generation of a redirect URI that will kick
+off a new OAuth authorisation flow.
+
```java
-public class MyComponent {}
- @Reference private OAuthTokenStore tokenStore;
+@Model(adaptables = SlingHttpServletRequest.class)
+public class MySlingModel {
+ @OSGiService private OAuthTokenAccess tokenAccess;
+ @SlingObject SlingHttpServletRequest request;
+ @OSGiService(filter = "(name=foo)") private ClientConnection connection;
+
+ public String getLink() {
+ // code elided
+ if ( accessTokenIsInvalid() ) {
+ OAuthTokenResponse response = tokenAccess.clearAccessToken(connection, request, request.getRequestURI());
+ return response.getRedirectUri().toString();
+ }
+ }
+}
+```
+
+
+#### When the request and response are not available
+
+
+This approach should be used when invalidating access tokens without user interaction, as it does not
+provide a mechanism to generate a redirect URL for restarting the OAuth authorisation flow and obtaining
+a new access token.
+
+```java
+@Component
+public class MyComponent {
+ @Reference private OAuthTokenAccess tokenAccess;
public void execute(@Reference OidcConnection connection, ResourceResolver resolver) {
// code elided
if ( accessTokenIsInvalid() ) {
- tokenStore.clearAccessToken(connection, resolver);
- // redirect to provider or present a message to the user
+ tokenAccess.clearAccessToken(connection, resolver);
}
}
}
```
-For classes that extend from the `OAuthEnabledSlingServlet` the following method override can be
-applied
+
+
+#### When extending OAuthEnabledSlingServlet
+
+For classes that extend from the `OAuthEnabledSlingServlet` the `isInvalidAccessTokenException` method can be
+overriden. If this method returns true, the access token is cleared and a new OAuth flow is started.
```java
@Component(service = { Servlet.class })
diff --git a/src/main/java/org/apache/sling/auth/oauth_client/OAuthTokenAccess.java b/src/main/java/org/apache/sling/auth/oauth_client/OAuthTokenAccess.java
new file mode 100644
index 0000000..07815b5
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/oauth_client/OAuthTokenAccess.java
@@ -0,0 +1,69 @@
+/*
+ * 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;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Entry point for accessing and clearing OAuth access tokens
+ *
+ * <p>The tokens are stored distinctly for each client connection and user. The client connection is identified by
+ * {@link ClientConnection#name() name} and the user is identified by the {@link ResourceResolver#getUserID() user id}. </p>
+ *
+ * <p>The storage strategy may vary and is controlled by the currently active implementation of the {@link OAuthTokenStore}.</p>
+ */
+@NotNull
+public interface OAuthTokenAccess {
+
+ /**
+ * Retrieves an existing access, valid, access token from storage.
+ *
+ * <p>Refreshes expired access tokens if a refresh token is available but does not attempt to retrieve new access tokens.</p>
+ *
+ * @param connection the client connection to retrieve token for
+ * @param request the request used to determine the current user for which to retrieve the token and to build the redirect URL
+ * @param redirectPath the path to redirect to after completing the OAuth flow
+ * @return the token response
+ */
+ OAuthTokenResponse getAccessToken(ClientConnection connection, SlingHttpServletRequest request, String redirectPath);
+
+ /**
+ * Clears the access token for the given connection and user, as identified by the request.
+ *
+ * <p>Returns a response that does not have a valid token and contains a URI to redirect the user to.</p>
+ *
+ * @param connection the client connection to clear the token for
+ * @param request the request used to determine the current user for which to retrieve the token and to build the redirect URL
+ * @param redirectPath the path to redirect to after completing the OAuth flow
+ * @return the token response
+ */
+ OAuthTokenResponse clearAccessToken(ClientConnection connection, SlingHttpServletRequest request, String redirectPath);
+
+ /**
+ * Clears the access token for the given connection and user, as identified by the resource resolver
+ *
+ * <p>For scenarios where a redirect URI should be generated after clearing the access token {@link #clearAccessToken(ClientConnection, SlingHttpServletRequest, String)}
+ * should be used instead.</p>
+ *
+ * @param connection the client connection to clear the token for
+ * @param resolver used to determine the current user for which to retrieve the token
+ */
+ void clearAccessToken(ClientConnection connection, ResourceResolver resolver);
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/auth/oauth_client/OAuthTokenResponse.java b/src/main/java/org/apache/sling/auth/oauth_client/OAuthTokenResponse.java
new file mode 100644
index 0000000..8d3553a
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/oauth_client/OAuthTokenResponse.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import java.net.URI;
+import java.util.Optional;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Encapsulates the response to a token request.
+ *
+ * <p>This class has two top-level states:</p>
+ * <ol>
+ * <li>has a valid access token: {@link #hasValidToken()} returns {@code true}, and {@link #getTokenValue()} returns the token value.</li>
+ * <li>does not have a valid access token: {@link #hasValidToken()} returns {@code false}, and {@link #getRedirectUri()} returns the URI to redirect the user to.</li>
+ * </ol>
+ *
+ * <p>Methods generally throw {@link IllegalStateException} if they are called in an unexpected state and do not return null values.</p>
+ */
+@NotNull
+public class OAuthTokenResponse {
+
+ private final Optional<String> token;
+ private final ClientConnection connection;
+ private final SlingHttpServletRequest request;
+ private String redirectPath;
+
+ public OAuthTokenResponse(Optional<String> token, ClientConnection connection, SlingHttpServletRequest request, String redirectPath) {
+ this.token = token;
+ this.connection = connection;
+ this.request = request;
+ this.redirectPath = redirectPath;
+ }
+
+ /**
+ * Returns true if a valid access token is present and false otherwise
+ *
+ * @return true if a valid access token is present
+ */
+ public boolean hasValidToken() {
+ return token.isPresent();
+ }
+
+
+ /**
+ * Returns the a valid access token value and throws an {@link IllegalStateException} otherwise
+ *
+ * @return a valid access token value
+ * @throws IllegalStateException if no access token is present
+ */
+ public String getTokenValue() {
+ return token.orElseThrow(() -> new IllegalStateException("No access token present."));
+ }
+
+ /**
+ * Returns the URI to redirect the user to in order to start the OAuth flow
+ *
+ * @return the URI to redirect the user to
+ * @throws IllegalStateException if an access token is present
+ */
+ public URI getRedirectUri() {
+ if ( token.isPresent() )
+ throw new IllegalStateException("Access token is present, will not generate a new redirect URI.");
+
+ return OAuthUris.getOidcEntryPointUri(connection, request, redirectPath);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/auth/oauth_client/TokenAccessImpl.java b/src/main/java/org/apache/sling/auth/oauth_client/TokenAccessImpl.java
new file mode 100644
index 0000000..bad019f
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/oauth_client/TokenAccessImpl.java
@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+import java.util.Optional;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+public class TokenAccessImpl implements OAuthTokenAccess {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private OAuthTokenStore tokenStore;
+ private OAuthTokenRefresher tokenRefresher;
+
+ @Activate
+ public TokenAccessImpl(@Reference OAuthTokenStore tokenStore, @Reference OAuthTokenRefresher tokenRefresher) {
+ this.tokenStore = tokenStore;
+ this.tokenRefresher = tokenRefresher;
+ }
+
+ @Override
+ public OAuthTokenResponse getAccessToken(ClientConnection connection, SlingHttpServletRequest request, String redirectPath) {
+
+ ResourceResolver resolver = request.getResourceResolver();
+
+ OAuthToken token = tokenStore.getAccessToken(connection, resolver);
+
+ if ( logger.isDebugEnabled() )
+ logger.debug("Accessing token for connection {} and user {}", connection.name(), request.getUserPrincipal());
+
+ // valid access token present -> return token
+ if (token.getState() == TokenState.VALID) {
+ if (logger.isDebugEnabled())
+ logger.debug("Returning valid access token for connection {} and user {}", connection.name(), request.getUserPrincipal());
+
+ return new OAuthTokenResponse(Optional.of(token.getValue()), connection, request, redirectPath);
+ }
+
+ // expired token but refresh token present -> refresh and return
+ if (token.getState() == TokenState.EXPIRED) {
+ OAuthToken refreshToken = tokenStore.getRefreshToken(connection, resolver);
+ if (refreshToken.getState() == TokenState.VALID) {
+ if (logger.isDebugEnabled())
+ logger.debug("Refreshing expired access token for connection {} and user {}", connection.name(), request.getUserPrincipal());
+
+ OAuthTokens newTokens = tokenRefresher.refreshTokens(connection, refreshToken.getValue());
+ tokenStore.persistTokens(connection, resolver, newTokens);
+ return new OAuthTokenResponse(Optional.of(newTokens.accessToken()), connection, request, redirectPath);
+ }
+ }
+
+ // all other scenarios -> redirect
+ if ( logger.isDebugEnabled() )
+ logger.debug("No valid access token found for connection {} and user {}", connection.name(), request.getUserPrincipal());
+
+ return new OAuthTokenResponse(Optional.empty(), connection, request, redirectPath);
+ }
+
+ @Override
+ public OAuthTokenResponse clearAccessToken(ClientConnection connection, SlingHttpServletRequest request, String redirectPath) {
+
+ if ( logger.isDebugEnabled() )
+ logger.debug("Clearing access token for connection {} and user {}", connection.name(), request.getUserPrincipal());
+
+ tokenStore.clearAccessToken(connection, request.getResourceResolver());
+
+ return new OAuthTokenResponse(Optional.empty(), connection, request, redirectPath);
+ }
+
+ @Override
+ public void clearAccessToken(ClientConnection connection, ResourceResolver resolver) {
+
+ if ( logger.isDebugEnabled() )
+ logger.debug("Clearing access token for connection {} and user {}", connection.name(), resolver.getUserID());
+
+ tokenStore.clearAccessToken(connection, resolver);
+ }
+}
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 d92f50d..2b61046 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
@@ -27,10 +27,8 @@ import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.auth.oauth_client.ClientConnection;
import org.apache.sling.auth.oauth_client.OAuthToken;
-import org.apache.sling.auth.oauth_client.OAuthTokenRefresher;
-import org.apache.sling.auth.oauth_client.OAuthTokenStore;
-import org.apache.sling.auth.oauth_client.OAuthTokens;
-import org.apache.sling.auth.oauth_client.OAuthUris;
+import org.apache.sling.auth.oauth_client.OAuthTokenAccess;
+import org.apache.sling.auth.oauth_client.OAuthTokenResponse;
import org.apache.sling.auth.oauth_client.TokenState;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@@ -43,14 +41,11 @@ public abstract class OAuthEnabledSlingServlet extends SlingSafeMethodsServlet {
private final ClientConnection connection;
- private final OAuthTokenStore tokenStore;
-
- private final OAuthTokenRefresher oidcClient;
+ private final OAuthTokenAccess tokenAccess;
- protected OAuthEnabledSlingServlet(ClientConnection connection, OAuthTokenStore tokenStore, OAuthTokenRefresher oidcClient) {
+ protected OAuthEnabledSlingServlet(ClientConnection connection, OAuthTokenAccess tokenAccess) {
this.connection = Objects.requireNonNull(connection, "connection may not null");
- this.tokenStore = Objects.requireNonNull(tokenStore, "tokenStore may not null");
- this.oidcClient = Objects.requireNonNull(oidcClient, "oidcClient may not null");
+ this.tokenAccess = Objects.requireNonNull(tokenAccess, "tokenAccess may not null");
}
@Override
@@ -67,26 +62,11 @@ public abstract class OAuthEnabledSlingServlet extends SlingSafeMethodsServlet {
if ( logger.isDebugEnabled() )
logger.debug("Configured with connection (name={}) and redirectPath={}", connection.name(), redirectPath);
- OAuthToken tokenResponse = tokenStore.getAccessToken(connection, request.getResourceResolver());
-
- switch ( tokenResponse.getState() ) {
- case VALID:
- doGetWithPossiblyInvalidToken(request, response, tokenResponse, redirectPath);
- break;
- case MISSING:
- response.sendRedirect(OAuthUris.getOidcEntryPointUri(connection, request, redirectPath).toString());
- break;
- case EXPIRED:
- OAuthToken refreshToken = tokenStore.getRefreshToken(connection, request.getResourceResolver());
- if ( refreshToken.getState() != TokenState.VALID ) {
- response.sendRedirect(OAuthUris.getOidcEntryPointUri(connection, request, redirectPath).toString());
- return;
- }
-
- OAuthTokens oidcTokens = oidcClient.refreshTokens(connection, refreshToken.getValue());
- tokenStore.persistTokens(connection, request.getResourceResolver(), oidcTokens);
- doGetWithPossiblyInvalidToken(request, response, new OAuthToken(TokenState.VALID, oidcTokens.accessToken()), redirectPath);
- break;
+ 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());
}
}
@@ -95,9 +75,9 @@ public abstract class OAuthEnabledSlingServlet extends SlingSafeMethodsServlet {
doGetWithToken(request, response, token);
} catch (ServletException | IOException e) {
if (isInvalidAccessTokenException(e)) {
- logger.warn("Invalid access token, clearing and attempting to retrieve a fresh one", e);
- tokenStore.clearAccessToken(connection, request.getResourceResolver());
- response.sendRedirect(OAuthUris.getOidcEntryPointUri(connection, request, redirectPath).toString());
+ logger.warn("Invalid access token, clearing restarting OAuth flow", e);
+ OAuthTokenResponse tokenResponse = tokenAccess.clearAccessToken(connection, request, getRedirectPath(request));
+ response.sendRedirect(tokenResponse.getRedirectUri().toString());
} else {
throw e;
}
diff --git a/src/test/java/org/apache/sling/auth/oauth_client/TokenAccessImplTest.java b/src/test/java/org/apache/sling/auth/oauth_client/TokenAccessImplTest.java
new file mode 100644
index 0000000..0f17245
--- /dev/null
+++ b/src/test/java/org/apache/sling/auth/oauth_client/TokenAccessImplTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.sling.auth.oauth_client.impl.MockOidcConnection;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(SlingContextExtension.class)
+class TokenAccessImplTest {
+
+ private SlingContext slingContext = new SlingContext();
+
+ @Test
+ void missingAccessToken() {
+
+ OAuthTokenStore tokenStore = new InMemoryOAuthTokenStore();
+
+ TokenAccessImpl tokenAccess = new TokenAccessImpl(tokenStore, null);
+
+ OAuthTokenResponse tokenResponse = tokenAccess.getAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/");
+
+ assertThat(tokenResponse)
+ .as("tokenResponse")
+ .isNotNull()
+ .satisfies( tr -> {
+ assertThat(tr.hasValidToken()).as("hasValidToken").isFalse();
+ assertThrows(IllegalStateException.class, tr::getTokenValue, "getTokenValue");
+ assertThat(tr.getRedirectUri()).as("redirectUri")
+ .isNotNull()
+ .asString()
+ .isNotBlank();
+ });
+ }
+
+ @Test
+ void presentAccessToken() {
+ OAuthTokenStore tokenStore = new InMemoryOAuthTokenStore();
+
+ TokenAccessImpl tokenAccess = new TokenAccessImpl(tokenStore, null);
+
+ tokenStore.persistTokens(MockOidcConnection.DEFAULT_CONNECTION, slingContext.resourceResolver(), new OAuthTokens("access", 0, null));
+
+ OAuthTokenResponse tokenResponse = tokenAccess.getAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/");
+
+ assertThat(tokenResponse)
+ .as("tokenResponse")
+ .isNotNull()
+ .satisfies( tr -> {
+ assertThat(tr.hasValidToken()).as("hasValidToken").isTrue();
+ assertThat(tr.getTokenValue()).as("tokenValue").isEqualTo("access");
+ assertThrows(IllegalStateException.class, tr::getRedirectUri, "getRedirectUri");
+ });
+ }
+
+ @Test
+ void refreshTokenUsed() {
+
+ OAuthTokens expiredTokens = new OAuthTokens("access", -1, "refresh");
+ OAuthTokens refreshedTokens = new OAuthTokens("access2", 0, null);
+
+ OAuthTokenStore tokenStore = new InMemoryOAuthTokenStore();
+
+ OAuthTokenRefresher tokenRefresher = new OAuthTokenRefresher() {
+ @Override
+ public OAuthTokens refreshTokens(ClientConnection connection, String refreshToken) {
+ if (!refreshToken.equals(expiredTokens.refreshToken()))
+ throw new IllegalArgumentException("Invalid refresh token");
+
+ return refreshedTokens;
+ }
+ };
+
+ TokenAccessImpl tokenAccess = new TokenAccessImpl(tokenStore, tokenRefresher);
+
+ tokenStore.persistTokens(MockOidcConnection.DEFAULT_CONNECTION, slingContext.resourceResolver(), expiredTokens);
+
+ OAuthTokenResponse tokenResponse = tokenAccess.getAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/");
+
+ assertThat(tokenResponse)
+ .as("tokenResponse")
+ .isNotNull()
+ .satisfies( tr -> {
+ assertThat(tr.hasValidToken()).as("hasValidToken").isTrue();
+ assertThat(tr.getTokenValue()).as("tokenValue").isEqualTo(refreshedTokens.accessToken());
+ assertThrows(IllegalStateException.class, tr::getRedirectUri, "getRedirectUri");
+ });
+ }
+
+ @Test
+ void clearAccessTokenWithResponse() {
+
+ OAuthTokenStore tokenStore = new InMemoryOAuthTokenStore();
+
+ TokenAccessImpl tokenAccess = new TokenAccessImpl(tokenStore, null);
+
+ tokenStore.persistTokens(MockOidcConnection.DEFAULT_CONNECTION, slingContext.resourceResolver(), new OAuthTokens("access", 0, null));
+
+ OAuthTokenResponse okResponse = tokenAccess.getAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/");
+ assertThat(okResponse.hasValidToken()).isTrue();
+
+ OAuthTokenResponse clearResponse = tokenAccess.clearAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/");
+
+ assertThat(clearResponse)
+ .as("tokenResponse after clear")
+ .isNotNull()
+ .extracting(OAuthTokenResponse::hasValidToken)
+ .isEqualTo(false);
+ }
+
+ @Test
+ void clearAccessTokenWithoutResponse() {
+
+ InMemoryOAuthTokenStore tokenStore = new InMemoryOAuthTokenStore();
+
+ TokenAccessImpl tokenAccess = new TokenAccessImpl(tokenStore, null);
+
+ tokenStore.persistTokens(MockOidcConnection.DEFAULT_CONNECTION, slingContext.resourceResolver(), new OAuthTokens("access", 0, null));
+
+ OAuthTokenResponse okResponse = tokenAccess.getAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/");
+ assertThat(okResponse.hasValidToken()).isTrue();
+
+ tokenAccess.clearAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.resourceResolver());
+
+ assertThat(tokenStore.allTokens())
+ .as("all persisted tokens")
+ .isEmpty();
+ }
+
+}