This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch issue/high-level-api in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-auth-oauth-client.git
commit 2f1f9efff04e1f43fc17a2b7d281601b359e3bd9 Author: Robert Munteanu <[email protected]> AuthorDate: Tue Dec 10 11:32:21 2024 +0100 feat: introduce a high-level API for retrieving access tokens --- README.md | 98 ++++++++++++-- .../sling/auth/oauth_client/TokenAccess.java | 70 ++++++++++ .../sling/auth/oauth_client/TokenAccessImpl.java | 100 ++++++++++++++ .../sling/auth/oauth_client/TokenResponse.java | 84 ++++++++++++ .../support/OAuthEnabledSlingServlet.java | 46 ++----- .../auth/oauth_client/TokenAccessImplTest.java | 149 +++++++++++++++++++++ 6 files changed, 500 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index d64fa0d..75687fe 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 `TokenAccess` 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 TokenAccess tokenAccess; + + private TokenResponse 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 TokenAccess 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 TokenAccess tokenAccess; + @SlingObject SlingHttpServletRequest request; + @OSGiService(filter = "(name=foo)") private ClientConnection connection; + + public String getLink() { + // code elided + if ( accessTokenIsInvalid() ) { + TokenResponse 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 TokenAccess 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/TokenAccess.java b/src/main/java/org/apache/sling/auth/oauth_client/TokenAccess.java new file mode 100644 index 0000000..3ae7aca --- /dev/null +++ b/src/main/java/org/apache/sling/auth/oauth_client/TokenAccess.java @@ -0,0 +1,70 @@ +/* + * 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 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()}. </p> + * + * <p>The storage strategy may vary and is controlled by the currently active implementation of the {@link OAuthTokenStore}.</p> + */ +@NotNull +public interface TokenAccess { + + /** + * 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 + */ + TokenResponse 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 + */ + TokenResponse 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/TokenAccessImpl.java b/src/main/java/org/apache/sling/auth/oauth_client/TokenAccessImpl.java new file mode 100644 index 0000000..6eaf978 --- /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 TokenAccess { + + 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 TokenResponse 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 TokenResponse(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 TokenResponse(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 TokenResponse(Optional.empty(), connection, request, redirectPath); + } + + @Override + public TokenResponse 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 TokenResponse(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/TokenResponse.java b/src/main/java/org/apache/sling/auth/oauth_client/TokenResponse.java new file mode 100644 index 0000000..9464522 --- /dev/null +++ b/src/main/java/org/apache/sling/auth/oauth_client/TokenResponse.java @@ -0,0 +1,84 @@ +/* + * 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: + * <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> + * + * <p>Methods generally throw {@link IllegalStateException} if they are called in an unexpected state and do not return null values.</p> + */ +@NotNull +public class TokenResponse { + + private final Optional<String> token; + private final ClientConnection connection; + private final SlingHttpServletRequest request; + private String redirectPath; + + public TokenResponse(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/support/OAuthEnabledSlingServlet.java b/src/main/java/org/apache/sling/auth/oauth_client/support/OAuthEnabledSlingServlet.java index d92f50d..6c6d838 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.TokenAccess; +import org.apache.sling.auth.oauth_client.TokenResponse; 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 TokenAccess tokenAccess; - protected OAuthEnabledSlingServlet(ClientConnection connection, OAuthTokenStore tokenStore, OAuthTokenRefresher oidcClient) { + protected OAuthEnabledSlingServlet(ClientConnection connection, TokenAccess 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; + TokenResponse 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); + TokenResponse 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..24401c4 --- /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); + + TokenResponse 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)); + + TokenResponse 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); + + TokenResponse 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)); + + TokenResponse okResponse = tokenAccess.getAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/"); + assertThat(okResponse.hasValidToken()).isTrue(); + + TokenResponse clearResponse = tokenAccess.clearAccessToken(MockOidcConnection.DEFAULT_CONNECTION, slingContext.request(), "/"); + + assertThat(clearResponse) + .as("tokenResponse after clear") + .isNotNull() + .extracting(TokenResponse::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)); + + TokenResponse 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(); + } + +}
