diff --git a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java index e065d7a..2596e07 100644 --- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java @@ -42,6 +42,7 @@ import org.gcube.common.gxhttp.util.ContentUtils; import org.gcube.common.gxrest.request.GXHTTPStringRequest; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import org.gcube.common.gxrest.response.inbound.JsonUtils; +import org.gcube.common.keycloak.model.JSONWebKeySet; import org.gcube.common.keycloak.model.ModelUtils; import org.gcube.common.keycloak.model.PublishedRealmRepresentation; import org.gcube.common.keycloak.model.TokenIntrospectionResponse; @@ -125,6 +126,23 @@ public class DefaultKeycloakClient implements KeycloakClient { } } + @Override + public URL getJWKEndpointURL(URL realmBaseURL) throws KeycloakClientException { + logger.debug("Constructing JWK endpoint URL starting from base URL: {}", realmBaseURL); + try { + URL jwkURL = null; + if (realmBaseURL.getPath().endsWith("/")) { + jwkURL = new URL(realmBaseURL, OPEN_ID_URI_PATH + "/" + JWK_URI_PATH); + } else { + jwkURL = new URL(realmBaseURL.toString() + "/" + OPEN_ID_URI_PATH + "/" + JWK_URI_PATH); + } + logger.debug("Constructed JWK URL is: {}", jwkURL); + return jwkURL; + } catch (MalformedURLException e) { + throw new KeycloakClientException("Cannot constructs JWK URL from base URL: " + realmBaseURL, e); + } + } + @Override public URL getIntrospectionEndpointURL(URL realmBaseURL) throws KeycloakClientException { logger.debug("Constructing introspection URL starting from base URL: {}", realmBaseURL); @@ -184,11 +202,21 @@ public class DefaultKeycloakClient implements KeycloakClient { try { return JsonUtils.fromJson(ContentUtils.toByteArray(realmURL.openStream()), PublishedRealmRepresentation.class); + } catch (Exception e) { throw new KeycloakClientException("Getting realm's info", e); } } + @Override + public JSONWebKeySet getRealmJSONWebKeySet(URL jwkURL) throws KeycloakClientException { + try { + return JsonUtils.fromJson(ContentUtils.toByteArray(jwkURL.openStream()), JSONWebKeySet.class); + } catch (Exception e) { + throw new KeycloakClientException("Getting realm's JWK", e); + } + } + @Override public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret) throws KeycloakClientException { diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java index 5f2b4f3..082cc8a 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java @@ -4,7 +4,7 @@ import java.net.URL; import java.util.List; import java.util.Map; -import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; +import org.gcube.common.keycloak.model.JSONWebKeySet; import org.gcube.common.keycloak.model.PublishedRealmRepresentation; import org.gcube.common.keycloak.model.TokenIntrospectionResponse; import org.gcube.common.keycloak.model.TokenResponse; @@ -14,6 +14,7 @@ public interface KeycloakClient { public static final String PROD_ROOT_SCOPE = "/d4science.research-infrastructures.eu"; public static final String OPEN_ID_URI_PATH = "protocol/openid-connect"; public static final String TOKEN_URI_PATH = "token"; + public static final String JWK_URI_PATH = "certs"; public static final String TOKEN_INTROSPECT_URI_PATH = "introspect"; public static final String AVATAR_URI_PATH = "account-avatar"; public final static String D4S_CONTEXT_HEADER_NAME = "X-D4Science-Context"; @@ -49,6 +50,15 @@ public interface KeycloakClient { */ URL getTokenEndpointURL(URL realmBaseURL) throws KeycloakClientException; + /** + * Constructs the Keycloak JWK endpoint {@link URL} from the realm's base URL. + * + * @param realmBaseURL the realm's base URL to use + * @return the Keycloak JWK endpoint URL + * @throws KeycloakClientException if something goes wrong discovering the endpoint URL + */ + URL getJWKEndpointURL(URL realmBaseURL) throws KeycloakClientException; + /** * Constructs the Keycloak introspection endpoint {@link URL} from the realm's base URL. * @@ -77,7 +87,7 @@ public interface KeycloakClient { URL getAvatarEndpointURL(URL realmBaseURL) throws KeycloakClientException; /** - * Get the realm info setup (RSA public_key, token-service URL, + * Gets the realm info setup (RSA public_key, token-service URL, * account-service URL and tokens-not-before setting) * * @param realmURL the realm URL @@ -86,6 +96,8 @@ public interface KeycloakClient { */ PublishedRealmRepresentation getRealmInfo(URL realmURL) throws KeycloakClientException; + JSONWebKeySet getRealmJSONWebKeySet(URL jwkURL) throws KeycloakClientException; + /** * Queries an OIDC token from the context's Keycloak server, by using provided clientId and client secret. * diff --git a/src/main/java/org/gcube/common/keycloak/model/JSONWebKeySet.java b/src/main/java/org/gcube/common/keycloak/model/JSONWebKeySet.java new file mode 100644 index 0000000..1142ad3 --- /dev/null +++ b/src/main/java/org/gcube/common/keycloak/model/JSONWebKeySet.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gcube.common.keycloak.model; + +import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Stian Thorgersen + */ +public class JSONWebKeySet { + + @JsonProperty("keys") + private JWK[] keys; + + public JWK[] getKeys() { + return keys; + } + + public void setKeys(JWK[] keys) { + this.keys = keys; + } + +} diff --git a/src/main/java/org/gcube/common/keycloak/model/JWK.java b/src/main/java/org/gcube/common/keycloak/model/JWK.java new file mode 100644 index 0000000..5d92295 --- /dev/null +++ b/src/main/java/org/gcube/common/keycloak/model/JWK.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gcube.common.keycloak.model; + +import java.util.HashMap; +import java.util.Map; + +import org.gcube.com.fasterxml.jackson.annotation.JsonAnyGetter; +import org.gcube.com.fasterxml.jackson.annotation.JsonAnySetter; +import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Stian Thorgersen + */ +public class JWK { + + public static final String KEY_ID = "kid"; + + public static final String KEY_TYPE = "kty"; + + public static final String ALGORITHM = "alg"; + + public static final String PUBLIC_KEY_USE = "use"; + + public enum Use { + SIG("sig"), + ENCRYPTION("enc"); + + private String str; + + Use(String str) { + this.str = str; + } + + public String asString() { + return str; + } + } + + @JsonProperty(KEY_ID) + private String keyId; + + @JsonProperty(KEY_TYPE) + private String keyType; + + @JsonProperty(ALGORITHM) + private String algorithm; + + @JsonProperty(PUBLIC_KEY_USE) + private String publicKeyUse; + + protected Map otherClaims = new HashMap(); + + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public String getKeyType() { + return keyType; + } + + public void setKeyType(String keyType) { + this.keyType = keyType; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getPublicKeyUse() { + return publicKeyUse; + } + + public void setPublicKeyUse(String publicKeyUse) { + this.publicKeyUse = publicKeyUse; + } + + @JsonAnyGetter + public Map getOtherClaims() { + return otherClaims; + } + + @JsonAnySetter + public void setOtherClaims(String name, Object value) { + otherClaims.put(name, value); + } + +} diff --git a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java index 480d7cc..97e172f 100644 --- a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java +++ b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertTrue; import java.net.URL; import java.util.Collections; +import org.gcube.common.keycloak.model.JSONWebKeySet; import org.gcube.common.keycloak.model.ModelUtils; import org.gcube.common.keycloak.model.PublishedRealmRepresentation; import org.gcube.common.keycloak.model.TokenIntrospectionResponse; @@ -126,13 +127,26 @@ public class TestKeycloakClient { } @Test - public void test10QueryRealmInfo() throws Exception { - logger.info("*** [1.0] Start testing query realm info..."); + public void test10aQueryRealmInfo() throws Exception { + logger.info("*** [1.0a] Start testing query realm info..."); KeycloakClient client = KeycloakClientFactory.newInstance(); PublishedRealmRepresentation realmInfo = client.getRealmInfo(client.getRealmBaseURL(DEV_ROOT_CONTEXT)); - logger.info("*** [1.0] Realm info public key PEM: {}", realmInfo.getPublicKeyPem()); - logger.info("*** [1.0] Realm info public key: {}", realmInfo.getPublicKey()); + logger.info("*** [1.0a] Realm info public key PEM: {}", realmInfo.getPublicKeyPem()); + logger.info("*** [1.0a] Realm info public key: {}", realmInfo.getPublicKey()); + } + + @Test + public void test10bQueryRealmJWK() throws Exception { + logger.info("*** [1.0b] Start testing query realm JWK..."); + KeycloakClient client = KeycloakClientFactory.newInstance(); + JSONWebKeySet jsonWebKeySet = client + .getRealmJSONWebKeySet(client.getJWKEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT))); + + for (int i = 0; i < jsonWebKeySet.getKeys().length; i++) { + logger.info("*** [1.0b] Realm JWK public key {} algorithm: {}", i, + jsonWebKeySet.getKeys()[i].getAlgorithm()); + } } @Test