Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update quarkus-oidc-token-propagation to better work with JWT tokens and update smalrye-jwt to 2.4.4 #15606

Merged
merged 1 commit into from
Mar 11, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/native-tests.json
Original file line number Diff line number Diff line change
@@ -75,7 +75,7 @@
{
"category": "Security2",
"timeout": 70,
"test-modules": "oidc oidc-code-flow oidc-tenancy keycloak-authorization oidc-client oidc-token-propagation oidc-wiremock oidc-client-wiremock"
"test-modules": "oidc oidc-code-flow oidc-tenancy keycloak-authorization oidc-client oidc-token-propagation smallrye-jwt-token-propagation oidc-wiremock oidc-client-wiremock"
},
{
"category": "Security3",
14 changes: 13 additions & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@
<microprofile-opentracing-api.version>1.3.3</microprofile-opentracing-api.version>
<microprofile-reactive-streams-operators.version>1.0.1</microprofile-reactive-streams-operators.version>
<microprofile-rest-client.version>1.4.1</microprofile-rest-client.version>
<microprofile-jwt.version>1.1.1</microprofile-jwt.version>
<smallrye-common.version>1.5.0</smallrye-common.version>
<smallrye-config.version>1.11.1</smallrye-config.version>
<smallrye-health.version>2.2.6</smallrye-health.version>
@@ -46,7 +47,7 @@
<smallrye-graphql.version>1.0.22</smallrye-graphql.version>
<smallrye-opentracing.version>1.3.5</smallrye-opentracing.version>
<smallrye-fault-tolerance.version>4.3.2</smallrye-fault-tolerance.version>
<smallrye-jwt.version>2.4.3</smallrye-jwt.version>
<smallrye-jwt.version>2.4.4</smallrye-jwt.version>
<smallrye-context-propagation.version>1.1.0</smallrye-context-propagation.version>
<smallrye-reactive-streams-operators.version>1.0.13</smallrye-reactive-streams-operators.version>
<smallrye-converter-api.version>1.4.0</smallrye-converter-api.version>
@@ -3579,6 +3580,17 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.jwt</groupId>
<artifactId>microprofile-jwt-auth-api</artifactId>
<version>${microprofile-jwt.version}</version>
<exclusions>
<exclusion>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.versioning</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/security-jwt.adoc
Original file line number Diff line number Diff line change
@@ -947,6 +947,7 @@ SmallRye JWT supports the following properties which can be used to customize th
|smallrye.jwt.new-token.lifespan|300|Token lifespan in seconds which will be used to calculate an `exp` (expiry) claim value if this claim has not already been set.
|smallrye.jwt.new-token.issuer|none|Token issuer which can be used to set an `iss` (issuer) claim value if this claim has not already been set.
|smallrye.jwt.new-token.audience|none|Token audience which can be used to set an `aud` (audience) claim value if this claim has not already been set.
|smallrye.jwt.new-token.override-matching-claims|false| Set this property to `true` for `smallrye.jwt.new-token.issuer` and `smallrye.jwt.new-token.audience` values to override the already initialized `iss` (issuer) and `aud` (audience) claims.
gastaldi marked this conversation as resolved.
Show resolved Hide resolved
|===

== References
75 changes: 71 additions & 4 deletions docs/src/main/asciidoc/security-openid-connect-client.adoc
Original file line number Diff line number Diff line change
@@ -323,14 +323,20 @@ Using `private_key_jwt` or `private_key_jwt` authentication methods ensures that
[[token-propagation]]
== Token Propagation in MicroProfile RestClient client filter

`quarkus-oidc-token-propagation` extension provides `io.quarkus.oidc.token.propagation.AccessTokenRequestFilter` JAX-RS ClientRequestFilter which propagates the current link:security-openid-connect[Bearer] or link:security-openid-connect-web-authentication[Authorization Code Flow] access token as an HTTP `Authorization` `Bearer` scheme value.
`quarkus-oidc-token-propagation` extension provide `io.quarkus.oidc.token.propagation.AccessTokenRequestFilter` and `io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter` JAX-RS ClientRequestFilters which propagates the current link:security-openid-connect[Bearer] or link:security-openid-connect-web-authentication[Authorization Code Flow] access token as an HTTP `Authorization` `Bearer` scheme value.

=== AccessTokenRequestFilter

`AccessTokenRequestFilter` treats all tokens as Strings and as such it can work with both JWT and opaque tokens.

When you need to propagate the current Authorization Code Flow access token then `AccessTokenRequestFilter` will be the best option as such tokens do not need to be exchanged or otherwise re-enhanced. Authorization Code Flow access tokens may be also be opaque/binary tokens.

You can selectively register `AccessTokenRequestFilter` by using either `io.quarkus.oidc.token.propagation.AccessToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider`, for example:

[source,java]
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.TokenCredential;
import io.quarkus.oidc.token.propagation.AccessToken;

@RegisterRestClient
@AccessToken
@@ -359,9 +365,70 @@ public interface ProtectedResourceService {
}
----

Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-token-propagation.register-filter=true` property is set.
Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-token-propagation.register-filter` property is set to `true` and `quarkus.oidc-token-propagation.json-web-token` property is set to `false` (which is a default value).

This filter will be additionally enhanced in the future to support exchanging the access tokens before propagating them.

=== JsonWebTokenRequestFilter

Using `JsonWebTokenRequestFilter` is recommended if you work with Bearer JWT tokens where these tokens can have their claims such as `issuer` and `audience` modified and the updated tokens secured (for example, re-signed) again. It expects an injected `org.eclipse.microprofile.jwt.JsonWebToken` and therefore will not work with the opaque tokens.

Direct end to end Bearer token propagation should be avoided if possible. For example, `Client -> Service A -> Service B` where `Service B` receives a token sent by `Client` to `Service A`. In such cases `Service B` will not be able to distinguish if the token came from `Service A` or from `Client` directly. For `Service B` to verify the token came from `Service A` it should be able to assert a new issuer and audience claims.

`JsonWebTokenRequestFilter` makes it easy for `Service A` implemementations to update the injected `org.eclipse.microprofile.jwt.JsonWebToken` with the new `issuer` and `audience` claim values and secure the updated token again with a new signature. The only difficult step is to ensure `Service A` has a signing key - it should be provisioned from a secure file system or from the remote secure storage such as Vault.

You can selectively register `JsonWebTokenRequestFilter` by using either `io.quarkus.oidc.token.propagation.JsonWebToken` or `org.eclipse.microprofile.rest.client.annotation.RegisterProvider`, for example:

[source,java]
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebToken;

@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {

@GET
String getUserName();
}
----
or

[source,java]
----
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter;

@RegisterRestClient
@RegisterProvider(JsonWebTokenTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {

@GET
String getUserName();
}
----

Alternatively, `JsonWebTokenRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if both `quarkus.oidc-token-propagation.register-filter` and ``quarkus.oidc-token-propagation.json-web-token` properties are set to `true`.

If this filter has to update the inject token and secure it with a new signature again then you can configure it like this:

[source,properties]
----
quarkus.oidc-token-propagation.secure-json-web-token=true
smallrye.jwt.sign.key.location=/privateKey.pem
# Set a new issuer
smallrye.jwt.new-token.issuer=http://frontend-resource
# Set a new audience
smallrye.jwt.new-token.audience=http://downstream-resource
# Override the existing token issuer and audience claims if they are already set
smallrye.jwt.new-token.override-matching-claims=true
----


This filter will be enhanced in the future to support re-signing and/or exchanging the access tokens before propagating them.
This filter will be additionally enhanced in the future to support exchanging the access tokens before propagating them.

== References

6 changes: 5 additions & 1 deletion extensions/oidc-token-propagation/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -20,12 +20,16 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-deployment</artifactId>
<artifactId>quarkus-security-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build-deployment</artifactId>
</dependency>
</dependencies>

<build>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.oidc.token.propagation.deployment;

import java.util.function.BooleanSupplier;

import org.jboss.jandex.DotName;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
@@ -9,16 +11,19 @@
import io.quarkus.deployment.builditem.EnableAllSecurityServicesBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.oidc.deployment.OidcBuildStep.IsEnabled;
import io.quarkus.oidc.token.propagation.AccessToken;
import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;
import io.quarkus.oidc.token.propagation.JsonWebToken;
import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter;
import io.quarkus.oidc.token.propagation.runtime.OidcTokenPropagationBuildTimeConfig;
import io.quarkus.oidc.token.propagation.runtime.OidcTokenPropagationConfig;
import io.quarkus.restclient.deployment.RestClientAnnotationProviderBuildItem;
import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem;

public class OidcTokenPropagationBuildStep {

private static final DotName TOKEN_CREDENTIAL = DotName.createSimple(AccessToken.class.getName());
private static final DotName ACCESS_TOKEN_CREDENTIAL = DotName.createSimple(AccessToken.class.getName());
private static final DotName JWT_ACCESS_TOKEN_CREDENTIAL = DotName.createSimple(JsonWebToken.class.getName());

OidcTokenPropagationConfig config;

@@ -38,12 +43,26 @@ void registerProvider(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<ResteasyJaxrsProviderBuildItem> jaxrsProviders,
BuildProducer<RestClientAnnotationProviderBuildItem> restAnnotationProvider) {
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(AccessTokenRequestFilter.class));
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(JsonWebTokenRequestFilter.class));
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, AccessTokenRequestFilter.class));
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, JsonWebTokenRequestFilter.class));

