From 9e65b1ee8ecb43a505657f2d77c3a42c8b8cdece Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Thu, 24 Oct 2024 08:15:49 +0200 Subject: [PATCH 01/12] refactor: use new HttpSecurityPolicy interface BREAKING CHANGE: requires APIM 4.6+ --- .circleci/config.yml | 2 +- .github/pull_request_template.md | 12 ++--- .github/renovate.json | 24 +--------- pom.xml | 47 +++---------------- .../gravitee/policy/oauth2/Oauth2Policy.java | 42 ++++++++++------- .../policy/oauth2/utils/TokenExtractor.java | 8 ++-- .../policy/oauth2/Oauth2PolicyTest.java | 19 ++++---- 7 files changed, 51 insertions(+), 103 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b37b6814..1c473006 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ version: 2.1 setup: true orbs: - gravitee: gravitee-io/gravitee@4.1.1 + gravitee: gravitee-io/gravitee@4.8.0 # our single workflow, that triggers the setup job defined above, filters on tag and branches are needed otherwise # some workflow and job will not be triggered for tags (default CircleCI behavior) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5acc9a25..665f40fa 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,9 @@ -**Issue** +## Issue -https://gravitee.atlassian.net/browse/APIM-XXXX +https://gravitee.atlassian.net/browse/XXXXX -**Description** +## Description A small description of what you did in that PR. -**Additional context** - - - - +## Additional context \ No newline at end of file diff --git a/.github/renovate.json b/.github/renovate.json index c41f20b5..4ac11dc9 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,25 +1,3 @@ { - "extends": ["config:base", ":label(dependencies)"], - "rebaseWhen": "conflicted", - "packageRules": [ - { - "matchDatasources": ["orb"], - "matchUpdateTypes": ["patch", "minor"], - "automerge": true, - "automergeType": "branch", - "semanticCommitType": "ci" - }, - { - "matchDepTypes": ["provided", "test", "build", "import", "parent"], - "matchUpdateTypes": ["patch", "minor"], - "automerge": true, - "automergeType": "branch", - "semanticCommitType": "chore" - }, - { - "matchDepTypes": ["provided", "test", "build", "import", "parent"], - "matchUpdateTypes": ["major"], - "semanticCommitType": "chore" - } - ] + "extends": ["github>gravitee-io/renovate-config:policy"] } diff --git a/pom.xml b/pom.xml index 8a037262..95f92f99 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ io.gravitee.policy gravitee-policy-oauth2 - 3.0.5 + 4.0.0-APIM-7143-impl-security-chain-SNAPSHOT Gravitee.io APIM - Policy - OAuth2 Check access token validity during request processing using token introspection @@ -30,21 +30,11 @@ io.gravitee gravitee-parent - 22.0.2 + 22.2.2 - 6.0.3 - 3.2.1 - 1.11.0 - 4.0.0 - 2.1.1 - 4.0.0 - 1.1.0 - 1.4.0 - 1.4.0 - 9.15.2 - 31.1-jre + 4.6.0-SNAPSHOT 3.6.0 1.1.0 @@ -57,32 +47,12 @@ - io.gravitee - gravitee-bom - ${gravitee-bom.version} + io.gravitee.apim + gravitee-apim-bom + ${gravitee-apim.version} import pom - - io.gravitee.policy - gravitee-policy-api - ${gravitee-policy-api.version} - - - io.gravitee.gateway - gravitee-gateway-api - ${gravitee-gateway-api.version} - - - io.gravitee.resource - gravitee-resource-api - ${gravitee-resource-api.version} - - - io.gravitee.common - gravitee-common - ${gravitee-common.version} - @@ -111,13 +81,11 @@ io.gravitee.resource gravitee-resource-oauth2-provider-api - ${gravitee-resource-oauth2-provider-api.version} provided io.gravitee.resource gravitee-resource-cache-provider-api - ${gravitee-resource-cache-provider-api.version} provided @@ -149,7 +117,7 @@ com.nimbusds nimbus-jose-jwt - ${nimbus-jose-jwt.version} + provided @@ -182,7 +150,6 @@ com.google.guava guava - ${guava.version} test diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index f6609ca8..69fb6945 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -23,9 +23,10 @@ import io.gravitee.common.security.jwt.LazyJWT; import io.gravitee.gateway.api.http.HttpHeaderNames; import io.gravitee.gateway.reactive.api.ExecutionFailure; -import io.gravitee.gateway.reactive.api.context.HttpExecutionContext; -import io.gravitee.gateway.reactive.api.policy.SecurityPolicy; +import io.gravitee.gateway.reactive.api.context.GenericExecutionContext; +import io.gravitee.gateway.reactive.api.context.http.HttpPlainExecutionContext; import io.gravitee.gateway.reactive.api.policy.SecurityToken; +import io.gravitee.gateway.reactive.api.policy.http.HttpSecurityPolicy; import io.gravitee.policy.api.annotations.RequireResource; import io.gravitee.policy.oauth2.configuration.OAuth2PolicyConfiguration; import io.gravitee.policy.oauth2.introspection.TokenIntrospectionCache; @@ -52,7 +53,7 @@ * @author GraviteeSource Team */ @RequireResource -public class Oauth2Policy extends Oauth2PolicyV3 implements SecurityPolicy { +public class Oauth2Policy extends Oauth2PolicyV3 implements HttpSecurityPolicy { public static final String CONTEXT_ATTRIBUTE_JWT = "jwt"; public static final String CONTEXT_ATTRIBUTE_TOKEN = CONTEXT_ATTRIBUTE_PREFIX + "token"; @@ -87,7 +88,7 @@ public boolean requireSubscription() { } @Override - public Maybe extractSecurityToken(HttpExecutionContext ctx) { + public Maybe extractSecurityToken(HttpPlainExecutionContext ctx) { final OAuth2Resource oauth2Resource = getOauth2Resource(ctx); if (oauth2Resource == null) { log.debug("Skipping security token extraction cause no oauth2 resource configured"); @@ -108,11 +109,11 @@ public Maybe extractSecurityToken(HttpExecutionContext ctx) { } @Override - public Completable onRequest(final HttpExecutionContext ctx) { + public Completable onRequest(final HttpPlainExecutionContext ctx) { return handleSecurity(ctx); } - private Completable handleSecurity(final HttpExecutionContext ctx) { + private Completable handleSecurity(final HttpPlainExecutionContext ctx) { return Completable .defer(() -> { log.debug("Read access_token from request {}", ctx.request().id()); @@ -153,7 +154,7 @@ private Completable handleSecurity(final HttpExecutionContext ctx) { * @param canUseCache allow retrieval of a previously extracted token from the request context cache * @return JWT token, or empty if no token found. */ - private Maybe extractAccessToken(HttpExecutionContext ctx, boolean canUseCache) { + private Maybe extractAccessToken(HttpPlainExecutionContext ctx, boolean canUseCache) { return Maybe .defer(() -> { LazyJWT jwt = canUseCache ? ctx.getAttribute(CONTEXT_ATTRIBUTE_JWT) : null; @@ -171,7 +172,7 @@ private Maybe extractAccessToken(HttpExecutionContext ctx, boolean canUs } private Completable validateOAuth2Payload( - HttpExecutionContext ctx, + HttpPlainExecutionContext ctx, TokenIntrospectionResult tokenIntrospectionResult, OAuth2Resource oauth2Resource ) { @@ -217,7 +218,7 @@ private Completable validateOAuth2Payload( * @return OAuth2Response */ protected Single introspectAccessToken( - HttpExecutionContext ctx, + HttpPlainExecutionContext ctx, String accessToken, OAuth2Resource oauth2Resource ) { @@ -247,7 +248,11 @@ protected Single introspectAccessToken( ); } - private Completable introspectAndValidateAccessToken(HttpExecutionContext ctx, String accessToken, OAuth2Resource oauth2Resource) { + private Completable introspectAndValidateAccessToken( + HttpPlainExecutionContext ctx, + String accessToken, + OAuth2Resource oauth2Resource + ) { return introspectAccessToken(ctx, accessToken, oauth2Resource) .flatMapCompletable(introspectionResult -> { if (introspectionResult.isSuccess()) { @@ -278,7 +283,7 @@ private Completable introspectAndValidateAccessToken(HttpExecutionContext ctx, S * error="invalid_token", * error_description="The access token expired" */ - private Completable sendError(HttpExecutionContext ctx, String responseKey) { + private Completable sendError(HttpPlainExecutionContext ctx, String responseKey) { String headerValue = BEARER_AUTHORIZATION_TYPE + " realm=\"gravitee.io\""; ctx.response().headers().add(HttpHeaderNames.WWW_AUTHENTICATE, headerValue); @@ -289,10 +294,10 @@ private Completable sendError(HttpExecutionContext ctx, String responseKey) { /** * Get Oauth2 resource configured at policy level. * - * @param ctx HttpExecutionContext + * @param ctx HttpPlainExecutionContext * @return OAuth2Resource */ - private OAuth2Resource getOauth2Resource(HttpExecutionContext ctx) { + private OAuth2Resource getOauth2Resource(HttpPlainExecutionContext ctx) { if (oAuth2PolicyConfiguration.getOauthResource() == null) { return null; } @@ -308,16 +313,17 @@ private OAuth2Resource getOauth2Resource(HttpExecutionContext ctx) { /** * Get cache configured at policy level. * - * @param ctx HttpExecutionContext + * @param ctx HttpPlainExecutionContext * @return Cache */ - private Cache getPolicyTokenIntrospectionCache(HttpExecutionContext ctx) { + private Cache getPolicyTokenIntrospectionCache(HttpPlainExecutionContext ctx) { if (oAuth2PolicyConfiguration.getOauthCacheResource() != null) { CacheResource cacheResource = ctx .getComponent(ResourceManager.class) .getResource(oAuth2PolicyConfiguration.getOauthCacheResource(), CacheResource.class); if (cacheResource != null) { - return cacheResource.getCache(ctx); + // The cast is working because the context instance that is sent by the gateway implements both interfaces + return cacheResource.getCache((GenericExecutionContext) ctx); } } return null; @@ -326,10 +332,10 @@ private Cache getPolicyTokenIntrospectionCache(HttpExecutionContext ctx) { /** * Get token introspection cache from request context. * - * @param ctx HttpExecutionContext + * @param ctx HttpPlainExecutionContext * @return TokenIntrospectionCache */ - private TokenIntrospectionCache getContextTokenIntrospectionCache(HttpExecutionContext ctx) { + private TokenIntrospectionCache getContextTokenIntrospectionCache(HttpPlainExecutionContext ctx) { TokenIntrospectionCache cache = ctx.getInternalAttribute(ATTR_INTERNAL_TOKEN_INTROSPECTIONS); if (cache == null) { cache = new TokenIntrospectionCache(); diff --git a/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java b/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java index d6883db3..99a744bc 100644 --- a/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java +++ b/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java @@ -16,10 +16,10 @@ package io.gravitee.policy.oauth2.utils; import io.gravitee.common.util.MultiValueMap; +import io.gravitee.gateway.api.Request; import io.gravitee.gateway.api.http.HttpHeaderNames; import io.gravitee.gateway.api.http.HttpHeaders; -import io.gravitee.gateway.reactive.api.context.HttpRequest; -import io.gravitee.gateway.reactive.api.context.Request; +import io.gravitee.gateway.reactive.api.context.http.HttpPlainRequest; import java.util.List; import java.util.Optional; import org.springframework.util.ObjectUtils; @@ -48,7 +48,7 @@ public class TokenExtractor { * * @return the access token as string, {@link Optional#empty()} if no token has been found. */ - public static Optional extract(HttpRequest request) { + public static Optional extract(HttpPlainRequest request) { return extractFromHeaders(request.headers()).or(() -> extractFromParameters(request.parameters())); } @@ -57,7 +57,7 @@ public static Optional extract(HttpRequest request) { * * @param request the request to extract the JWT token from. * @return the access token as string or null if no token has been found. - * @see #extract(HttpRequest) + * @see #extract(HttpPlainRequest) */ @Deprecated public static String extract(io.gravitee.gateway.api.Request request) { diff --git a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java index a493af45..65466193 100644 --- a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java +++ b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java @@ -54,9 +54,10 @@ import io.gravitee.gateway.api.http.HttpHeaderNames; import io.gravitee.gateway.api.http.HttpHeaders; import io.gravitee.gateway.reactive.api.ExecutionFailure; -import io.gravitee.gateway.reactive.api.context.HttpExecutionContext; -import io.gravitee.gateway.reactive.api.context.Request; -import io.gravitee.gateway.reactive.api.context.Response; +import io.gravitee.gateway.reactive.api.context.GenericExecutionContext; +import io.gravitee.gateway.reactive.api.context.http.HttpPlainExecutionContext; +import io.gravitee.gateway.reactive.api.context.http.HttpPlainRequest; +import io.gravitee.gateway.reactive.api.context.http.HttpPlainResponse; import io.gravitee.gateway.reactive.api.policy.SecurityToken; import io.gravitee.gateway.reactive.core.context.DefaultExecutionContext; import io.gravitee.gateway.reactive.core.context.MutableRequest; @@ -107,13 +108,13 @@ class Oauth2PolicyTest { private OAuth2PolicyConfiguration configuration; @Mock - private Request request; + private HttpPlainRequest request; @Mock - private Response response; + private HttpPlainResponse response; - @Mock - private HttpExecutionContext ctx; + @Mock(extraInterfaces = GenericExecutionContext.class) + private HttpPlainExecutionContext ctx; @Mock private ResourceManager resourceManager; @@ -435,7 +436,7 @@ void shouldPutIntrospectionToCache() throws IOException { private void prepareCacheResource() { when(configuration.getOauthCacheResource()).thenReturn(OAUTH_CACHE_RESOURCE); - when(cacheResource.getCache(any(HttpExecutionContext.class))).thenReturn(cache); + when(cacheResource.getCache(any(GenericExecutionContext.class))).thenReturn(cache); when(resourceManager.getResource(OAUTH_CACHE_RESOURCE, CacheResource.class)).thenReturn(cacheResource); } @@ -565,7 +566,7 @@ void shouldIntrospectOnlyOnce() throws IOException { final String payload = readJsonResource("/io/gravitee/policy/oauth2/oauth2-response09.json").toString(); prepareIntrospection(token, payload, true); - HttpExecutionContext ctx = new DefaultExecutionContext(mock(MutableRequest.class), mock(MutableResponse.class)); + HttpPlainExecutionContext ctx = new DefaultExecutionContext(mock(MutableRequest.class), mock(MutableResponse.class)); TestObserver result1 = cut.introspectAccessToken(ctx, token, oAuth2Resource).test(); TestObserver result2 = cut.introspectAccessToken(ctx, token, oAuth2Resource).test(); From a5a87a8367a9c48b2863488efba85a737842892e Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Fri, 25 Oct 2024 14:20:31 +0200 Subject: [PATCH 02/12] feat: implement kafka security policy https://gravitee.atlassian.net/browse/APIM-7143 --- pom.xml | 6 + .../gravitee/policy/oauth2/Oauth2Policy.java | 185 ++++++++++++------ .../TokenIntrospectionResult.java | 12 ++ .../policy/oauth2/utils/TokenExtractor.java | 45 ++++- .../policy/oauth2/Oauth2PolicyTest.java | 3 +- 5 files changed, 187 insertions(+), 64 deletions(-) diff --git a/pom.xml b/pom.xml index 95f92f99..6b085b06 100644 --- a/pom.xml +++ b/pom.xml @@ -120,6 +120,12 @@ provided + + org.apache.kafka + kafka-clients + provided + + io.gravitee.apim.gateway diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index 69fb6945..2d9bfe6f 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -23,10 +23,12 @@ import io.gravitee.common.security.jwt.LazyJWT; import io.gravitee.gateway.api.http.HttpHeaderNames; import io.gravitee.gateway.reactive.api.ExecutionFailure; -import io.gravitee.gateway.reactive.api.context.GenericExecutionContext; +import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext; import io.gravitee.gateway.reactive.api.context.http.HttpPlainExecutionContext; +import io.gravitee.gateway.reactive.api.context.kafka.KafkaConnectionContext; import io.gravitee.gateway.reactive.api.policy.SecurityToken; import io.gravitee.gateway.reactive.api.policy.http.HttpSecurityPolicy; +import io.gravitee.gateway.reactive.api.policy.kafka.KafkaSecurityPolicy; import io.gravitee.policy.api.annotations.RequireResource; import io.gravitee.policy.oauth2.configuration.OAuth2PolicyConfiguration; import io.gravitee.policy.oauth2.introspection.TokenIntrospectionCache; @@ -45,6 +47,11 @@ import io.reactivex.rxjava3.core.Single; import java.util.List; import java.util.Optional; +import java.util.Set; +import javax.security.auth.callback.Callback; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.kafka.common.security.oauthbearer.internals.secured.BasicOAuthBearerToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,12 +60,13 @@ * @author GraviteeSource Team */ @RequireResource -public class Oauth2Policy extends Oauth2PolicyV3 implements HttpSecurityPolicy { +public class Oauth2Policy extends Oauth2PolicyV3 implements HttpSecurityPolicy, KafkaSecurityPolicy { public static final String CONTEXT_ATTRIBUTE_JWT = "jwt"; public static final String CONTEXT_ATTRIBUTE_TOKEN = CONTEXT_ATTRIBUTE_PREFIX + "token"; public static final String ATTR_INTERNAL_TOKEN_INTROSPECTIONS = "token-introspection-cache"; + public static final String ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT = "token-introspection-result"; private static final Logger log = LoggerFactory.getLogger(Oauth2Policy.class); public Oauth2Policy(OAuth2PolicyConfiguration oAuth2PolicyConfiguration) { @@ -89,13 +97,72 @@ public boolean requireSubscription() { @Override public Maybe extractSecurityToken(HttpPlainExecutionContext ctx) { + return getSecurityTokenFromContext(ctx); + } + + @Override + public Maybe extractSecurityToken(KafkaConnectionContext ctx) { + return getSecurityTokenFromContext(ctx); + } + + @Override + public Completable onRequest(final HttpPlainExecutionContext ctx) { + return Completable + .defer(() -> { + log.debug("Read access_token from request {}", ctx.request().id()); + return handleSecurity(ctx); + }) + .andThen( + Completable.fromRunnable(() -> { + if (!oAuth2PolicyConfiguration.isPropagateAuthHeader()) { + ctx.request().headers().remove(HttpHeaderNames.AUTHORIZATION); + } + }) + ) + .doAfterTerminate(() -> ctx.removeInternalAttribute(ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT)); + } + + @Override + public Completable authenticate(KafkaConnectionContext ctx) { + return handleSecurity(ctx) + .andThen( + Completable.fromRunnable(() -> { + Callback[] callbacks = ctx.callbacks(); + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerValidatorCallback oauthCallback) { + String extractedToken = ctx.getAttribute(CONTEXT_ATTRIBUTE_TOKEN); + String user = ctx.getAttribute(ATTR_USER); + TokenIntrospectionResult tokenIntrospectionResult = ctx.getInternalAttribute( + ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT + ); + + Long expirationTime = tokenIntrospectionResult.getExpirationTime(); + Long issueTime = tokenIntrospectionResult.getIssuedAtTime(); + + OAuthBearerToken token = new BasicOAuthBearerToken( + extractedToken, + Set.of(), // Scopes are fully managed by Gravitee, it is useless to extract & provide them to the Kafka security context. + (expirationTime == null ? Long.MAX_VALUE : expirationTime), + user, + issueTime + ); + + oauthCallback.token(token); + } + } + }) + ) + .doAfterTerminate(() -> ctx.removeInternalAttribute(ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT)); + } + + private Maybe getSecurityTokenFromContext(BaseExecutionContext ctx) { final OAuth2Resource oauth2Resource = getOauth2Resource(ctx); if (oauth2Resource == null) { log.debug("Skipping security token extraction cause no oauth2 resource configured"); return Maybe.empty(); } - return extractAccessToken(ctx, true) + return fetchJWTToken(ctx, true) .flatMap(token -> introspectAccessToken(ctx, token, oauth2Resource).toMaybe()) .flatMap(introspectionResult -> { if (introspectionResult.hasClientId()) { @@ -108,41 +175,30 @@ public Maybe extractSecurityToken(HttpPlainExecutionContext ctx) }); } - @Override - public Completable onRequest(final HttpPlainExecutionContext ctx) { - return handleSecurity(ctx); - } + private Completable handleSecurity(final BaseExecutionContext ctx) { + final OAuth2Resource oauth2Resource = getOauth2Resource(ctx); - private Completable handleSecurity(final HttpPlainExecutionContext ctx) { - return Completable - .defer(() -> { - log.debug("Read access_token from request {}", ctx.request().id()); - final OAuth2Resource oauth2Resource = getOauth2Resource(ctx); + if (oauth2Resource == null) { + return interruptWith( + ctx, + new ExecutionFailure(UNAUTHORIZED_401).key(OAUTH2_MISSING_SERVER_KEY).message(OAUTH2_UNAUTHORIZED_MESSAGE) + ); + } - if (oauth2Resource == null) { - return ctx.interruptWith( - new ExecutionFailure(UNAUTHORIZED_401).key(OAUTH2_MISSING_SERVER_KEY).message(OAUTH2_UNAUTHORIZED_MESSAGE) - ); + return fetchJWTToken(ctx, false) + .switchIfEmpty( + Maybe.defer(() -> sendError(ctx, UNAUTHORIZED_401, OAUTH2_MISSING_HEADER_KEY, OAUTH2_UNAUTHORIZED_MESSAGE).toMaybe()) + ) + .flatMapCompletable(accessToken -> { + if (accessToken.isBlank()) { + return sendError(ctx, UNAUTHORIZED_401, OAUTH2_MISSING_ACCESS_TOKEN_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); } - return extractAccessToken(ctx, false) - .switchIfEmpty(Maybe.defer(() -> sendError(ctx, OAUTH2_MISSING_HEADER_KEY).toMaybe())) - .flatMapCompletable(accessToken -> { - if (accessToken.isBlank()) { - return sendError(ctx, OAUTH2_MISSING_ACCESS_TOKEN_KEY); - } + // Set access_token in context + ctx.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN, accessToken); - // Set access_token in context - ctx.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_ACCESS_TOKEN, accessToken); - - // Introspect and validate access token - return introspectAndValidateAccessToken(ctx, accessToken, oauth2Resource); - }); - }) - .doOnTerminate(() -> { - if (!oAuth2PolicyConfiguration.isPropagateAuthHeader()) { - ctx.request().headers().remove(HttpHeaderNames.AUTHORIZATION); - } + // Introspect and validate access token + return introspectAndValidateAccessToken(ctx, accessToken, oauth2Resource); }); } @@ -154,12 +210,12 @@ private Completable handleSecurity(final HttpPlainExecutionContext ctx) { * @param canUseCache allow retrieval of a previously extracted token from the request context cache * @return JWT token, or empty if no token found. */ - private Maybe extractAccessToken(HttpPlainExecutionContext ctx, boolean canUseCache) { + private Maybe fetchJWTToken(BaseExecutionContext ctx, boolean canUseCache) { return Maybe .defer(() -> { LazyJWT jwt = canUseCache ? ctx.getAttribute(CONTEXT_ATTRIBUTE_JWT) : null; if (jwt == null) { - Optional token = TokenExtractor.extract(ctx.request()); + Optional token = TokenExtractor.extract(ctx); if (token.isEmpty()) { return Maybe.empty(); } @@ -172,12 +228,12 @@ private Maybe extractAccessToken(HttpPlainExecutionContext ctx, boolean } private Completable validateOAuth2Payload( - HttpPlainExecutionContext ctx, + BaseExecutionContext ctx, TokenIntrospectionResult tokenIntrospectionResult, OAuth2Resource oauth2Resource ) { if (!tokenIntrospectionResult.hasValidPayload()) { - return sendError(ctx, OAUTH2_INVALID_SERVER_RESPONSE_KEY); + return sendError(ctx, UNAUTHORIZED_401, OAUTH2_INVALID_SERVER_RESPONSE_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); } if (tokenIntrospectionResult.hasClientId()) { @@ -196,7 +252,7 @@ private Completable validateOAuth2Payload( // Check required scopes to access the resource if (oAuth2PolicyConfiguration.isCheckRequiredScopes()) { if (!hasRequiredScopes(scopes, oAuth2PolicyConfiguration.getRequiredScopes(), oAuth2PolicyConfiguration.isModeStrict())) { - return sendError(ctx, OAUTH2_INSUFFICIENT_SCOPE_KEY); + return sendError(ctx, UNAUTHORIZED_401, OAUTH2_INSUFFICIENT_SCOPE_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); } } @@ -218,7 +274,7 @@ private Completable validateOAuth2Payload( * @return OAuth2Response */ protected Single introspectAccessToken( - HttpPlainExecutionContext ctx, + BaseExecutionContext ctx, String accessToken, OAuth2Resource oauth2Resource ) { @@ -248,27 +304,21 @@ protected Single introspectAccessToken( ); } - private Completable introspectAndValidateAccessToken( - HttpPlainExecutionContext ctx, - String accessToken, - OAuth2Resource oauth2Resource - ) { + private Completable introspectAndValidateAccessToken(BaseExecutionContext ctx, String accessToken, OAuth2Resource oauth2Resource) { return introspectAccessToken(ctx, accessToken, oauth2Resource) .flatMapCompletable(introspectionResult -> { + ctx.setInternalAttribute(ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT, introspectionResult); if (introspectionResult.isSuccess()) { return validateOAuth2Payload(ctx, introspectionResult, oauth2Resource); } else { - ctx.response().headers().add(HttpHeaderNames.WWW_AUTHENTICATE, BEARER_AUTHORIZATION_TYPE + " realm=gravitee.io "); - if (introspectionResult.getOauth2ResponseThrowable() == null) { - return ctx.interruptWith( - new ExecutionFailure(UNAUTHORIZED_401).key(OAUTH2_INVALID_ACCESS_TOKEN_KEY).message(OAUTH2_UNAUTHORIZED_MESSAGE) - ); + return sendError(ctx, UNAUTHORIZED_401, OAUTH2_INVALID_ACCESS_TOKEN_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); } else { - return ctx.interruptWith( - new ExecutionFailure(SERVICE_UNAVAILABLE_503) - .key(OAUTH2_SERVER_UNAVAILABLE_KEY) - .message(OAUTH2_TEMPORARILY_UNAVAILABLE_MESSAGE) + return sendError( + ctx, + SERVICE_UNAVAILABLE_503, + OAUTH2_SERVER_UNAVAILABLE_KEY, + OAUTH2_TEMPORARILY_UNAVAILABLE_MESSAGE ); } } @@ -283,12 +333,24 @@ private Completable introspectAndValidateAccessToken( * error="invalid_token", * error_description="The access token expired" */ - private Completable sendError(HttpPlainExecutionContext ctx, String responseKey) { - String headerValue = BEARER_AUTHORIZATION_TYPE + " realm=\"gravitee.io\""; + private Completable sendError(BaseExecutionContext ctx, int errorStatus, String responseKey, String failureMessage) { + if (ctx instanceof HttpPlainExecutionContext httpPlainExecutionContext) { + String headerValue = BEARER_AUTHORIZATION_TYPE + " realm=\"gravitee.io\""; - ctx.response().headers().add(HttpHeaderNames.WWW_AUTHENTICATE, headerValue); + httpPlainExecutionContext.response().headers().add(HttpHeaderNames.WWW_AUTHENTICATE, headerValue); - return ctx.interruptWith(new ExecutionFailure(UNAUTHORIZED_401).key(responseKey).message(OAUTH2_UNAUTHORIZED_MESSAGE)); + return httpPlainExecutionContext.interruptWith(new ExecutionFailure(errorStatus).key(responseKey).message(failureMessage)); + } + // FIXME: Kafka Gateway - manage interruption with Kafka. + return Completable.error(new Exception(responseKey)); + } + + private Completable interruptWith(BaseExecutionContext ctx, ExecutionFailure failure) { + if (ctx instanceof HttpPlainExecutionContext httpPlainExecutionContext) { + return httpPlainExecutionContext.interruptWith(failure); + } + // FIXME: Kafka Gateway - manage interruption with Kafka. + return Completable.error(new Exception(failure.key())); } /** @@ -297,7 +359,7 @@ private Completable sendError(HttpPlainExecutionContext ctx, String responseKey) * @param ctx HttpPlainExecutionContext * @return OAuth2Resource */ - private OAuth2Resource getOauth2Resource(HttpPlainExecutionContext ctx) { + private OAuth2Resource getOauth2Resource(BaseExecutionContext ctx) { if (oAuth2PolicyConfiguration.getOauthResource() == null) { return null; } @@ -316,14 +378,13 @@ private OAuth2Resource getOauth2Resource(HttpPlainExecutionContext ctx) { * @param ctx HttpPlainExecutionContext * @return Cache */ - private Cache getPolicyTokenIntrospectionCache(HttpPlainExecutionContext ctx) { + private Cache getPolicyTokenIntrospectionCache(BaseExecutionContext ctx) { if (oAuth2PolicyConfiguration.getOauthCacheResource() != null) { CacheResource cacheResource = ctx .getComponent(ResourceManager.class) .getResource(oAuth2PolicyConfiguration.getOauthCacheResource(), CacheResource.class); if (cacheResource != null) { - // The cast is working because the context instance that is sent by the gateway implements both interfaces - return cacheResource.getCache((GenericExecutionContext) ctx); + return cacheResource.getCache(ctx); } } return null; @@ -335,7 +396,7 @@ private Cache getPolicyTokenIntrospectionCache(HttpPlainExecutionContext ctx) { * @param ctx HttpPlainExecutionContext * @return TokenIntrospectionCache */ - private TokenIntrospectionCache getContextTokenIntrospectionCache(HttpPlainExecutionContext ctx) { + private TokenIntrospectionCache getContextTokenIntrospectionCache(BaseExecutionContext ctx) { TokenIntrospectionCache cache = ctx.getInternalAttribute(ATTR_INTERNAL_TOKEN_INTROSPECTIONS); if (cache == null) { cache = new TokenIntrospectionCache(); diff --git a/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java b/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java index e79c2af5..a2f7074a 100644 --- a/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java +++ b/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java @@ -40,6 +40,7 @@ public class TokenIntrospectionResult { public static final String OAUTH_PAYLOAD_CLIENT_ID_NODE = "client_id"; public static final String OAUTH_PAYLOAD_SUB_NODE = "sub"; public static final String OAUTH_PAYLOAD_EXP = "exp"; + public static final String OAUTH_PAYLOAD_ISSUED_AT = "iat"; private String oauth2ResponsePayload; private boolean success; @@ -81,6 +82,17 @@ public boolean hasExpirationTime() { return getExpirationTime() != null; } + public Long getIssuedAtTime() { + if (hasValidPayload() && oAuth2ResponseJsonNode.has(OAUTH_PAYLOAD_ISSUED_AT)) { + return oAuth2ResponseJsonNode.get(OAUTH_PAYLOAD_ISSUED_AT).asLong(); + } + return null; + } + + public boolean hasIssuedAtTime() { + return getIssuedAtTime() != null; + } + public List extractScopes(String scopeSeparator) { if (hasValidPayload()) { JsonNode scopesNode = oAuth2ResponseJsonNode.path(OAUTH_PAYLOAD_SCOPE_NODE); diff --git a/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java b/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java index 99a744bc..01cdada6 100644 --- a/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java +++ b/src/main/java/io/gravitee/policy/oauth2/utils/TokenExtractor.java @@ -19,9 +19,15 @@ import io.gravitee.gateway.api.Request; import io.gravitee.gateway.api.http.HttpHeaderNames; import io.gravitee.gateway.api.http.HttpHeaders; +import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext; +import io.gravitee.gateway.reactive.api.context.http.HttpPlainExecutionContext; import io.gravitee.gateway.reactive.api.context.http.HttpPlainRequest; +import io.gravitee.gateway.reactive.api.context.kafka.KafkaConnectionContext; import java.util.List; import java.util.Optional; +import javax.security.auth.callback.Callback; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallback; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -35,6 +41,43 @@ public class TokenExtractor { static final String BEARER = "Bearer"; static final String ACCESS_TOKEN = "access_token"; + /** + * Extract an access token from a {@link BaseExecutionContext}. + * If no access token has been found, an {@link Optional#empty()} is returned. + * + * @param ctx the execution context to extract the access token from. + * + * @return the access token as string, {@link Optional#empty()} if no token has been found. + */ + public static Optional extract(BaseExecutionContext ctx) { + if (ctx instanceof HttpPlainExecutionContext httpPlainExecutionContext) { + return extract(httpPlainExecutionContext.request()); + } + KafkaConnectionContext kafkaConnectionContext = (KafkaConnectionContext) ctx; + return extract(kafkaConnectionContext.callbacks()); + } + + /** + * Extract an access token from a {@link javax.security.auth.callback.Callback} array. + * If no access token has been found, an {@link Optional#empty()} is returned. + * + * @param callbacks the callbacks to extract the access token from. + * + * @return the access token as string, {@link Optional#empty()} if no token has been found. + */ + private static Optional extract(Callback[] callbacks) { + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerValidatorCallback oAuthCallback) { + String token = oAuthCallback.tokenValue(); + return Optional.of(token); + } else if (callback instanceof OAuthBearerExtensionsValidatorCallback oAuthCallback) { + String token = oAuthCallback.token().value(); + return Optional.of(token); + } + } + return Optional.empty(); + } + /** * Extract an access token from a {@link Request} headers or parameters. *
    @@ -48,7 +91,7 @@ public class TokenExtractor { * * @return the access token as string, {@link Optional#empty()} if no token has been found. */ - public static Optional extract(HttpPlainRequest request) { + private static Optional extract(HttpPlainRequest request) { return extractFromHeaders(request.headers()).or(() -> extractFromParameters(request.parameters())); } diff --git a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java index 65466193..1b4fca4b 100644 --- a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java +++ b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java @@ -55,6 +55,7 @@ import io.gravitee.gateway.api.http.HttpHeaders; import io.gravitee.gateway.reactive.api.ExecutionFailure; import io.gravitee.gateway.reactive.api.context.GenericExecutionContext; +import io.gravitee.gateway.reactive.api.context.base.BaseExecutionContext; import io.gravitee.gateway.reactive.api.context.http.HttpPlainExecutionContext; import io.gravitee.gateway.reactive.api.context.http.HttpPlainRequest; import io.gravitee.gateway.reactive.api.context.http.HttpPlainResponse; @@ -436,7 +437,7 @@ void shouldPutIntrospectionToCache() throws IOException { private void prepareCacheResource() { when(configuration.getOauthCacheResource()).thenReturn(OAUTH_CACHE_RESOURCE); - when(cacheResource.getCache(any(GenericExecutionContext.class))).thenReturn(cache); + when(cacheResource.getCache(any(BaseExecutionContext.class))).thenReturn(cache); when(resourceManager.getResource(OAUTH_CACHE_RESOURCE, CacheResource.class)).thenReturn(cacheResource); } From e954a570d31086db8da276fbddd14e5c328e1c8d Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Wed, 13 Nov 2024 10:26:59 +0100 Subject: [PATCH 03/12] refactor: introduce failure enum --- .../gravitee/policy/oauth2/Oauth2Policy.java | 83 ++++++++++++------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index 2d9bfe6f..bc308c5f 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -49,6 +49,7 @@ import java.util.Optional; import java.util.Set; import javax.security.auth.callback.Callback; +import lombok.Getter; import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken; import org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallback; import org.apache.kafka.common.security.oauthbearer.internals.secured.BasicOAuthBearerToken; @@ -69,6 +70,42 @@ public class Oauth2Policy extends Oauth2PolicyV3 implements HttpSecurityPolicy, public static final String ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT = "token-introspection-result"; private static final Logger log = LoggerFactory.getLogger(Oauth2Policy.class); + private enum Oauth2Failure { + OAUTH2_MISSING_SERVER_FAILURE(UNAUTHORIZED_401, OAUTH2_MISSING_SERVER_KEY, OAUTH2_UNAUTHORIZED_MESSAGE, false), + OAUTH2_MISSING_HEADER_FAILURE(UNAUTHORIZED_401, OAUTH2_MISSING_HEADER_KEY, OAUTH2_UNAUTHORIZED_MESSAGE, true), + OAUTH2_MISSING_ACCESS_TOKEN_FAILURE(UNAUTHORIZED_401, OAUTH2_MISSING_ACCESS_TOKEN_KEY, OAUTH2_UNAUTHORIZED_MESSAGE, true), + OAUTH2_INVALID_ACCESS_TOKEN_FAILURE(UNAUTHORIZED_401, OAUTH2_INVALID_ACCESS_TOKEN_KEY, OAUTH2_UNAUTHORIZED_MESSAGE, true), + OAUTH2_INVALID_SERVER_RESPONSE_FAILURE(UNAUTHORIZED_401, OAUTH2_INVALID_SERVER_RESPONSE_KEY, OAUTH2_UNAUTHORIZED_MESSAGE, true), + OAUTH2_INSUFFICIENT_SCOPE_FAILURE(UNAUTHORIZED_401, OAUTH2_INSUFFICIENT_SCOPE_KEY, OAUTH2_UNAUTHORIZED_MESSAGE, true), + OAUTH2_SERVER_UNAVAILABLE_FAILURE( + SERVICE_UNAVAILABLE_503, + OAUTH2_SERVER_UNAVAILABLE_KEY, + OAUTH2_TEMPORARILY_UNAVAILABLE_MESSAGE, + true + ); + + private final int httpStatusCode; + + @Getter + private final String failureKey; + + private final String failureMessage; + + @Getter + private final boolean addWWWAuthenticateHeader; + + Oauth2Failure(int httpStatusCode, String failureKey, String failureMessage, boolean addWWWAuthenticateHeader) { + this.httpStatusCode = httpStatusCode; + this.failureKey = failureKey; + this.failureMessage = failureMessage; + this.addWWWAuthenticateHeader = addWWWAuthenticateHeader; + } + + public ExecutionFailure toExecutionFailure() { + return new ExecutionFailure(httpStatusCode).key(failureKey).message(failureMessage); + } + } + public Oauth2Policy(OAuth2PolicyConfiguration oAuth2PolicyConfiguration) { super(oAuth2PolicyConfiguration); } @@ -179,19 +216,14 @@ private Completable handleSecurity(final BaseExecutionContext ctx) { final OAuth2Resource oauth2Resource = getOauth2Resource(ctx); if (oauth2Resource == null) { - return interruptWith( - ctx, - new ExecutionFailure(UNAUTHORIZED_401).key(OAUTH2_MISSING_SERVER_KEY).message(OAUTH2_UNAUTHORIZED_MESSAGE) - ); + return interruptWith(ctx, Oauth2Failure.OAUTH2_MISSING_SERVER_FAILURE); } return fetchJWTToken(ctx, false) - .switchIfEmpty( - Maybe.defer(() -> sendError(ctx, UNAUTHORIZED_401, OAUTH2_MISSING_HEADER_KEY, OAUTH2_UNAUTHORIZED_MESSAGE).toMaybe()) - ) + .switchIfEmpty(Maybe.defer(() -> interruptWith(ctx, Oauth2Failure.OAUTH2_MISSING_HEADER_FAILURE).toMaybe())) .flatMapCompletable(accessToken -> { if (accessToken.isBlank()) { - return sendError(ctx, UNAUTHORIZED_401, OAUTH2_MISSING_ACCESS_TOKEN_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); + return interruptWith(ctx, Oauth2Failure.OAUTH2_MISSING_ACCESS_TOKEN_FAILURE); } // Set access_token in context @@ -233,7 +265,7 @@ private Completable validateOAuth2Payload( OAuth2Resource oauth2Resource ) { if (!tokenIntrospectionResult.hasValidPayload()) { - return sendError(ctx, UNAUTHORIZED_401, OAUTH2_INVALID_SERVER_RESPONSE_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); + return interruptWith(ctx, Oauth2Failure.OAUTH2_INVALID_SERVER_RESPONSE_FAILURE); } if (tokenIntrospectionResult.hasClientId()) { @@ -252,7 +284,7 @@ private Completable validateOAuth2Payload( // Check required scopes to access the resource if (oAuth2PolicyConfiguration.isCheckRequiredScopes()) { if (!hasRequiredScopes(scopes, oAuth2PolicyConfiguration.getRequiredScopes(), oAuth2PolicyConfiguration.isModeStrict())) { - return sendError(ctx, UNAUTHORIZED_401, OAUTH2_INSUFFICIENT_SCOPE_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); + return interruptWith(ctx, Oauth2Failure.OAUTH2_INSUFFICIENT_SCOPE_FAILURE); } } @@ -312,14 +344,9 @@ private Completable introspectAndValidateAccessToken(BaseExecutionContext ctx, S return validateOAuth2Payload(ctx, introspectionResult, oauth2Resource); } else { if (introspectionResult.getOauth2ResponseThrowable() == null) { - return sendError(ctx, UNAUTHORIZED_401, OAUTH2_INVALID_ACCESS_TOKEN_KEY, OAUTH2_UNAUTHORIZED_MESSAGE); + return interruptWith(ctx, Oauth2Failure.OAUTH2_INVALID_ACCESS_TOKEN_FAILURE); } else { - return sendError( - ctx, - SERVICE_UNAVAILABLE_503, - OAUTH2_SERVER_UNAVAILABLE_KEY, - OAUTH2_TEMPORARILY_UNAVAILABLE_MESSAGE - ); + return interruptWith(ctx, Oauth2Failure.OAUTH2_SERVER_UNAVAILABLE_FAILURE); } } }); @@ -333,24 +360,16 @@ private Completable introspectAndValidateAccessToken(BaseExecutionContext ctx, S * error="invalid_token", * error_description="The access token expired" */ - private Completable sendError(BaseExecutionContext ctx, int errorStatus, String responseKey, String failureMessage) { + private Completable interruptWith(BaseExecutionContext ctx, Oauth2Failure failure) { if (ctx instanceof HttpPlainExecutionContext httpPlainExecutionContext) { - String headerValue = BEARER_AUTHORIZATION_TYPE + " realm=\"gravitee.io\""; - - httpPlainExecutionContext.response().headers().add(HttpHeaderNames.WWW_AUTHENTICATE, headerValue); - - return httpPlainExecutionContext.interruptWith(new ExecutionFailure(errorStatus).key(responseKey).message(failureMessage)); - } - // FIXME: Kafka Gateway - manage interruption with Kafka. - return Completable.error(new Exception(responseKey)); - } - - private Completable interruptWith(BaseExecutionContext ctx, ExecutionFailure failure) { - if (ctx instanceof HttpPlainExecutionContext httpPlainExecutionContext) { - return httpPlainExecutionContext.interruptWith(failure); + if (failure.isAddWWWAuthenticateHeader()) { + String headerValue = BEARER_AUTHORIZATION_TYPE + " realm=\"gravitee.io\""; + httpPlainExecutionContext.response().headers().add(HttpHeaderNames.WWW_AUTHENTICATE, headerValue); + } + return httpPlainExecutionContext.interruptWith(failure.toExecutionFailure()); } // FIXME: Kafka Gateway - manage interruption with Kafka. - return Completable.error(new Exception(failure.key())); + return Completable.error(new Exception(failure.getFailureKey())); } /** From aebf5215379c553cc310b53476cdec48738799d3 Mon Sep 17 00:00:00 2001 From: "Gravitee.io Bot" Date: Thu, 14 Nov 2024 14:58:20 +0000 Subject: [PATCH 04/12] chore(release): 4.0.0-alpha.1 [skip ci] --- CHANGELOG.md | 17 +++++++++++++++++ pom.xml | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d0c83d..00748cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# [4.0.0-alpha.1](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/3.0.5...4.0.0-alpha.1) (2024-11-14) + + +### Code Refactoring + +* use new HttpSecurityPolicy interface ([9e65b1e](https://github.com/gravitee-io/gravitee-policy-oauth2/commit/9e65b1ee8ecb43a505657f2d77c3a42c8b8cdece)) + + +### Features + +* implement kafka security policy ([a5a87a8](https://github.com/gravitee-io/gravitee-policy-oauth2/commit/a5a87a8367a9c48b2863488efba85a737842892e)) + + +### BREAKING CHANGES + +* requires APIM 4.6+ + ## [3.0.5](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/3.0.4...3.0.5) (2024-08-29) diff --git a/pom.xml b/pom.xml index 6b085b06..6a8ca04e 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ io.gravitee.policy gravitee-policy-oauth2 - 4.0.0-APIM-7143-impl-security-chain-SNAPSHOT + 4.0.0-alpha.1 Gravitee.io APIM - Policy - OAuth2 Check access token validity during request processing using token introspection From f9f361714061887e01e78cbd85ae91c23c16ca1a Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Fri, 22 Nov 2024 17:37:27 +0100 Subject: [PATCH 05/12] docs: update compatibility matrix [skip ci] --- README.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 01ce58a1..1b6679d3 100644 --- a/README.adoc +++ b/README.adoc @@ -37,9 +37,10 @@ $ curl -H "Authorization: Bearer |accessToken|" \ |=== | Plugin version | APIM version +| 4.x | 4.6.x to latest +| 3.x | 4.0.x to 4.5.x +| 2.x | 3.20.x | 1.x | Up to 3.19.x -| 2.0.x | 3.20.x -| 3.x | 4.x to latest |=== == Attributes From 121bfebf7199db8078781941038caaeb839af13c Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Fri, 22 Nov 2024 17:41:15 +0100 Subject: [PATCH 06/12] fix: invoke callback and complete on auth failure --- .../java/io/gravitee/policy/oauth2/Oauth2Policy.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index bc308c5f..b92d8e35 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -180,7 +180,7 @@ public Completable authenticate(KafkaConnectionContext ctx) { extractedToken, Set.of(), // Scopes are fully managed by Gravitee, it is useless to extract & provide them to the Kafka security context. (expirationTime == null ? Long.MAX_VALUE : expirationTime), - user, + user != null ? user : "unknown", issueTime ); @@ -189,6 +189,15 @@ public Completable authenticate(KafkaConnectionContext ctx) { } }) ) + .onErrorResumeNext(throwable -> { + Callback[] callbacks = ctx.callbacks(); + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerValidatorCallback oauthCallback) { + oauthCallback.error("invalid_token", null, null); + } + } + return Completable.complete(); + }) .doAfterTerminate(() -> ctx.removeInternalAttribute(ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT)); } From a8ee73f469431f749ca1f9250832edd2f90afa34 Mon Sep 17 00:00:00 2001 From: "Gravitee.io Bot" Date: Mon, 25 Nov 2024 07:41:04 +0000 Subject: [PATCH 07/12] chore(release): 4.0.0-alpha.2 [skip ci] --- CHANGELOG.md | 7 +++++++ pom.xml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00748cb2..ea4d606c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [4.0.0-alpha.2](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/4.0.0-alpha.1...4.0.0-alpha.2) (2024-11-25) + + +### Bug Fixes + +* invoke callback and complete on auth failure ([121bfeb](https://github.com/gravitee-io/gravitee-policy-oauth2/commit/121bfebf7199db8078781941038caaeb839af13c)) + # [4.0.0-alpha.1](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/3.0.5...4.0.0-alpha.1) (2024-11-14) diff --git a/pom.xml b/pom.xml index 6a8ca04e..cf40a357 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ io.gravitee.policy gravitee-policy-oauth2 - 4.0.0-alpha.1 + 4.0.0-alpha.2 Gravitee.io APIM - Policy - OAuth2 Check access token validity during request processing using token introspection From 024ba6e50fd2af9ebc3967740d20993877eb9821 Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Thu, 28 Nov 2024 15:49:16 +0100 Subject: [PATCH 08/12] feat: set a max value for kafka token lifetime https://gravitee.atlassian.net/browse/APIM-7143 --- .../io/gravitee/policy/oauth2/Oauth2Policy.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index b92d8e35..717a03fb 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -55,6 +55,7 @@ import org.apache.kafka.common.security.oauthbearer.internals.secured.BasicOAuthBearerToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; /** * @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com) @@ -68,6 +69,10 @@ public class Oauth2Policy extends Oauth2PolicyV3 implements HttpSecurityPolicy, public static final String ATTR_INTERNAL_TOKEN_INTROSPECTIONS = "token-introspection-cache"; public static final String ATTR_INTERNAL_TOKEN_INTROSPECTION_RESULT = "token-introspection-result"; + + private static final String KAFKA_OAUTHBEARER_MAX_TOKEN_LIFETIME = "kafka.oauthbearer.maxTokenLifetime"; + private static final long DEFAULT_MAX_TOKEN_LIFETIME_MS = 60 * 60 * 1000L; // 1 hour + private static final Logger log = LoggerFactory.getLogger(Oauth2Policy.class); private enum Oauth2Failure { @@ -176,10 +181,17 @@ public Completable authenticate(KafkaConnectionContext ctx) { Long expirationTime = tokenIntrospectionResult.getExpirationTime(); Long issueTime = tokenIntrospectionResult.getIssuedAtTime(); + Environment environment = ctx.getComponent(Environment.class); + long maxTokenLifetime = environment.getProperty( + KAFKA_OAUTHBEARER_MAX_TOKEN_LIFETIME, + Long.class, + DEFAULT_MAX_TOKEN_LIFETIME_MS + ); + OAuthBearerToken token = new BasicOAuthBearerToken( extractedToken, Set.of(), // Scopes are fully managed by Gravitee, it is useless to extract & provide them to the Kafka security context. - (expirationTime == null ? Long.MAX_VALUE : expirationTime), + (expirationTime == null ? maxTokenLifetime : Math.min(maxTokenLifetime, expirationTime * 1000)), user != null ? user : "unknown", issueTime ); From 97ad32e6a4e901d9acdaf3fc8f7216d286cf704f Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Thu, 28 Nov 2024 15:51:14 +0100 Subject: [PATCH 09/12] refactor: use templateEngine.evalNow instead of getValue --- src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java | 5 +---- .../java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java index 717a03fb..a2567095 100644 --- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java +++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java @@ -406,10 +406,7 @@ private OAuth2Resource getOauth2Resource(BaseExecutionContext ctx) { return ctx .getComponent(ResourceManager.class) - .getResource( - ctx.getTemplateEngine().getValue(oAuth2PolicyConfiguration.getOauthResource(), String.class), - OAuth2Resource.class - ); + .getResource(ctx.getTemplateEngine().evalNow(oAuth2PolicyConfiguration.getOauthResource(), String.class), OAuth2Resource.class); } /** diff --git a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java index 1b4fca4b..9df327ba 100644 --- a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java +++ b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyTest.java @@ -588,7 +588,7 @@ private String prepareToken() { private void prepareOauth2Resource() { when(configuration.getOauthResource()).thenReturn(OAUTH_RESOURCE); - when(templateEngine.getValue(OAUTH_RESOURCE, String.class)).thenReturn(OAUTH_RESOURCE); + when(templateEngine.evalNow(OAUTH_RESOURCE, String.class)).thenReturn(OAUTH_RESOURCE); when(resourceManager.getResource(OAUTH_RESOURCE, OAuth2Resource.class)).thenReturn(oAuth2Resource); lenient().when(oAuth2Resource.getScopeSeparator()).thenReturn(DEFAULT_OAUTH_SCOPE_SEPARATOR); } From f40977153340ad71bf6be981a5edbd55aed1e78e Mon Sep 17 00:00:00 2001 From: "Gravitee.io Bot" Date: Fri, 29 Nov 2024 07:02:07 +0000 Subject: [PATCH 10/12] chore(release): 4.0.0-alpha.3 [skip ci] --- CHANGELOG.md | 7 +++++++ pom.xml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4d606c..f390d9de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [4.0.0-alpha.3](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/4.0.0-alpha.2...4.0.0-alpha.3) (2024-11-29) + + +### Features + +* set a max value for kafka token lifetime ([024ba6e](https://github.com/gravitee-io/gravitee-policy-oauth2/commit/024ba6e50fd2af9ebc3967740d20993877eb9821)) + # [4.0.0-alpha.2](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/4.0.0-alpha.1...4.0.0-alpha.2) (2024-11-25) diff --git a/pom.xml b/pom.xml index cf40a357..f56c0607 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ io.gravitee.policy gravitee-policy-oauth2 - 4.0.0-alpha.2 + 4.0.0-alpha.3 Gravitee.io APIM - Policy - OAuth2 Check access token validity during request processing using token introspection From 7ecbb489d36915a159eeebb1e1b211e72c4508c3 Mon Sep 17 00:00:00 2001 From: Florent CHAMFROY Date: Mon, 30 Dec 2024 15:33:39 +0100 Subject: [PATCH 11/12] fix(deps): bump apim version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f56c0607..68f77d01 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ - 4.6.0-SNAPSHOT + 4.6.0-alpha.3 3.6.0 1.1.0 From 429903a5ab05257c18fd428466e5789e6e3d2a73 Mon Sep 17 00:00:00 2001 From: "Gravitee.io Bot" Date: Mon, 30 Dec 2024 14:37:52 +0000 Subject: [PATCH 12/12] chore(release): 4.0.0-alpha.4 [skip ci] --- CHANGELOG.md | 7 +++++++ pom.xml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f390d9de..6f1ac378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [4.0.0-alpha.4](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/4.0.0-alpha.3...4.0.0-alpha.4) (2024-12-30) + + +### Bug Fixes + +* **deps:** bump apim version ([7ecbb48](https://github.com/gravitee-io/gravitee-policy-oauth2/commit/7ecbb489d36915a159eeebb1e1b211e72c4508c3)) + # [4.0.0-alpha.3](https://github.com/gravitee-io/gravitee-policy-oauth2/compare/4.0.0-alpha.2...4.0.0-alpha.3) (2024-11-29) diff --git a/pom.xml b/pom.xml index 68f77d01..16205ab0 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ io.gravitee.policy gravitee-policy-oauth2 - 4.0.0-alpha.3 + 4.0.0-alpha.4 Gravitee.io APIM - Policy - OAuth2 Check access token validity during request processing using token introspection