Skip to content

Commit

Permalink
Merge pull request #3228 from ingef/feature/oidc_support_multiple_sig…
Browse files Browse the repository at this point in the history
…n_keys

parse all signing keys and compare kids with token
  • Loading branch information
thoniTUB authored Nov 27, 2023
2 parents 1ab464c + a319f2e commit 5cf7c89
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.bakdata.conquery.models.auth.oidc;

import java.lang.reflect.Array;
import java.security.PublicKey;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

import com.bakdata.conquery.io.storage.MetaStorage;
import com.bakdata.conquery.models.auth.ConqueryAuthenticationInfo;
import com.bakdata.conquery.models.auth.ConqueryAuthenticationRealm;
Expand All @@ -9,21 +15,19 @@
import com.bakdata.conquery.models.identifiable.ids.specific.UserId;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.BearerToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.pam.UnsupportedTokenException;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.exceptions.TokenNotActiveException;
import org.keycloak.jose.JOSEParser;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

/**
* This realm uses the configured public key to verify the signature of a provided JWT and extracts informations about
* the authenticated user from it.
Expand Down Expand Up @@ -65,17 +69,26 @@ public ConqueryAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken to
return null;
}
JwtPkceVerifyingRealmFactory.IdpConfiguration idpConfiguration = idpConfigurationOpt.get();
final BearerToken bearerToken = (BearerToken) token;

log.trace("Parsing token ({}) to extract key id from header", bearerToken.getToken());
final String keyId = JOSEParser.parse(bearerToken.getToken()).getHeader().getKeyId();
log.trace("Key id of token signer: {}", keyId);
final PublicKey publicKey = idpConfiguration.signingKeys().get(keyId);

if (publicKey == null) {
throw new UnsupportedTokenException("Token was signed by a key with an unknown Id: " + keyId);
}

log.trace("Creating token verifier");
TokenVerifier<AccessToken> verifier = TokenVerifier.create(((BearerToken) token).getToken(), AccessToken.class)
.withChecks(new TokenVerifier.RealmUrlCheck(idpConfiguration.getIssuer()), TokenVerifier.SUBJECT_EXISTS_CHECK, activeVerifier)
TokenVerifier<AccessToken> verifier = TokenVerifier.create(bearerToken.getToken(), AccessToken.class)
.withChecks(new TokenVerifier.RealmUrlCheck(idpConfiguration.issuer()), TokenVerifier.SUBJECT_EXISTS_CHECK, activeVerifier)
.withChecks(tokenChecks)
.publicKey(idpConfiguration.getPublicKey())
.publicKey(publicKey)
.audience(allowedAudience);

String subject;
log.trace("Verifying token");
AccessToken accessToken = null;
final AccessToken accessToken;
try {
verifier.verify();
accessToken = verifier.getToken();
Expand All @@ -84,14 +97,14 @@ public ConqueryAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken to
log.trace("Verification failed", e);
throw new IncorrectCredentialsException(e);
}
subject = accessToken.getSubject();
final String subject = accessToken.getSubject();

if (subject == null) {
// Should not happen, as sub is mandatory in an access_token
throw new UnsupportedTokenException("Unable to extract a subject from the provided token.");
}

log.trace("Authentication successfull for subject {}", subject);
log.trace("Authentication was successful for subject: {}", subject);


UserId userId = new UserId(subject);
Expand All @@ -102,7 +115,6 @@ public ConqueryAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken to
}

// Try alternative ids
List<UserId> alternativeIds = new ArrayList<>();
for (String alternativeIdClaim : alternativeIdClaims) {
Object altId = accessToken.getOtherClaims().get(alternativeIdClaim);
if (!(altId instanceof String)) {
Expand All @@ -120,15 +132,4 @@ public ConqueryAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken to
throw new UnknownAccountException("The user id was unknown: " + subject);
}

public static final TokenVerifier.Predicate<JsonWebToken> IS_ACTIVE = new TokenVerifier.Predicate<JsonWebToken>() {
@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (!t.isActive()) {
throw new TokenNotActiveException(t, "Token is not active");
}

return true;
}
};

}
Original file line number Diff line number Diff line change
@@ -1,28 +1,62 @@
package com.bakdata.conquery.models.config.auth;

import com.bakdata.conquery.apiv1.RequestAwareUriBuilder;
import java.io.IOException;
import java.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.ws.rs.client.Client;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import com.bakdata.conquery.apiv1.RequestHelper;
import com.bakdata.conquery.commands.ManagerNode;
import com.bakdata.conquery.io.cps.CPSType;
import com.bakdata.conquery.io.jackson.Jackson;
import com.bakdata.conquery.models.auth.ConqueryAuthenticationRealm;
import com.bakdata.conquery.models.auth.oidc.JwtPkceVerifyingRealm;
import com.bakdata.conquery.models.auth.web.AuthCookieFilter;
import com.bakdata.conquery.models.auth.web.RedirectingAuthFilter;
import com.bakdata.conquery.resources.admin.AdminServlet;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.AccessTokenResponse;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import io.dropwizard.validation.ValidationMethod;
import lombok.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
Expand All @@ -33,21 +67,6 @@
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.representations.AccessToken;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.ws.rs.client.Client;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response;

import java.io.IOException;
import java.net.URI;
import java.security.PublicKey;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Supplier;

/**
* A realm that verifies oauth tokens using PKCE.
*
Expand Down Expand Up @@ -85,7 +104,7 @@ public class JwtPkceVerifyingRealmFactory implements AuthenticationRealmFactory
/**
* See wellKnownEndpoint.
*/
private IdpConfiguration idpConfiguration;
private volatile IdpConfiguration idpConfiguration;