if (config.registerFilter) {
jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(AccessTokenRequestFilter.class.getName()));
Class<?> filterClass = config.jsonWebToken ? JsonWebTokenRequestFilter.class : AccessTokenRequestFilter.class;
jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(filterClass.getName()));
} else {
restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(TOKEN_CREDENTIAL,
restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(ACCESS_TOKEN_CREDENTIAL,
AccessTokenRequestFilter.class));
restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(JWT_ACCESS_TOKEN_CREDENTIAL,
JsonWebTokenRequestFilter.class));
}
}

public static class IsEnabled implements BooleanSupplier {
OidcTokenPropagationBuildTimeConfig config;

public boolean getAsBoolean() {
return config.enabled;
}
}
}
10 changes: 9 additions & 1 deletion extensions/oidc-token-propagation/runtime/pom.xml
Original file line number Diff line number Diff line change
@@ -16,12 +16,20 @@
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.jwt</groupId>
<artifactId>microprofile-jwt-auth-api</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
</dependencies>

<build>
Original file line number Diff line number Diff line change
@@ -2,37 +2,22 @@

import java.io.IOException;

import javax.annotation.Priority;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Priorities;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;

import org.jboss.logging.Logger;
import io.quarkus.oidc.token.propagation.runtime.AbstractTokenRequestFilter;
import io.quarkus.security.credential.TokenCredential;

