Skip to content

Commit

Permalink
Update quarkus-oidc-token-propagation to better work with JWT tokens …
Browse files Browse the repository at this point in the history
…and update smalrye-jwt to 2.4.4
  • Loading branch information
sberyozkin committed Mar 10, 2021
1 parent eb98535 commit e0af988
Show file tree
Hide file tree
Showing 38 changed files with 1,043 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .github/native-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/security-jwt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
|===

== References
Expand Down
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
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion extensions/oidc-token-propagation/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
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;
Expand All @@ -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;

Expand All @@ -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
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit e0af988

Please sign in to comment.