Skip to content

Commit

Permalink
Allow skipping OIDC issuer verification
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Apr 9, 2021
1 parent b790e96 commit 6e761d9
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 8 deletions.
Expand Up @@ -570,6 +570,12 @@ quarkus.oidc.introspection-path=/protocol/openid-connect/token/introspect
quarkus.oidc.end-session-path=/protocol/openid-connect/logout
----

[[jwt-claim-verification]]
== JSON Web Token Claim Verification

Please see link:security-openid-connect#jwt-claim-verification[JSON Web Token Claim verification] section about the claim verification, including the `iss` (issuer) claim.
It applies to ID tokens but also to access tokens in a JWT format if the `web-app` application has requested the access token verification.

== Token Propagation
Please see link:security-openid-connect-client#token-propagation[Token Propagation] section about the Authorization Code Flow access token propagation to the downstream services.

Expand Down
46 changes: 46 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect.adoc
Expand Up @@ -427,6 +427,51 @@ quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
----

[[jwt-claim-verification]]
== JSON Web Token Claim Verification

Once the bearer JWT token's signature has been verified and its `expires at` (`exp`) claim has been checked, the `iss` (`issuer`) claim value is verified next.

By default, the `iss` claim value is compared to the `issuer` property which may have been discovered in the well-known provider configuration.
But if `quarkus.oidc.token.issuer` property is set then the `iss` claim value is compared to it instead.

In some cases, this `iss` claim verification may not work. For example, if the discovered `issuer` property contains an internal HTTP/IP address while the token `iss` claim value contains an external HTTP/IP address. Or when a discovered `issuer` property contains the template tenant variable but the token `iss` claim value has the complete tenant-specific issuer value.

In such cases you may want to consider skipping the issuer verification by setting `quarkus.oidc.token.issuer=any`. Please note that it is not recommended and should be avoided unless no other options are available:

- If you work with Keycloak and observe the issuer verification errors due to the different host addresses then configure Keycloak with a `KEYCLOAK_FRONTEND_URL` property to ensure the same host address is used.
- If the `iss` property is tenant specific in a multi-tenant deployment then you can use the `SecurityIdentity` `tenant-id` attribute to check the issuer is correct in the endpoint itself or the custom JAX-RS filter, for example:

[source, java]
----
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.security.identity.SecurityIdentity;
@Provider
public class IssuerValidator implements ContainerRequestFilter {
@Inject
OidcConfigurationMetadata configMetadata;
@Inject JsonWebToken jwt;
@Inject SecurityIdentity identity;
public void filter(ContainerRequestContext requestContext) {
String issuer = configMetadata.getIssuer().replace("{tenant-id}", identity.getAttribute("tenant-id"));
if (!issuer.equals(jwt.getIssuer())) {
requestContext.abortWith(Response.status(401).build());
}
}
}
----

Note it is also recommended to use `quarkus.oidc.token.audience` property to verify the token `aud` (`audience`) claim value.

== Token Propagation

Please see link:security-openid-connect-client#token-propagation[Token Propagation] section about the Bearer access token propagation to the downstream services.
Expand All @@ -446,6 +491,7 @@ Start by adding the following dependencies to your test project:
<groupId>org.eclipse.jetty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
Expand Down
Expand Up @@ -640,6 +640,12 @@ public static Token fromAudience(String... audience) {

/**
* Expected issuer 'iss' claim value.
* Note this property overrides the `issuer` property which may be set in OpenId Connect provider's well-known
* configuration.
* If the `iss` claim value varies depending on the host/IP address or tenant id of the provider then you may skip the
* issuer verification by setting this property to 'any' but it should be done only when other options (such as
* configuring
* the provider to use the fixed `iss` claim value) are not possible.
*/
@ConfigItem
public Optional<String> issuer = Optional.empty();
Expand Down
Expand Up @@ -33,49 +33,70 @@
public class OidcProvider {

private static final Logger LOG = Logger.getLogger(OidcProvider.class);
private static final String ANY_ISSUER = "any";
private static final String[] SUPPORTED_ALGORITHMS = new String[] { SignatureAlgorithm.RS256.getAlgorithm(),
SignatureAlgorithm.RS384.getAlgorithm(),
SignatureAlgorithm.RS512.getAlgorithm(),
SignatureAlgorithm.ES256.getAlgorithm(),
SignatureAlgorithm.ES384.getAlgorithm(),
SignatureAlgorithm.ES512.getAlgorithm() };
private static final AlgorithmConstraints ALGORITHM_CONSTRAINTS = new AlgorithmConstraints(
AlgorithmConstraints.ConstraintType.PERMIT, SUPPORTED_ALGORITHMS);

final OidcProviderClient client;
final RefreshableVerificationKeyResolver keyResolver;
final OidcTenantConfig oidcConfig;
final String issuer;
final String[] audience;

public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeyCache jwks) {
this.client = client;
this.oidcConfig = oidcConfig;
this.keyResolver = jwks == null ? null : new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval);

this.issuer = checkIssuerProp();
this.audience = checkAudienceProp();
}

public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig) {
this.client = null;
this.oidcConfig = oidcConfig;
this.keyResolver = new LocalPublicKeyResolver(publicKeyEnc);
this.issuer = checkIssuerProp();
this.audience = checkAudienceProp();
}

private String checkIssuerProp() {
String issuerProp = null;
if (oidcConfig != null) {
issuerProp = oidcConfig.token.issuer.orElse(null);
if (issuerProp == null && client != null) {
issuerProp = client.getMetadata().getIssuer();
}
}
return ANY_ISSUER.equals(issuerProp) ? null : issuerProp;
}

private String[] checkAudienceProp() {
List<String> audienceProp = oidcConfig != null ? oidcConfig.token.audience.orElse(null) : null;
return audienceProp != null ? audienceProp.toArray(new String[] {}) : null;
}

public TokenVerificationResult verifyJwtToken(String token) throws InvalidJwtException {
JwtConsumerBuilder builder = new JwtConsumerBuilder();

builder.setVerificationKeyResolver(keyResolver);

builder.setJwsAlgorithmConstraints(
new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, SUPPORTED_ALGORITHMS));
builder.setJwsAlgorithmConstraints(ALGORITHM_CONSTRAINTS);

builder.setRequireExpirationTime();
builder.setRequireIssuedAt();

String issuer = oidcConfig.token.issuer.orElse(null);
if (issuer == null && client != null) {
issuer = client.getMetadata().getIssuer();
}
if (issuer != null) {
builder.setExpectedIssuer(issuer);
}
if (oidcConfig.token.audience.isPresent()) {
builder.setExpectedAudience(oidcConfig.token.audience.get().toArray(new String[] {}));
if (audience != null) {
builder.setExpectedAudience(audience);
} else {
builder.setSkipDefaultAudienceValidation();
}
Expand Down

0 comments on commit 6e761d9

Please sign in to comment.