import io.quarkus.oidc.AccessTokenCredential;

@Provider
@Singleton
@Priority(Priorities.AUTHENTICATION)
public class AccessTokenRequestFilter implements ClientRequestFilter {
private static final Logger LOG = Logger.getLogger(AccessTokenRequestFilter.class);
private static final String BEARER_SCHEME_WITH_SPACE = "Bearer ";
public class AccessTokenRequestFilter extends AbstractTokenRequestFilter {

@Inject
AccessTokenCredential tokenCredential;
Instance<TokenCredential> accessToken;

@Override
public void filter(ClientRequestContext requestContext) throws IOException {
try {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + tokenCredential.getToken());
} catch (Exception ex) {
LOG.debugf("Access token is not available, aborting the request with HTTP 401 error: %s", ex.getMessage());
requestContext.abortWith(Response.status(401).build());
if (verifyTokenInstance(requestContext, accessToken)) {
propagateToken(requestContext, accessToken.get().getToken());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.oidc.token.propagation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonWebToken {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.quarkus.oidc.token.propagation;

import java.io.IOException;

import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.ws.rs.client.ClientRequestContext;

import org.eclipse.microprofile.config.inject.ConfigProperty;

import io.quarkus.oidc.token.propagation.runtime.AbstractTokenRequestFilter;
import io.smallrye.jwt.build.Jwt;

public class JsonWebTokenRequestFilter extends AbstractTokenRequestFilter {
@Inject
Instance<org.eclipse.microprofile.jwt.JsonWebToken> jwtAccessToken;

@Inject
@ConfigProperty(name = "quarkus.oidc-token-propagation.secure-json-web-token")
boolean resignToken;

@Override
public void filter(ClientRequestContext requestContext) throws IOException {
if (verifyTokenInstance(requestContext, jwtAccessToken)) {
propagateToken(requestContext, getToken());
}
}

private String getToken() {
if (resignToken) {
return Jwt.claims(jwtAccessToken.get()).sign();
} else {
return jwtAccessToken.get().getRawToken();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkus.oidc.token.propagation.runtime;

import java.io.IOException;

import javax.annotation.Priority;
import javax.enterprise.inject.Instance;
import javax.inject.Singleton;
import javax.ws.rs.Priorities;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;

import org.jboss.logging.Logger;

@Provider
@Singleton
@Priority(Priorities.AUTHENTICATION)
public abstract class AbstractTokenRequestFilter implements ClientRequestFilter {
private static final Logger LOG = Logger.getLogger(AbstractTokenRequestFilter.class);
private static final String BEARER_SCHEME_WITH_SPACE = "Bearer ";

public void propagateToken(ClientRequestContext requestContext, String token) throws IOException {
if (token != null) {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + token);
} else {
LOG.debugf("Injected access token is null, aborting the request with HTTP 401 error");
abortRequest(requestContext);
}
}

protected boolean verifyTokenInstance(ClientRequestContext requestContext, Instance<?> instance) throws IOException {
if (!instance.isResolvable()) {
LOG.debugf("Access token is not injected, aborting the request with HTTP 401 error");
abortRequest(requestContext);
return false;
}
if (instance.isAmbiguous()) {
LOG.debugf("More than one access token instance is available, aborting the request with HTTP 401 error");
abortRequest(requestContext);
return false;
}

return true;
}

protected void abortRequest(ClientRequestContext requestContext) {
requestContext.abortWith(Response.status(401).build());
}
}
Loading