/**
* A leeway for token's expiration in seconds, this should be a short time.
Expand Down Expand Up @@ -119,25 +138,15 @@ public boolean isConfigurationAvailable() {
return wellKnownEndpoint != null || idpConfiguration != null;
}

@AllArgsConstructor
@Getter
public static class IdpConfiguration {

/**
* The public key information that is used to validate signed JWT.
* It can be retrieved from the IDP.
*/
@NonNull
private final PublicKey publicKey;

@NonNull
private final URI authorizationEndpoint;

@NonNull
private final URI tokenEndpoint;

@NotEmpty
private final String issuer;
/**
* @param signingKeys The public key information that is used to validate signed JWT.
* It can be retrieved from the IDP.
*/
public record IdpConfiguration(
@NonNull Map<String, PublicKey> signingKeys,
@NonNull URI authorizationEndpoint,
@NonNull URI tokenEndpoint,
@NotEmpty String issuer) {
}

public ConqueryAuthenticationRealm createRealm(ManagerNode manager) {
Expand Down Expand Up @@ -230,14 +239,19 @@ private IdpConfiguration retrieveIdpConfiguration(final Client client) {
}


final List<JWK> keys = jwks.getKeys();
if (keys.size() != 1) {
throw new IllegalStateException("Expected exactly 1 jwk for realm but found: " + keys.size());
}
// Filter for keys that are used for signing (discard encryption keys)
final Map<String, PublicKey> signingKeys = jwks.getKeys().stream()
.filter(jwk -> JWK.Use.SIG.name().equals(jwk.getPublicKeyUse()))
.collect(Collectors.toMap(JWK::getKeyId, JwtPkceVerifyingRealmFactory::getPublicKey));

JWK jwk = keys.get(0);

return new IdpConfiguration(getPublicKey(jwk), authorizationEndpoint, tokenEndpoint, issuer);
if (signingKeys.isEmpty()) {
throw new IllegalStateException("No signing keys could be retrieved from IDP. Received these JWKs (Key Ids):" + jwks.getKeys()
.stream()
.map(JWK::getKeyId));
}

return new IdpConfiguration(signingKeys, authorizationEndpoint, tokenEndpoint, issuer);
}


Expand Down Expand Up @@ -290,7 +304,7 @@ private URI initiateLogin(ContainerRequestContext request) {
return null;
}
JwtPkceVerifyingRealmFactory.IdpConfiguration idpConfiguration = idpConfigurationOpt.get();
return UriBuilder.fromUri(idpConfiguration.getAuthorizationEndpoint())
return UriBuilder.fromUri(idpConfiguration.authorizationEndpoint())
.queryParam("response_type", "code")
.queryParam("client_id", client)
.queryParam("redirect_uri", UriBuilder.fromUri(RequestHelper.getRequestURL(request)).path(AdminServlet.ADMIN_UI).build())
Expand Down Expand Up @@ -419,7 +433,7 @@ private AccessTokenResponse getTokenResponse(ContainerRequestContext request, Au

// Send the auth code/refresh token to the IDP to redeem them for a new access and refresh token
final TokenRequest tokenRequest = new TokenRequest(
UriBuilder.fromUri(idpConfiguration.getTokenEndpoint()).build(),
UriBuilder.fromUri(idpConfiguration.tokenEndpoint()).build(),
new ClientID(client),
authzGrant
);
Expand Down
Loading

0 comments on commit 5cf7c89

Please sign in to comment.