From d97ead7a49c49b9901bea26eec108353aec8c068 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 30 Jul 2021 10:16:34 +0200 Subject: [PATCH 01/24] chore: create FAPI resource service as required by the FAPI Conformance Tests fixes gravitee-io/issues5934 --- gravitee-am-fapi-resource-api/pom.xml | 57 ++++++++ .../io/gravitee/sample/fapi/api/FapiApi.java | 128 ++++++++++++++++++ .../fapi/api/FapiResourceApiHandler.java | 88 ++++++++++++ pom.xml | 1 + 4 files changed, 274 insertions(+) create mode 100644 gravitee-am-fapi-resource-api/pom.xml create mode 100644 gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiApi.java create mode 100644 gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiResourceApiHandler.java diff --git a/gravitee-am-fapi-resource-api/pom.xml b/gravitee-am-fapi-resource-api/pom.xml new file mode 100644 index 0000000000..3f34e1198d --- /dev/null +++ b/gravitee-am-fapi-resource-api/pom.xml @@ -0,0 +1,57 @@ + + + + 4.0.0 + + + io.gravitee.am + gravitee-am-parent + 3.10.0-SNAPSHOT + + + io.gravitee.am.fapi + gravitee-fapi-resource-api + 3.10.0-SNAPSHOT + Gravitee.io - OIDC FAPI - Resource API + + + 11 + 11 + + + + + io.vertx + vertx-web + + + + commons-cli + commons-cli + 1.4 + + + com.nimbusds + nimbus-jose-jwt + 8.17 + + + \ No newline at end of file diff --git a/gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiApi.java b/gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiApi.java new file mode 100644 index 0000000000..8ed261edb2 --- /dev/null +++ b/gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiApi.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.sample.fapi.api; + +import io.vertx.core.Vertx; +import io.vertx.core.http.*; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.PfxOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.StaticHandler; +import org.apache.commons.cli.*; + +import java.util.Set; + +import static io.vertx.core.http.HttpMethod.GET; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + */ +public class FapiApi { + + public static final String CONF_HOST = "host"; + public static final String CONF_PORT = "port"; + public static final String CONF_TRUST_STORE_PATH = "trustStorePath"; + public static final String CONF_TRUST_STORE_TYPE = "trustStoreType"; + public static final String CONF_TRUST_STORE_PASSWORD = "trustStorePassword"; + public static final String CONF_KEY_STORE_PATH = "keyStorePath"; + public static final String CONF_KEY_STORE_TYPE = "keyStoreType"; + public static final String CONF_KEY_STORE_PASSWORD = "keyStorePassword"; + + public static void main(String[] args) throws Exception { + CommandLine cmd = parseArgs(args); + HttpServerOptions options = buildHttpOptions(cmd); + + Vertx vertx = Vertx.vertx(); + HttpServer server = vertx.createHttpServer(options); + + Router router = Router.router(vertx); + router.route().handler(StaticHandler.create()); + + router.route("/fapi/api") + .method(GET) + .produces("application/json") + .handler(new FapiResourceApiHandler()); + + server.requestHandler(router) + + .listen(); + System.out.println("Server listening on port " + cmd.getOptionValue(CONF_PORT, "9443")); + } + + private static HttpServerOptions buildHttpOptions(CommandLine cmd) { + HttpServerOptions options = new HttpServerOptions(); + options.setPort(Integer.parseInt(cmd.getOptionValue(CONF_PORT, "9443"))); + options.setHost(cmd.getOptionValue(CONF_HOST, "0.0.0.0")); + options.setSsl(true); + options.setUseAlpn(false); + options.setEnabledSecureTransportProtocols(Set.of("TLSv1.2", "TLSv1.3")); + options.addEnabledCipherSuite("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256") + .addEnabledCipherSuite("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") + .addEnabledCipherSuite("TLS_DHE_RSA_WITH_AES_256_GCM_SHA384") + .addEnabledCipherSuite("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); + options.setClientAuth(ClientAuth.REQUEST); + + if (cmd.getOptionValue(CONF_TRUST_STORE_TYPE, "pkcs12").equalsIgnoreCase("pkcs12")) { + options.setPfxTrustOptions(new PfxOptions() + .setPath(cmd.getOptionValue(CONF_TRUST_STORE_PATH)) + .setPassword(cmd.getOptionValue(CONF_TRUST_STORE_PASSWORD))); + } else{ + options.setTrustStoreOptions(new JksOptions() + .setPath(cmd.getOptionValue(CONF_TRUST_STORE_PATH)) + .setPassword(cmd.getOptionValue(CONF_TRUST_STORE_PASSWORD))); + } + + if (cmd.getOptionValue(CONF_KEY_STORE_TYPE, "pkcs12").equalsIgnoreCase("pkcs12")) { + options.setPfxKeyCertOptions(new PfxOptions() + .setPath(cmd.getOptionValue(CONF_KEY_STORE_PATH)) + .setPassword(cmd.getOptionValue(CONF_KEY_STORE_PASSWORD))); + } else{ + options.setKeyStoreOptions(new JksOptions() + .setPath(cmd.getOptionValue(CONF_KEY_STORE_PATH)) + .setPassword(cmd.getOptionValue(CONF_KEY_STORE_PASSWORD))); + } + return options; + } + + private static CommandLine parseArgs(String[] args) throws ParseException { + Options options = new Options(); + Option host = new Option(CONF_HOST, true, "binding interface"); + host.setRequired(false); + options.addOption(host); + + Option port = new Option(CONF_PORT, true, "listening port"); + port.setRequired(false); + options.addOption(port); + // TODO create each options with required true + options.addOption(CONF_TRUST_STORE_PATH, true, "truststore path"); + options.addOption(CONF_TRUST_STORE_TYPE, true, "truststore type"); + options.addOption(CONF_TRUST_STORE_PASSWORD, true, "truststore password"); + options.addOption(CONF_KEY_STORE_PATH, true, "keystore path"); + options.addOption(CONF_KEY_STORE_TYPE, true, "keystore type"); + options.addOption(CONF_KEY_STORE_PASSWORD, true, "keystore password"); + + CommandLineParser parser = new DefaultParser(); + CommandLine cmd = parser.parse( options, args); + + if (!(cmd.hasOption(CONF_KEY_STORE_PATH) && cmd.hasOption(CONF_KEY_STORE_PATH))) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp( "java io.gravitee.sample.fapi.api.FapiApi ", options ); + System.exit(1); + } + + return cmd; + } +} diff --git a/gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiResourceApiHandler.java b/gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiResourceApiHandler.java new file mode 100644 index 0000000000..d04a7227fb --- /dev/null +++ b/gravitee-am-fapi-resource-api/src/main/java/io/gravitee/sample/fapi/api/FapiResourceApiHandler.java @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.sample.fapi.api; + +import com.nimbusds.jose.util.Base64URL; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.impl.jose.JWT; +import io.vertx.ext.web.RoutingContext; + +import javax.net.ssl.SSLSession; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + + +public class FapiResourceApiHandler implements Handler { + + @Override + public void handle(RoutingContext routingContext) { + try { + final SSLSession sslSession = routingContext.request().sslSession(); + Certificate[] peerCertificates = sslSession.getPeerCertificates(); + X509Certificate peerCertificate = (X509Certificate) peerCertificates[0]; + + String thumbprint256 = getThumbprint(peerCertificate, "SHA-256"); + + final String auth = routingContext.request().getHeader(HttpHeaders.AUTHORIZATION); + if (auth != null && auth.startsWith("Bearer ")) { + + String jwtString = auth.replaceFirst("Bearer ", ""); + final JsonObject jwt = JWT.parse(jwtString).getJsonObject("payload"); + + if (jwt.containsKey("cnf") && thumbprint256.equals(jwt.getJsonObject("cnf").getString("x5t#S256"))) { + //response ok + final int statusCode = 200; + routingContext.response() + .putHeader("content-type", "application/json") + .putHeader("Date", DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())) + .putHeader("x-fapi-auth-date", routingContext.request().getHeader("x-fapi-auth-date")) + .putHeader("x-fapi-customer-ip-address", routingContext.request().getHeader("x-fapi-customer-ip-address")) + .putHeader("x-fapi-interaction-id", Optional.ofNullable(routingContext.request().getHeader("x-fapi-interaction-id")).orElse(UUID.randomUUID().toString())) + .setStatusCode(statusCode) + .end(jwt.encodePrettily()); // return JWT as JSON object + } + } + + // default response unauthorized + routingContext.response() + .putHeader("content-type", "application/json") + .setStatusCode(401).end(); + } catch (Exception e) { + e.printStackTrace(); + routingContext.fail(500, e); + } + + } + + public static String getThumbprint(X509Certificate cert, String algorithm) + throws NoSuchAlgorithmException, CertificateEncodingException { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] der = cert.getEncoded(); + md.update(der); + byte[] digest = md.digest(); + return Base64URL.encode(digest).toString(); + } +} diff --git a/pom.xml b/pom.xml index 587239430d..41159abe1e 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ gravitee-am-ui gravitee-am-resource gravitee-am-botdetection + gravitee-am-fapi-resource-api From 4f6b14c5604b0f6cc44b30a31806ec8d3d30f6b9 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 30 Jul 2021 11:08:21 +0200 Subject: [PATCH 02/24] feat: manage Client Certificate Bound AccessToken fixes gravitee-io/issues#4028 --- .../io/gravitee/am/common/jwt/Claims.java | 6 ++- .../java/io/gravitee/am/common/jwt/JWT.java | 10 +++++ .../handler/common/utils/ConstantKeys.java | 3 +- .../auth/provider/CertificateUtils.java | 39 +++++++++++++++++++ .../ClientCertificateAuthProvider.java | 10 ++++- .../ClientSelfSignedAuthProvider.java | 16 ++------ .../endpoint/token/TokenEndpoint.java | 6 +++ .../introspection/IntrospectionResponse.java | 10 +++++ .../impl/IntrospectionServiceImpl.java | 3 ++ .../oauth2/service/request/OAuth2Request.java | 13 +++++++ .../oauth2/service/request/TokenRequest.java | 4 ++ .../service/token/impl/AccessToken.java | 13 +++++++ .../service/token/impl/TokenServiceImpl.java | 7 +++- .../discovery/OpenIDProviderMetadata.java | 12 +++++- .../impl/OpenIDDiscoveryServiceImpl.java | 9 +++++ .../ClientCertificateAuthProviderTest.java | 6 ++- .../discovery/OpenIDDiscoveryServiceTest.java | 29 ++++++++++++++ .../repository/oauth2/model/AccessToken.java | 16 ++++++++ 18 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/CertificateUtils.java diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/Claims.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/Claims.java index 1cc94b193a..cfbd781e95 100644 --- a/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/Claims.java +++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/Claims.java @@ -97,10 +97,14 @@ public interface Claims { * The oauth 2.0 "scopes" */ String scope = "scope"; + /** + * The oauth 2.0 confirmation method + */ + String cnf = "cnf"; static List claims() { return Arrays.asList(iss, sub, aud, exp, nbf, iat, - jti, domain, claims, ip_address, user_agent, scope); + jti, domain, claims, ip_address, user_agent, scope, cnf); } } diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/JWT.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/JWT.java index caa612d50a..7fa8cb9e42 100644 --- a/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/JWT.java +++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/jwt/JWT.java @@ -27,6 +27,8 @@ */ public class JWT extends HashMap { + public static final String CONFIRMATION_METHOD_X509_THUMBPRINT = "x5t#S256"; + public JWT() { } public JWT(Map claims) { @@ -116,4 +118,12 @@ public void setClaimsRequestParameter(Object claims) { public boolean hasScope(String scope) { return getScope() != null && Arrays.asList(getScope().split("\\s+")).contains(scope); } + + public void setConfirmationMethod(Map confirmation) { + put(Claims.cnf, confirmation); + } + + public Object getConfirmationMethod() { + return get(Claims.cnf); + } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java index ddace6c054..6af71f388e 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java @@ -15,8 +15,6 @@ */ package io.gravitee.am.gateway.handler.common.utils; -import io.gravitee.am.service.validators.AccountSettingsValidator; - /** * @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com) * @author GraviteeSource Team @@ -55,6 +53,7 @@ public interface ConstantKeys { String SKIP_ACTION_KEY = "skipAction"; String TRANSACTION_ID_KEY = "tid"; String OIDC_PROVIDER_ID_TOKEN_KEY = "op_id_token"; + String PEER_CERTIFICATE_THUMBPRINT = "x509_thumbprint_s256"; // enrich authentication flow keys String AUTH_FLOW_CONTEXT_KEY = "authFlowContext"; diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/CertificateUtils.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/CertificateUtils.java new file mode 100644 index 0000000000..8119553042 --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/CertificateUtils.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.resources.auth.provider; + +import com.nimbusds.jose.util.Base64URL; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class CertificateUtils { + + public static String getThumbprint(X509Certificate cert, String algorithm) + throws NoSuchAlgorithmException, CertificateEncodingException { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] der = cert.getEncoded(); + md.update(der); + byte[] digest = md.digest(); + return Base64URL.encode(digest).toString(); + } +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java index 22ce3120d9..3432aa11bf 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java @@ -16,6 +16,7 @@ package io.gravitee.am.gateway.handler.oauth2.resources.auth.provider; import io.gravitee.am.common.oidc.ClientAuthenticationMethod; +import io.gravitee.am.gateway.handler.common.utils.ConstantKeys; import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException; import io.gravitee.am.model.oidc.Client; import io.vertx.core.AsyncResult; @@ -27,12 +28,16 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; +import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.List; +import static io.gravitee.am.gateway.handler.oauth2.resources.auth.provider.CertificateUtils.getThumbprint; + /** * Client Authentication method : tls_client_auth * @@ -67,17 +72,18 @@ public void handle(Client client, RoutingContext context, Handler thumbprint256.equals(jwk.getX5tS256()) || thumbprint.equals(jwk.getX5t())); if (match) { + context.put(ConstantKeys.PEER_CERTIFICATE_THUMBPRINT, thumbprint256); handler.handle(Future.succeededFuture(client)); } else { handler.handle(Future.failedFuture(new InvalidClientException("Invalid client: invalid self-signed certificate"))); @@ -89,12 +89,4 @@ public void handle(Client client, RoutingContext context, Handler context.response() .putHeader(HttpHeaders.CACHE_CONTROL, "no-store") diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/IntrospectionResponse.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/IntrospectionResponse.java index 5abf79f36d..3520d1acbc 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/IntrospectionResponse.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/IntrospectionResponse.java @@ -16,6 +16,9 @@ package io.gravitee.am.gateway.handler.oauth2.service.introspection; import io.gravitee.am.common.jwt.JWT; +import io.gravitee.am.common.oidc.idtoken.Claims; + +import java.util.Map; /** * The introspection response. @@ -72,4 +75,11 @@ public String getTokenType() { public void setTokenType(String tokenType) { put(TOKEN_TYPE, tokenType); } + + public void setConfirmationMethod(Map confirmationMethod) { + put(Claims.cnf, confirmationMethod); + } + public Object getConfirmationMethod() { + return get(Claims.cnf); + } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java index 3acf739c62..2c2bb3deb8 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java @@ -75,6 +75,9 @@ private IntrospectionResponse convert(AccessToken accessToken, User user) { if (accessToken.getAdditionalInformation() != null && !accessToken.getAdditionalInformation().isEmpty()) { accessToken.getAdditionalInformation().forEach((k, v) -> introspectionResponse.putIfAbsent(k, v)); } + + introspectionResponse.setConfirmationMethod(accessToken.getConfirmationMethod()); + // remove "aud" claim due to some backend APIs unable to verify the "aud" value // see introspectionResponse.remove(Claims.aud); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/OAuth2Request.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/OAuth2Request.java index 83f3b7281a..eedc79deac 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/OAuth2Request.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/OAuth2Request.java @@ -117,6 +117,11 @@ public class OAuth2Request extends BaseRequest { private MultiValueMap pathParameters = null; + /** + * REQUIRED for Certificate Bound Access Token + */ + private String confirmationMethodX5S256; + public String getClientId() { return clientId; } @@ -217,6 +222,14 @@ public void setPermissions(List permissions) { this.permissions = permissions; } + public String getConfirmationMethodX5S256() { + return confirmationMethodX5S256; + } + + public void setConfirmationMethodX5S256(String confirmationMethodX5S256) { + this.confirmationMethodX5S256 = confirmationMethodX5S256; + } + public boolean shouldGenerateIDToken() { if (getResponseType() != null && ResponseType.CODE_TOKEN.equals(getResponseType())) { return false; diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java index 7e61b5bbf7..fcbb08b754 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java @@ -65,6 +65,7 @@ public class TokenRequest extends OAuth2Request { */ private String requestingPartyToken; + public String getUsername() { return username; } @@ -158,6 +159,9 @@ public OAuth2Request createOAuth2Request() { // set UMA 2.0 permissions oAuth2Request.setPermissions(getPermissions()); + // certificate bound access token + oAuth2Request.setConfirmationMethodX5S256(getConfirmationMethodX5S256()); + return oAuth2Request; } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/AccessToken.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/AccessToken.java index 202987e04c..45852f60b8 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/AccessToken.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/AccessToken.java @@ -15,18 +15,31 @@ */ package io.gravitee.am.gateway.handler.oauth2.service.token.impl; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.gravitee.am.gateway.handler.oauth2.service.token.Token; import io.gravitee.am.gateway.handler.oauth2.service.token.jackson.AccessTokenSerializer; +import java.util.Map; + /** * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) * @author GraviteeSource Team */ @JsonSerialize(using = AccessTokenSerializer.class) public class AccessToken extends Token { + @JsonProperty("cnf") + public Map confirmationMethod; public AccessToken(String value) { super(value); } + + public Map getConfirmationMethod() { + return confirmationMethod; + } + + public void setConfirmationMethod(Map confirmationMethod) { + this.confirmationMethod = confirmationMethod; + } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java index 893863bc0e..03cddfd32b 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java @@ -37,13 +37,13 @@ import io.gravitee.am.gateway.handler.oauth2.service.token.TokenManager; import io.gravitee.am.gateway.handler.oauth2.service.token.TokenService; import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDDiscoveryService; -import io.gravitee.am.model.AuthenticationFlowContext; import io.gravitee.am.model.TokenClaim; import io.gravitee.am.model.User; import io.gravitee.am.model.oidc.Client; import io.gravitee.am.model.uma.PermissionRequest; import io.gravitee.am.repository.oauth2.api.AccessTokenRepository; import io.gravitee.am.repository.oauth2.api.RefreshTokenRepository; +import io.gravitee.common.util.Maps; import io.gravitee.common.util.MultiValueMap; import io.gravitee.gateway.api.ExecutionContext; import io.gravitee.gateway.api.context.SimpleExecutionContext; @@ -242,6 +242,9 @@ private Token convert(JWT accessToken, String encodedAccessToken, String encoded */ private Token convertAccessToken(JWT jwt) { AccessToken accessToken = new AccessToken(jwt.getJti()); + if (jwt.getConfirmationMethod() != null) { + accessToken.setConfirmationMethod((Map) jwt.getConfirmationMethod()); + } return convert(accessToken, jwt); } @@ -271,6 +274,8 @@ private JWT createAccessTokenJWT(OAuth2Request request, Client client, User user // set exp claim jwt.setExp(Instant.ofEpochSecond(jwt.getIat()).plusSeconds(client.getAccessTokenValiditySeconds()).getEpochSecond()); + jwt.setConfirmationMethod(Maps.builder().put(JWT.CONFIRMATION_METHOD_X509_THUMBPRINT, request.getConfirmationMethodX5S256()).build()); + // set claims parameter (only for an access token) // useful for UserInfo Endpoint to request for specific claims MultiValueMap requestParameters = request.parameters(); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java index 7733db80fd..62f58e2beb 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java @@ -164,11 +164,13 @@ public class OpenIDProviderMetadata { @JsonProperty("request_object_endpoint") private String requestObjectEndpoint; - public String getIssuer() { return issuer; } + @JsonProperty("tls_client_certificate_bound_access_tokens") + private Boolean tlsClientCertificateBoundAccessTokens = Boolean.FALSE; + public void setIssuer(String issuer) { this.issuer = issuer; } @@ -524,4 +526,12 @@ public String getRequestObjectEndpoint() { public void setRequestObjectEndpoint(String requestObjectEndpoint) { this.requestObjectEndpoint = requestObjectEndpoint; } + + public Boolean getTlsClientCertificateBoundAccessTokens() { + return tlsClientCertificateBoundAccessTokens; + } + + public void setTlsClientCertificateBoundAccessTokens(Boolean tlsClientCertificateBoundAccessTokens) { + this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; + } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java index fa382d53ee..cc868b65d9 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java @@ -26,6 +26,7 @@ import io.gravitee.am.service.utils.GrantTypeUtils; import io.gravitee.am.service.utils.ResponseTypeUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import java.util.Arrays; import java.util.Collections; @@ -59,6 +60,9 @@ public class OpenIDDiscoveryServiceImpl implements OpenIDDiscoveryService { @Autowired private ScopeService scopeService; + @Autowired + private Environment env; + @Override public OpenIDProviderMetadata getConfiguration(String basePath) { OpenIDProviderMetadata openIDProviderMetadata = new OpenIDProviderMetadata(); @@ -120,6 +124,11 @@ public OpenIDProviderMetadata getConfiguration(String basePath) { openIDProviderMetadata.setAcrValuesSupported(AcrValues.values()); + // certificate bound accessToken requires TLS & Client Auth + final Boolean secured = env.getProperty("http.secured", Boolean.class, false); + final String clientAuth = env.getProperty("http.ssl.clientAuth", String.class, "none"); + openIDProviderMetadata.setTlsClientCertificateBoundAccessTokens(secured && !clientAuth.equalsIgnoreCase("none")); + return openIDProviderMetadata; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java index 7ef3016f65..db65c5861e 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java @@ -15,6 +15,7 @@ */ package io.gravitee.am.gateway.handler.oauth2.resources.auth.provider; +import io.gravitee.am.gateway.handler.common.utils.ConstantKeys; import io.gravitee.am.model.oidc.Client; import io.vertx.reactivex.core.http.HttpServerRequest; import io.vertx.reactivex.ext.web.RoutingContext; @@ -32,8 +33,7 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * Client Authentication method : tls_client_auth @@ -103,6 +103,7 @@ public void authorized_client() throws Exception { SSLSession sslSession = mock(SSLSession.class); X509Certificate certificate = mock(X509Certificate.class); + when(certificate.getEncoded()).thenReturn("MYCERTIFICATE".getBytes()); Principal subjectDN = mock(Principal.class); when(client.getTlsClientAuthSubjectDn()).thenReturn("CN=localhost, O=GraviteeSource, C=FR"); @@ -119,6 +120,7 @@ public void authorized_client() throws Exception { latch.countDown(); Assert.assertNotNull(clientAsyncResult); Assert.assertNotNull(clientAsyncResult.result()); + verify(context).put(eq(ConstantKeys.PEER_CERTIFICATE_THUMBPRINT), any()); }); assertTrue(latch.await(10, TimeUnit.SECONDS)); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDDiscoveryServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDDiscoveryServiceTest.java index 7cc896e307..37670aa328 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDDiscoveryServiceTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDDiscoveryServiceTest.java @@ -21,12 +21,16 @@ import io.gravitee.am.gateway.handler.oidc.service.discovery.impl.OpenIDDiscoveryServiceImpl; import io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils; import io.gravitee.am.model.Domain; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.env.Environment; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** @@ -45,6 +49,21 @@ public class OpenIDDiscoveryServiceTest { @Mock private ScopeService scopeService; + @Mock + private Environment environment; + + @Before + public void prepare() { + Mockito.when(environment.getProperty("http.secured", Boolean.class, false)).thenReturn(false); + Mockito.when(environment.getProperty("http.ssl.clientAuth", String.class, "none")).thenReturn("none"); + } + + private void enableMtls() { + Mockito.reset(environment); + Mockito.when(environment.getProperty("http.secured", Boolean.class, false)).thenReturn(true); + Mockito.when(environment.getProperty("http.ssl.clientAuth", String.class, "none")).thenReturn("required"); + } + @Test public void shouldContain_request_parameter_supported() { OpenIDProviderMetadata openIDProviderMetadata = openIDDiscoveryService.getConfiguration("/"); @@ -83,5 +102,15 @@ public void shouldContain_request_object_signing_alg_values_supported() { OpenIDProviderMetadata openIDProviderMetadata = openIDDiscoveryService.getConfiguration("/"); assertTrue(JWAlgorithmUtils.getSupportedRequestObjectSigningAlg().containsAll(openIDProviderMetadata.getRequestObjectSigningAlgValuesSupported())); } + + @Test + public void shouldContain_tls_client_certificate_bound_access_tokens() { + OpenIDProviderMetadata openIDProviderMetadata = openIDDiscoveryService.getConfiguration("/"); + assertFalse(openIDProviderMetadata.getTlsClientCertificateBoundAccessTokens()); + enableMtls(); + openIDProviderMetadata = openIDDiscoveryService.getConfiguration("/"); + assertTrue(openIDProviderMetadata.getTlsClientCertificateBoundAccessTokens()); + } + } diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/AccessToken.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/AccessToken.java index c4c88a0d6d..b96395cff7 100644 --- a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/AccessToken.java +++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/AccessToken.java @@ -15,6 +15,8 @@ */ package io.gravitee.am.repository.oauth2.model; +import java.util.Map; + /** * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Team @@ -33,6 +35,20 @@ public class AccessToken extends Token { */ private String authorizationCode; + /** + * Confirmation method https://datatracker.ietf.org/doc/html/rfc8705#section-3.1 + * This attribute isn't persisted, it is only here to allow insertion into the Introspection response + */ + private Map confirmationMethod; + + public Map getConfirmationMethod() { + return confirmationMethod; + } + + public void setConfirmationMethod(Map confirmationMethod) { + this.confirmationMethod = confirmationMethod; + } + public String getRefreshToken() { return refreshToken; } From 9392a543e22acbdde73367b138bdc62f750189d1 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 30 Jul 2021 14:00:11 +0200 Subject: [PATCH 03/24] fix: keep query parameters coming from the redirect_uri for an error response fixes gravitee-io/issues#5939 --- .../authorization/AuthorizationRequestFailureHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java index df855e3f6c..126c62050e 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java @@ -211,7 +211,8 @@ private String append(String base, Map query, boolean fragment) .host(redirectUri.getHost()) .port(redirectUri.getPort()) .userInfo(redirectUri.getUserInfo()) - .path(redirectUri.getPath()); + .path(redirectUri.getPath()) + .query(redirectUri.getQuery()); // append error parameters in "application/x-www-form-urlencoded" format if (fragment) { From c1ff7ec5e7cd4a2dcfa7a57032928e18c78a2b83 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 3 Aug 2021 12:28:16 +0200 Subject: [PATCH 04/24] feat: manage FAPI security profile settings for domain fixes gravitee-io/issues#5951 --- .../gravitee/am/model/oidc/OIDCSettings.java | 11 ++++ .../model/oidc/SecurityProfileSettings.java | 41 ++++++++++++ .../management/MongoDomainRepository.java | 26 ++++++++ .../model/oidc/OIDCSettingsMongo.java | 9 +++ .../oidc/SecurityProfileSettingsMongo.java | 35 ++++++++++ .../model/openid/PatchOIDCSettings.java | 22 +++++++ .../openid/PatchSecurityProfileSettings.java | 53 +++++++++++++++ gravitee-am-ui/src/app/app-routing.module.ts | 15 +++++ gravitee-am-ui/src/app/app.module.ts | 2 + .../oidc-profile/oidc-profile.component.html | 53 +++++++++++++++ .../oidc-profile/oidc-profile.component.scss | 0 .../oidc-profile.component.spec.ts | 40 ++++++++++++ .../oidc-profile/oidc-profile.component.ts | 65 +++++++++++++++++++ 13 files changed, 372 insertions(+) create mode 100644 gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/SecurityProfileSettings.java create mode 100644 gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/SecurityProfileSettingsMongo.java create mode 100644 gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchSecurityProfileSettings.java create mode 100644 gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.html create mode 100644 gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.scss create mode 100644 gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.spec.ts create mode 100644 gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.ts diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/OIDCSettings.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/OIDCSettings.java index c81f9eba96..57b7ac8dac 100644 --- a/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/OIDCSettings.java +++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/OIDCSettings.java @@ -26,6 +26,8 @@ public class OIDCSettings { private ClientRegistrationSettings clientRegistrationSettings; + private SecurityProfileSettings securityProfileSettings; + /** * Enable redirect_uri strict matching during OIDC flow (check for redirect_uri_mismatch exception) */ @@ -60,9 +62,18 @@ public void setPostLogoutRedirectUris(List postLogoutRedirectUris) { this.postLogoutRedirectUris = postLogoutRedirectUris; } + public SecurityProfileSettings getSecurityProfileSettings() { + return securityProfileSettings; + } + + public void setSecurityProfileSettings(SecurityProfileSettings securityProfileSettings) { + this.securityProfileSettings = securityProfileSettings; + } + public static OIDCSettings defaultSettings() { OIDCSettings defaultSettings = new OIDCSettings(); defaultSettings.setClientRegistrationSettings(ClientRegistrationSettings.defaultSettings()); + defaultSettings.setSecurityProfileSettings(SecurityProfileSettings.defaultSettings()); defaultSettings.setRedirectUriStrictMatching(false); return defaultSettings; } diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/SecurityProfileSettings.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/SecurityProfileSettings.java new file mode 100644 index 0000000000..d96497ec90 --- /dev/null +++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/SecurityProfileSettings.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.model.oidc; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class SecurityProfileSettings { + + /** + * Apply the standard Financial-grade API security profile (version 1.0). + */ + private boolean enablePlainFapi; + + public boolean isEnablePlainFapi() { + return enablePlainFapi; + } + + public void setEnablePlainFapi(boolean enablePlainFapi) { + this.enablePlainFapi = enablePlainFapi; + } + + public static SecurityProfileSettings defaultSettings() { + //By default all boolean are set to false. + return new SecurityProfileSettings(); + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java index 1602beddd9..97d1983e21 100644 --- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoDomainRepository.java @@ -30,6 +30,7 @@ import io.gravitee.am.model.login.WebAuthnSettings; import io.gravitee.am.model.oidc.ClientRegistrationSettings; import io.gravitee.am.model.oidc.OIDCSettings; +import io.gravitee.am.model.oidc.SecurityProfileSettings; import io.gravitee.am.model.scim.SCIMSettings; import io.gravitee.am.model.uma.UMASettings; import io.gravitee.am.repository.management.api.DomainRepository; @@ -37,6 +38,7 @@ import io.gravitee.am.repository.mongodb.management.internal.model.*; import io.gravitee.am.repository.mongodb.management.internal.model.oidc.ClientRegistrationSettingsMongo; import io.gravitee.am.repository.mongodb.management.internal.model.oidc.OIDCSettingsMongo; +import io.gravitee.am.repository.mongodb.management.internal.model.oidc.SecurityProfileSettingsMongo; import io.gravitee.am.repository.mongodb.management.internal.model.uma.UMASettingsMongo; import io.reactivex.*; import org.bson.BsonDocument; @@ -224,6 +226,7 @@ private static OIDCSettings convert(OIDCSettingsMongo oidcMongo) { OIDCSettings oidcSettings = new OIDCSettings(); oidcSettings.setRedirectUriStrictMatching(oidcMongo.isRedirectUriStrictMatching()); oidcSettings.setClientRegistrationSettings(convert(oidcMongo.getClientRegistrationSettings())); + oidcSettings.setSecurityProfileSettings(convert(oidcMongo.getSecurityProfileSettings())); oidcSettings.setPostLogoutRedirectUris(oidcMongo.getPostLogoutRedirectUris()); return oidcSettings; @@ -258,6 +261,17 @@ private static ClientRegistrationSettings convert(ClientRegistrationSettingsMong return result; } + private static SecurityProfileSettings convert(SecurityProfileSettingsMongo profiles) { + if (profiles == null) { + return null; + } + + SecurityProfileSettings result = new SecurityProfileSettings(); + result.setEnablePlainFapi(profiles.isEnablePlainFapi()); + + return result; + } + private static OIDCSettingsMongo convert(OIDCSettings oidc) { if (oidc == null) { return null; @@ -266,6 +280,7 @@ private static OIDCSettingsMongo convert(OIDCSettings oidc) { OIDCSettingsMongo oidcSettings = new OIDCSettingsMongo(); oidcSettings.setRedirectUriStrictMatching(oidc.isRedirectUriStrictMatching()); oidcSettings.setClientRegistrationSettings(convert(oidc.getClientRegistrationSettings())); + oidcSettings.setSecurityProfileSettings(convert(oidc.getSecurityProfileSettings())); oidcSettings.setPostLogoutRedirectUris(oidc.getPostLogoutRedirectUris()); return oidcSettings; @@ -300,6 +315,17 @@ private static ClientRegistrationSettingsMongo convert(ClientRegistrationSetting return result; } + private static SecurityProfileSettingsMongo convert(SecurityProfileSettings profile) { + if (profile == null) { + return null; + } + + SecurityProfileSettingsMongo result = new SecurityProfileSettingsMongo(); + result.setEnablePlainFapi(profile.isEnablePlainFapi()); + + return result; + } + private static SCIMSettings convert(SCIMSettingsMongo scimMongo) { if (scimMongo == null) { return null; diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/OIDCSettingsMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/OIDCSettingsMongo.java index 19876bf930..cbf72de324 100644 --- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/OIDCSettingsMongo.java +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/OIDCSettingsMongo.java @@ -24,6 +24,7 @@ public class OIDCSettingsMongo { private ClientRegistrationSettingsMongo clientRegistrationSettings; + private SecurityProfileSettingsMongo securityProfileSettings; private boolean redirectUriStrictMatching; private List postLogoutRedirectUris; @@ -50,4 +51,12 @@ public List getPostLogoutRedirectUris() { public void setPostLogoutRedirectUris(List postLogoutRedirectUris) { this.postLogoutRedirectUris = postLogoutRedirectUris; } + + public SecurityProfileSettingsMongo getSecurityProfileSettings() { + return securityProfileSettings; + } + + public void setSecurityProfileSettings(SecurityProfileSettingsMongo securityProfileSettings) { + this.securityProfileSettings = securityProfileSettings; + } } diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/SecurityProfileSettingsMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/SecurityProfileSettingsMongo.java new file mode 100644 index 0000000000..6210370b65 --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/oidc/SecurityProfileSettingsMongo.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.mongodb.management.internal.model.oidc; + +import java.util.List; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class SecurityProfileSettingsMongo { + + private boolean enablePlainFapi; + + public boolean isEnablePlainFapi() { + return enablePlainFapi; + } + + public void setEnablePlainFapi(boolean enablePlainFapi) { + this.enablePlainFapi = enablePlainFapi; + } +} diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchOIDCSettings.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchOIDCSettings.java index c10a41bead..33b25c989e 100644 --- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchOIDCSettings.java +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchOIDCSettings.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.gravitee.am.model.oidc.ClientRegistrationSettings; import io.gravitee.am.model.oidc.OIDCSettings; +import io.gravitee.am.model.oidc.SecurityProfileSettings; import io.gravitee.am.model.permissions.Permission; import io.gravitee.am.service.utils.SetterUtils; @@ -37,6 +38,9 @@ public PatchOIDCSettings() {} @JsonProperty("clientRegistrationSettings") private Optional clientRegistrationSettings; + @JsonProperty("securityProfileSettings") + private Optional securityProfileSettings; + private Optional redirectUriStrictMatching; private Optional> postLogoutRedirectUris; @@ -49,6 +53,14 @@ public void setClientRegistrationSettings(Optional getSecurityProfileSettings() { + return securityProfileSettings; + } + + public void setSecurityProfileSettings(Optional securityProfileSettings) { + this.securityProfileSettings = securityProfileSettings; + } + public Optional getRedirectUriStrictMatching() { return redirectUriStrictMatching; } @@ -85,6 +97,16 @@ public OIDCSettings patch(OIDCSettings toPatch) { } } + if (getSecurityProfileSettings() != null) { + if (getSecurityProfileSettings().isPresent()) { + final PatchSecurityProfileSettings patcher = getSecurityProfileSettings().get(); + final SecurityProfileSettings source = toPatch.getSecurityProfileSettings(); + toPatch.setSecurityProfileSettings(patcher.patch(source)); + } else { + toPatch.setSecurityProfileSettings(SecurityProfileSettings.defaultSettings()); + } + } + return toPatch; } diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchSecurityProfileSettings.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchSecurityProfileSettings.java new file mode 100644 index 0000000000..e695d2d8f3 --- /dev/null +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/openid/PatchSecurityProfileSettings.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.service.model.openid; + +import io.gravitee.am.model.oidc.ClientRegistrationSettings; +import io.gravitee.am.model.oidc.SecurityProfileSettings; +import io.gravitee.am.service.utils.SetterUtils; + +import java.util.List; +import java.util.Optional; + +/** + * @@author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class PatchSecurityProfileSettings { + + public PatchSecurityProfileSettings() {} + + /** + * Apply the standard Financial-grade API security profile (version 1.0). + */ + private Optional enablePlainFapi; + + public Optional getEnablePlainFapi() { + return enablePlainFapi; + } + + public void setEnablePlainFapi(Optional enablePlainFapi) { + this.enablePlainFapi = enablePlainFapi; + } + + public SecurityProfileSettings patch(SecurityProfileSettings toPatch) { + SecurityProfileSettings result=toPatch!=null? toPatch: SecurityProfileSettings.defaultSettings(); + + SetterUtils.safeSet(result::setEnablePlainFapi, this.getEnablePlainFapi(), boolean.class); + + return result; + } +} diff --git a/gravitee-am-ui/src/app/app-routing.module.ts b/gravitee-am-ui/src/app/app-routing.module.ts index 6577787f7a..96e40308b5 100644 --- a/gravitee-am-ui/src/app/app-routing.module.ts +++ b/gravitee-am-ui/src/app/app-routing.module.ts @@ -210,6 +210,7 @@ import { BotDetectionPluginsResolver } from './resolvers/bot-detection-plugins.r import { BotDetectionComponent } from './domain/settings/botdetections/bot-detection/bot-detection.component'; import { BotDetectionResolver } from './resolvers/bot-detection.resolver'; import { ScopesAllResolver } from "./resolvers/scopes-all.resolver"; +import { OIDCProfileComponent } from './domain/settings/openid/oidc-profile/oidc-profile.component'; let applyOnLabel = (label) => label.toLowerCase().replace(/_/g, ' '); @@ -2224,6 +2225,20 @@ export const routes: Routes = [ } ] }, + { + path: 'oidc-profile', + component: OIDCProfileComponent, + canActivate: [AuthGuard], + data: { + menu: { + label: 'Security Profile', + section: 'Openid' + }, + perms: { + only: ['domain_openid_read'] + } + } + }, { path: 'uma', component: UmaComponent, diff --git a/gravitee-am-ui/src/app/app.module.ts b/gravitee-am-ui/src/app/app.module.ts index a5103f022c..005db415ba 100644 --- a/gravitee-am-ui/src/app/app.module.ts +++ b/gravitee-am-ui/src/app/app.module.ts @@ -323,6 +323,7 @@ import {IdenticonHashDirective} from './directives/identicon-hash.directive'; import {UserAvatarComponent} from './components/user-avatar/user-avatar.component'; import {NotFoundComponent} from './not-found/not-found.component'; import {UmaComponent} from './domain/settings/uma/uma.component'; +import {OIDCProfileComponent} from './domain/settings/openid/oidc-profile/oidc-profile.component'; import {ApplicationResourcesComponent} from './domain/applications/application/advanced/resources/resources.component'; import {ApplicationResourcesResolver} from './resolvers/application-resources.resolver'; import {ApplicationResourceComponent} from './domain/applications/application/advanced/resources/resource/resource.component'; @@ -558,6 +559,7 @@ import { GvFormControlDirective } from "./directives/gv-form-control.directive"; UserAvatarComponent, NotFoundComponent, UmaComponent, + OIDCProfileComponent, LoginSettingsComponent, UsersSearchInfoDialog, NewsletterComponent, diff --git a/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.html b/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.html new file mode 100644 index 0000000000..97bf0baa9d --- /dev/null +++ b/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.html @@ -0,0 +1,53 @@ + +
+

OpenID Security profile

+
+
+
+
+ + FAPI 1.0 Support + + Enable Financial-grade API Security Profile 1.0 +
+
+ +
+
+
+
+

Financial-grade API Security Profile 1.0

+
+

The Financial-grade API is a highly secured OAuth profile that aims to provide specific implementation guidelines + for security and interoperability (See FAPI 1.0 - part 1 and FAPI 1.0 - part 2).

+

Enable this option will perform specific controls and will required some behaviors that are optional in the OIDC core specification:

+
    +
  • Authorization endpoint parameters shall be provided using JWT Secured Authorization Request (by value or by reference)
  • +
  • Request Object shall contains the exp and nbf claims with a maximum duration of 60 seconds
  • +
  • PKCE code challenge method is restricted to S256
  • +
  • PKCE is required when authorization endpoint parameters are provided using JWT Secured Authorization Request by reference
  • +
  • reponse_mode jwt is required when the response_type value is code
  • +
  • Client shall use mTLS connection to bind the access_token to the client certificate
  • +
+
+
+
+
diff --git a/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.scss b/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.spec.ts b/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.spec.ts new file mode 100644 index 0000000000..c7ef3e4cb7 --- /dev/null +++ b/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.spec.ts @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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. + */ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { OIDCProfileComponent } from './oidc-profile.component'; + +describe('OIDCProfileComponent', () => { + let component: OIDCProfileComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ OIDCProfileComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OIDCProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.ts b/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.ts new file mode 100644 index 0000000000..9bc3b15dd3 --- /dev/null +++ b/gravitee-am-ui/src/app/domain/settings/openid/oidc-profile/oidc-profile.component.ts @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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. + */ +import {Component, OnInit} from '@angular/core'; +import {ActivatedRoute, Router} from "@angular/router"; +import { AuthService } from 'app/services/auth.service'; +import { DomainService } from 'app/services/domain.service'; +import { SnackbarService } from 'app/services/snackbar.service'; + +@Component({ + selector: 'app-oidc-profile', + templateUrl: './oidc-profile.component.html', + styleUrls: ['./oidc-profile.component.scss'] +}) +export class OIDCProfileComponent implements OnInit { + domainId: string; + domain: any = {}; + formChanged = false; + editMode: boolean; + + constructor(private domainService: DomainService, + private snackbarService: SnackbarService, + private authService: AuthService, + private route: ActivatedRoute, + private router: Router) { + } + + ngOnInit() { + this.domain = this.route.snapshot.data['domain']; + this.domainId = this.domain.id; + this.editMode = this.authService.hasPermissions(['domain_openid_update']); + } + + save() { + this.domainService.patchOpenidDCRSettings(this.domainId, this.domain).subscribe(data => { + this.domain = data; + this.formChanged = false; + this.snackbarService.open('OpenID Profile configuration updated'); + }); + } + + enableFAPI(event) { + if (!this.domain.oidc.securityProfileSettings) { + this.domain.oidc.securityProfileSettings = {}; + } + this.domain.oidc.securityProfileSettings.enablePlainFapi = event.checked; + this.formChanged = true; + } + + isFAPIEnabled() { + return this.domain.oidc.securityProfileSettings && this.domain.oidc.securityProfileSettings.enablePlainFapi; + } +} From 4bbe6e04196daf93b60121824a36274c4414e66d Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 3 Aug 2021 14:39:37 +0200 Subject: [PATCH 05/24] feat: control content of request parameter with oauth request params fixes gravitee-io/issues#4052 --- .../handler/oauth2/OAuth2Provider.java | 2 +- ...ationRequestParseRequestObjectHandler.java | 86 ++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index 5e2ede161f..3ec9ec1e83 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -200,7 +200,7 @@ private void initRouter() { .handler(new AuthorizationRequestParseProviderConfigurationHandler(openIDDiscoveryService)) .handler(new AuthorizationRequestParseRequiredParametersHandler()) .handler(new AuthorizationRequestParseClientHandler(clientSyncService)) - .handler(new AuthorizationRequestParseRequestObjectHandler(requestObjectService)) + .handler(new AuthorizationRequestParseRequestObjectHandler(requestObjectService, domain)) .handler(new AuthorizationRequestParseIdTokenHintHandler(idTokenService)) .handler(new AuthorizationRequestParseParametersHandler(domain)) .handler(authenticationFlowContextHandler) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index 2886447190..25d6c1a8a6 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -16,15 +16,21 @@ package io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization; import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; import io.gravitee.am.common.exception.oauth2.InvalidRequestException; import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; import io.gravitee.am.common.exception.oauth2.InvalidRequestUriException; +import io.gravitee.am.common.jwt.Claims; import io.gravitee.am.common.oidc.Parameters; import io.gravitee.am.common.oidc.Scope; +import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectService; +import io.gravitee.am.model.Domain; import io.reactivex.Maybe; +import io.reactivex.Single; import io.vertx.core.Handler; import io.vertx.reactivex.ext.web.RoutingContext; +import org.springframework.util.StringUtils; import java.net.URI; import java.text.ParseException; @@ -33,6 +39,7 @@ import java.util.stream.Stream; import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY; +import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.PROVIDER_METADATA_CONTEXT_KEY; /** * The request Authorization Request parameter enables OpenID Connect requests to be passed in a single, @@ -64,8 +71,11 @@ public class AuthorizationRequestParseRequestObjectHandler implements Handler handleRequestObjectValue(RoutingContext context) { return requestObjectService .readRequestObject(request, context.get(CLIENT_CONTEXT_KEY)) + .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) .toMaybe(); } else { return Maybe.empty(); } } + private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) { + try { + final JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet(); + + // according to https://openid.net/specs/openid-connect-core-1_0.html#RequestObject + // OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax + // So we test the consistency of these parameters + List scope = context.queryParam(io.gravitee.am.common.oauth2.Parameters.SCOPE); + final String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); + if (scope != null && !scope.isEmpty() && + (scopeClaim == null || !scopeClaim.equals(scope.get(0)))) { + return Single.error(new InvalidRequestObjectException("Request object must contains valid scope claim")); + } + + List redirectUri = context.queryParam(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + final String redirectUriClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + if ( (redirectUri == null) || + (redirectUri != null && (redirectUri.size() != 1 || redirectUriClaim == null || !redirectUriClaim.equals(redirectUri.get(0))))) { + return Single.error(new InvalidRequestException("Missing or invalid redirect_uri")); + } + + // here after these constraints are specific to the FAPI specification + if (isFapiEnabled()) { + + List state = context.queryParam(io.gravitee.am.common.oauth2.Parameters.STATE); + final String stateClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE); + if (state != null && !state.isEmpty() && + (stateClaim == null || !stateClaim.equals(state.get(0)))) { + return Single.error(new InvalidRequestObjectException("Request object must contains valid state claim")); + } + + final OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); + if (jwtClaimsSet.getAudience() == null || (openIDProviderMetadata != null && + !jwtClaimsSet.getAudience().contains(openIDProviderMetadata.getIssuer()))) { + // the aud claim in the request object shall be, or shall be an array containing, the OP’s Issuer Identifier URL; + return Single.error(new InvalidRequestObjectException("Invalid audience claim")); + } + + if (scopeClaim != null) { + if (scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(Parameters.NONCE))) { + // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#client-requesting-openid-scope + // If the client requests the openid scope, the authorization server shall require the nonce parameter defined + return Single.error(new InvalidRequestObjectException("Scope openid expect the nonce parameter defined")); + } + + if (!scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE))) { + // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#clients-not-requesting-openid-scope + // If the client does not request the openid scope, the authorization server shall require the state parameter defined + return Single.error(new InvalidRequestObjectException("Absence of scope openid expect the state parameter defined")); + } + } + } + } catch (ParseException e) { + return Single.error(new InvalidRequestObjectException()); + } + return Single.just(jwt); + } + + private boolean isFapiEnabled() { + return this.domain.getOidc() != null && + this.domain.getOidc().getSecurityProfileSettings() != null && + this.domain.getOidc().getSecurityProfileSettings().isEnablePlainFapi(); + } + private Maybe handleRequestObjectURI(RoutingContext context) { final String requestUri = context.request().getParam(Parameters.REQUEST_URI); @@ -169,6 +252,7 @@ private Maybe handleRequestObjectURI(RoutingContext context) { return requestObjectService .readRequestObjectFromURI(requestUri, context.get(CLIENT_CONTEXT_KEY)) + .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) .toMaybe(); } else { return Maybe.empty(); From 38c9d035d67788682eb552df54c63daa7d6f7db6 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 3 Aug 2021 14:40:40 +0200 Subject: [PATCH 06/24] feat: coontrole exp claims into the request jwt param fixes gravitee-io/issues#5940 --- .../AuthorizationRequestParseRequestObjectHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index 25d6c1a8a6..795e5361c7 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -182,7 +182,9 @@ private Maybe handleRequestObjectValue(RoutingContext context) { private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) { try { final JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet(); - + if (jwtClaimsSet.getExpirationTime() == null || jwtClaimsSet.getExpirationTime().before(new Date())) { + return Single.error(new InvalidRequestObjectException("Request object must contains valid exp claim")); + } // according to https://openid.net/specs/openid-connect-core-1_0.html#RequestObject // OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax // So we test the consistency of these parameters From ea9e330471b72de23dba0cace3436b6f0acfed42 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 3 Aug 2021 16:27:11 +0200 Subject: [PATCH 07/24] feat: response_type=code in the authorization request is not permitted in FAPI-RW if a JARM response has not been requested fixes gravitee-io/issues#5955 --- ...thorizationRequestParseParametersHandler.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java index b481f1fd75..9dbdd280fd 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java @@ -18,6 +18,7 @@ import io.gravitee.am.common.exception.oauth2.InvalidRequestException; import io.gravitee.am.common.oauth2.CodeChallengeMethod; import io.gravitee.am.common.oauth2.GrantType; +import io.gravitee.am.common.oauth2.ResponseType; import io.gravitee.am.common.oidc.Parameters; import io.gravitee.am.common.oidc.idtoken.Claims; import io.gravitee.am.common.web.UriBuilder; @@ -274,6 +275,21 @@ private void parseResponseTypeParameter(RoutingContext context, Client client) { if(!client.getResponseTypes().contains(responseType)) { throw new UnauthorizedClientException("Client should have all requested response_type"); } + + if (this.domain.getOidc() != null && + this.domain.getOidc().getSecurityProfileSettings() != null && + this.domain.getOidc().getSecurityProfileSettings().isEnablePlainFapi()) { + // For FAPI : https://openid.net/specs/openid-financial-api-part-2-1_0-final.html#authorization-server + // The authorization server + // shall require + // the response_type value code id_token, or + // the response_type value code in conjunction with the response_mode value jwt; + String responseMode = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.RESPONSE_MODE); + if (!((responseType.equals(io.gravitee.am.common.oidc.ResponseType.CODE_ID_TOKEN)) || + (responseType.equals(ResponseType.CODE) && (responseMode != null && responseMode.equalsIgnoreCase("jwt"))))) { + throw new InvalidRequestException("Invalid response_type"); + } + } } private void parseRedirectUriParameter(RoutingContext context, Client client) { From f3a4135664a63095a84c9cb5d8117f1e8b557504 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 3 Aug 2021 16:53:46 +0200 Subject: [PATCH 08/24] feat: allow to restrict ciphers for tls connections fixes gravitee-io/issues#5929 --- .../vertx/VertxHttpServerConfiguration.java | 16 ++++++++++++++++ .../am/gateway/vertx/VertxHttpServerFactory.java | 8 ++++++++ .../src/main/resources/config/gravitee.yml | 1 + 3 files changed, 25 insertions(+) diff --git a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerConfiguration.java b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerConfiguration.java index 9a1df90324..a07daf9b71 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerConfiguration.java +++ b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerConfiguration.java @@ -15,12 +15,16 @@ */ package io.gravitee.am.gateway.vertx; +import io.gravitee.common.util.EnvironmentUtils; import io.vertx.core.http.HttpServerOptions; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.ConfigurableEnvironment; +import java.util.List; +import java.util.Map; + /** * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Team @@ -86,6 +90,8 @@ public class VertxHttpServerConfiguration implements InitializingBean { private ClientAuthMode clientAuth; + private List authorizedTlsCipherSuites; + public int getPort() { return port; } @@ -238,6 +244,14 @@ public void setMaxFormAttributeSize(int maxFormAttributeSize) { this.maxFormAttributeSize = maxFormAttributeSize; } + public List getAuthorizedTlsCipherSuites() { + return authorizedTlsCipherSuites; + } + + public void setAuthorizedTlsCipherSuites(List authorizedTlsCipherSuites) { + this.authorizedTlsCipherSuites = authorizedTlsCipherSuites; + } + @Override public void afterPropertiesSet() throws Exception { String sClientAuthMode = environment.getProperty("http.ssl.clientAuth", ClientAuthMode.NONE.name()); @@ -249,6 +263,8 @@ public void afterPropertiesSet() throws Exception { } else { clientAuth = ClientAuthMode.valueOf(sClientAuthMode.toUpperCase()); } + + this.authorizedTlsCipherSuites = environment.getProperty("http.ssl.ciphers", List.class); } public enum ClientAuthMode { diff --git a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerFactory.java b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerFactory.java index 40c2ea4f46..82936d1ea0 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerFactory.java +++ b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-container/src/main/java/io/gravitee/am/gateway/vertx/VertxHttpServerFactory.java @@ -62,6 +62,14 @@ public HttpServer getObject() throws Exception { options.setEnabledSecureTransportProtocols(new HashSet<>(Arrays.asList(httpServerConfiguration.getTlsProtocols().split("\\s*,\\s*")))); } + // restrict the authorized ciphers + if (httpServerConfiguration.getAuthorizedTlsCipherSuites() != null) { + httpServerConfiguration.getAuthorizedTlsCipherSuites() + .stream() + .map(cipher -> cipher.trim()) + .forEach(options::addEnabledCipherSuite); + } + if (httpServerConfiguration.getClientAuth() == VertxHttpServerConfiguration.ClientAuthMode.NONE) { options.setClientAuth(ClientAuth.NONE); } else if (httpServerConfiguration.getClientAuth() == VertxHttpServerConfiguration.ClientAuthMode.REQUEST) { diff --git a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml index 14f98550ce..1360758f2a 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml +++ b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml @@ -43,6 +43,7 @@ # alpn: false # ssl: # clientAuth: request # Supports none, request, required +# ciphers: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 , TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 , TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 # tlsProtocols: TLSv1.2, TLSv1.3 # keystore: # type: jks # Supports jks, pem, pkcs12 From ae1338cb0b134b1997ec122177303d9383026aff Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 4 Aug 2021 18:45:05 +0200 Subject: [PATCH 09/24] fix: always provide the auth_time claim fixes gravitee-io/issues#5956 --- .../service/idtoken/impl/IDTokenServiceImpl.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/idtoken/impl/IDTokenServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/idtoken/impl/IDTokenServiceImpl.java index 7efa326a71..8a6c472730 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/idtoken/impl/IDTokenServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/idtoken/impl/IDTokenServiceImpl.java @@ -164,13 +164,18 @@ private IDToken createIDTokenJWT(OAuth2Request oAuth2Request, Client client, Use idToken.setAud(oAuth2Request.getClientId()); idToken.setIat(Instant.now().getEpochSecond()); idToken.setExp(Instant.ofEpochSecond(idToken.getIat()).plusSeconds(client.getIdTokenValiditySeconds()).getEpochSecond()); + // set auth_time (Time when the End-User authentication occurred.) + // + // according to OIDC specification (https://openid.net/specs/openid-connect-core-1_0.html#IDToken), + // this claim is optional but REQUIRED if the max_age parameter is specified + // or it the auth_time is part of the claims request. + // we decided to always provide this claim during the Financial-grand API conformance implementation + // since this claim was return by default in some cases even if conditions that require it were missing if (!oAuth2Request.isClientOnly() && user != null && user.getLoggedAt() != null) { - String maxAge = oAuth2Request.parameters().getFirst(Parameters.MAX_AGE); - if (maxAge != null) { - idToken.setAuthTime(user.getLoggedAt().getTime() / 1000l); - } + idToken.setAuthTime(user.getLoggedAt().getTime() / 1000l); } + // set nonce String nonce = oAuth2Request.parameters() != null ? oAuth2Request.parameters().getFirst(Parameters.NONCE) : null; if (nonce != null && !nonce.isEmpty()) { From 3933689f2a7c5c1d79daf17fa62e19a50215d711 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 4 Aug 2021 20:45:30 +0200 Subject: [PATCH 10/24] chore: refactor --- ...rizationRequestParseParametersHandler.java | 7 +- ...ationRequestParseRequestObjectHandler.java | 81 ++++++++++--------- .../granter/refresh/RefreshTokenGranter.java | 3 +- .../java/io/gravitee/am/model/Domain.java | 6 ++ 4 files changed, 55 insertions(+), 42 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java index 9dbdd280fd..91e31e4014 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java @@ -276,9 +276,7 @@ private void parseResponseTypeParameter(RoutingContext context, Client client) { throw new UnauthorizedClientException("Client should have all requested response_type"); } - if (this.domain.getOidc() != null && - this.domain.getOidc().getSecurityProfileSettings() != null && - this.domain.getOidc().getSecurityProfileSettings().isEnablePlainFapi()) { + if (this.domain.usePlainFapiProfile()) { // For FAPI : https://openid.net/specs/openid-financial-api-part-2-1_0-final.html#authorization-server // The authorization server // shall require @@ -350,7 +348,8 @@ private void checkMatchingRedirectUri(String requestedRedirect, List reg private boolean redirectMatches(String requestedRedirect, String registeredClientUri) { // if redirect_uri strict matching mode is enabled, do string matching - if (this.domain.isRedirectUriStrictMatching()) { + // FAPI also requires strict matching + if (this.domain.isRedirectUriStrictMatching() || this.domain.usePlainFapiProfile()) { return requestedRedirect.equals(registeredClientUri); } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index 795e5361c7..0d4f76611c 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -20,12 +20,14 @@ import io.gravitee.am.common.exception.oauth2.InvalidRequestException; import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; import io.gravitee.am.common.exception.oauth2.InvalidRequestUriException; +import io.gravitee.am.common.exception.oauth2.OAuth2Exception; import io.gravitee.am.common.jwt.Claims; import io.gravitee.am.common.oidc.Parameters; import io.gravitee.am.common.oidc.Scope; import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectService; import io.gravitee.am.model.Domain; +import io.gravitee.am.service.exception.InvalidRedirectUriException; import io.reactivex.Maybe; import io.reactivex.Single; import io.vertx.core.Handler; @@ -94,7 +96,7 @@ public void handle(RoutingContext context) { if ((context.request().getParam(Parameters.REQUEST) == null || context.request().getParam(Parameters.REQUEST).isEmpty()) && ((context.request().getParam(Parameters.REQUEST_URI) == null || context.request().getParam(Parameters.REQUEST_URI).isEmpty()))) { - if (isFapiEnabled()) { + if (this.domain.usePlainFapiProfile()) { // according to https://openid.net/specs/openid-financial-api-part-2-1_0.html#authorization-server // Authorization Server shall require a JWS signed JWT request object passed by value with the request parameter or by reference with the request_uri parameter; context.fail(new InvalidRequestException()); @@ -183,8 +185,9 @@ private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) try { final JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet(); if (jwtClaimsSet.getExpirationTime() == null || jwtClaimsSet.getExpirationTime().before(new Date())) { - return Single.error(new InvalidRequestObjectException("Request object must contains valid exp claim")); + throw new InvalidRequestObjectException("Request object must contains valid exp claim"); } + // according to https://openid.net/specs/openid-connect-core-1_0.html#RequestObject // OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax // So we test the consistency of these parameters @@ -192,57 +195,61 @@ private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) final String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); if (scope != null && !scope.isEmpty() && (scopeClaim == null || !scopeClaim.equals(scope.get(0)))) { - return Single.error(new InvalidRequestObjectException("Request object must contains valid scope claim")); + throw new InvalidRequestObjectException("Request object must contains valid scope claim"); } List redirectUri = context.queryParam(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); final String redirectUriClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); if ( (redirectUri == null) || (redirectUri != null && (redirectUri.size() != 1 || redirectUriClaim == null || !redirectUriClaim.equals(redirectUri.get(0))))) { - return Single.error(new InvalidRequestException("Missing or invalid redirect_uri")); + // remove redirect_uri provided as parameter and continue to let AuthorizationRequestParseParametersHandler + // throws the right error according to the client configuration + context.request().params().remove(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + throw new InvalidRequestException("Missing or invalid redirect_uri"); } // here after these constraints are specific to the FAPI specification - if (isFapiEnabled()) { - - List state = context.queryParam(io.gravitee.am.common.oauth2.Parameters.STATE); - final String stateClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE); - if (state != null && !state.isEmpty() && - (stateClaim == null || !stateClaim.equals(state.get(0)))) { - return Single.error(new InvalidRequestObjectException("Request object must contains valid state claim")); - } + validateRequestObjectClaimsAgainstFapi(context, jwtClaimsSet); - final OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); - if (jwtClaimsSet.getAudience() == null || (openIDProviderMetadata != null && - !jwtClaimsSet.getAudience().contains(openIDProviderMetadata.getIssuer()))) { - // the aud claim in the request object shall be, or shall be an array containing, the OP’s Issuer Identifier URL; - return Single.error(new InvalidRequestObjectException("Invalid audience claim")); - } - - if (scopeClaim != null) { - if (scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(Parameters.NONCE))) { - // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#client-requesting-openid-scope - // If the client requests the openid scope, the authorization server shall require the nonce parameter defined - return Single.error(new InvalidRequestObjectException("Scope openid expect the nonce parameter defined")); - } - - if (!scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE))) { - // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#clients-not-requesting-openid-scope - // If the client does not request the openid scope, the authorization server shall require the state parameter defined - return Single.error(new InvalidRequestObjectException("Absence of scope openid expect the state parameter defined")); - } - } - } + } catch (OAuth2Exception e) { + // in case of OAuth2 Exception related to the request object validation, + // we override parameters to use then in redirect (like the state one) + overrideRequestParameters(context, jwt); + return Single.error(e); } catch (ParseException e) { return Single.error(new InvalidRequestObjectException()); } return Single.just(jwt); } - private boolean isFapiEnabled() { - return this.domain.getOidc() != null && - this.domain.getOidc().getSecurityProfileSettings() != null && - this.domain.getOidc().getSecurityProfileSettings().isEnablePlainFapi(); + private void validateRequestObjectClaimsAgainstFapi(RoutingContext context, JWTClaimsSet jwtClaimsSet) throws ParseException { + if (this.domain.usePlainFapiProfile()) { + + List state = context.queryParam(io.gravitee.am.common.oauth2.Parameters.STATE); + final String stateClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE); + if (state != null && !state.isEmpty() && + (stateClaim == null || !stateClaim.equals(state.get(0)))) { + throw new InvalidRequestObjectException("Request object must contains valid state claim"); + } + + final OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); + if (jwtClaimsSet.getAudience() == null || (openIDProviderMetadata != null && + !jwtClaimsSet.getAudience().contains(openIDProviderMetadata.getIssuer()))) { + // the aud claim in the request object shall be, or shall be an array containing, the OP’s Issuer Identifier URL; + throw new InvalidRequestObjectException("Invalid audience claim"); + } + + String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); + if (scopeClaim != null && scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(Parameters.NONCE))) { + // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#client-requesting-openid-scope + // If the client requests the openid scope, the authorization server shall require the nonce parameter defined + throw new InvalidRequestObjectException("Scope openid expect the nonce parameter defined"); + } else if ((scopeClaim == null || !scopeClaim.contains("openid")) && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE))) { + // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#clients-not-requesting-openid-scope + // If the client does not request the openid scope, the authorization server shall require the state parameter defined + throw new InvalidRequestObjectException("Absence of scope openid expect the state parameter defined"); + } + } } private Maybe handleRequestObjectURI(RoutingContext context) { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/refresh/RefreshTokenGranter.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/refresh/RefreshTokenGranter.java index 9274e58e7a..b1e0731e8a 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/refresh/RefreshTokenGranter.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/refresh/RefreshTokenGranter.java @@ -17,6 +17,7 @@ import io.gravitee.am.common.exception.oauth2.InvalidRequestException; import io.gravitee.am.common.oauth2.GrantType; +import io.gravitee.am.common.oauth2.Parameters; import io.gravitee.am.gateway.handler.common.auth.user.UserAuthenticationManager; import io.gravitee.am.gateway.handler.oauth2.exception.InvalidGrantException; import io.gravitee.am.gateway.handler.oauth2.service.granter.AbstractTokenGranter; @@ -57,7 +58,7 @@ public RefreshTokenGranter(TokenRequestResolver tokenRequestResolver, TokenServi @Override protected Single parseRequest(TokenRequest tokenRequest, Client client) { - String refreshToken = tokenRequest.parameters().getFirst("refresh_token"); + String refreshToken = tokenRequest.parameters().getFirst(Parameters.REFRESH_TOKEN); if (refreshToken == null || refreshToken.isEmpty()) { return Single.error(new InvalidRequestException("A refresh token must be supplied.")); diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java index 2dee7877eb..4cb2949ade 100644 --- a/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java +++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/Domain.java @@ -423,6 +423,12 @@ public boolean isRedirectUriStrictMatching() { return this.getOidc()!=null && this.getOidc().isRedirectUriStrictMatching(); } + public boolean usePlainFapiProfile() { + return this.getOidc() != null && + this.getOidc().getSecurityProfileSettings() != null && + this.getOidc().getSecurityProfileSettings().isEnablePlainFapi(); + } + @Override public boolean equals(Object o) { if (this == o) return true; From db979d494a6b90a55a691fbe0d9d07120876bfdc Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 5 Aug 2021 10:48:12 +0200 Subject: [PATCH 11/24] feat: nfr claim required for requst object in FAPI specificiation fixes gravitee-io/issues#5965 --- .../AuthorizationRequestParseRequestObjectHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index 0d4f76611c..157bd0a8bf 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -70,6 +70,7 @@ public class AuthorizationRequestParseRequestObjectHandler implements Handler validateRequestObjectClaims(RoutingContext context, JWT jwt) private void validateRequestObjectClaimsAgainstFapi(RoutingContext context, JWTClaimsSet jwtClaimsSet) throws ParseException { if (this.domain.usePlainFapiProfile()) { + final Date nbf = jwtClaimsSet.getNotBeforeTime(); + if (nbf == null || (nbf.getTime() + ONE_HOUR_IN_MILLIS) < jwtClaimsSet.getExpirationTime().getTime()) { + throw new InvalidRequestObjectException("Request object older than 60 minutes"); + } + List state = context.queryParam(io.gravitee.am.common.oauth2.Parameters.STATE); final String stateClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE); if (state != null && !state.isEmpty() && From 517dd893842254df5c209dba7d782e691b1e7e1e Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 5 Aug 2021 15:14:57 +0200 Subject: [PATCH 12/24] fix: perform some request object validation only if the FAPI mode is enabled fixes graviee-io/issues#5970 --- ...ationRequestParseRequestObjectHandler.java | 123 +++++++++--------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index 157bd0a8bf..fefc07a229 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -27,7 +27,6 @@ import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectService; import io.gravitee.am.model.Domain; -import io.gravitee.am.service.exception.InvalidRedirectUriException; import io.reactivex.Maybe; import io.reactivex.Single; import io.vertx.core.Handler; @@ -183,79 +182,77 @@ private Maybe handleRequestObjectValue(RoutingContext context) { } private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) { - try { - final JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet(); - if (jwtClaimsSet.getExpirationTime() == null || jwtClaimsSet.getExpirationTime().before(new Date())) { - throw new InvalidRequestObjectException("Request object must contains valid exp claim"); - } + if (this.domain.usePlainFapiProfile()) { + try { + final JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet(); - // according to https://openid.net/specs/openid-connect-core-1_0.html#RequestObject - // OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax - // So we test the consistency of these parameters - List scope = context.queryParam(io.gravitee.am.common.oauth2.Parameters.SCOPE); - final String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); - if (scope != null && !scope.isEmpty() && - (scopeClaim == null || !scopeClaim.equals(scope.get(0)))) { - throw new InvalidRequestObjectException("Request object must contains valid scope claim"); - } + // according to https://openid.net/specs/openid-connect-core-1_0.html#RequestObject + // OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax + // but FAPI requires that these params are equals, so we test the consistency of these parameters + // in addition FAPI requires some claims that are optional in the OIDC core spec (like exp, nbf...) - List redirectUri = context.queryParam(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); - final String redirectUriClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); - if ( (redirectUri == null) || - (redirectUri != null && (redirectUri.size() != 1 || redirectUriClaim == null || !redirectUriClaim.equals(redirectUri.get(0))))) { - // remove redirect_uri provided as parameter and continue to let AuthorizationRequestParseParametersHandler - // throws the right error according to the client configuration - context.request().params().remove(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); - throw new InvalidRequestException("Missing or invalid redirect_uri"); - } + if (jwtClaimsSet.getExpirationTime() == null || jwtClaimsSet.getExpirationTime().before(new Date())) { + throw new InvalidRequestObjectException("Request object must contains valid exp claim"); + } - // here after these constraints are specific to the FAPI specification - validateRequestObjectClaimsAgainstFapi(context, jwtClaimsSet); + List redirectUri = context.queryParam(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + final String redirectUriClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + if ( (redirectUri == null) || + (redirectUri != null && (redirectUri.size() != 1 || redirectUriClaim == null || !redirectUriClaim.equals(redirectUri.get(0))))) { + // remove redirect_uri provided as parameter and continue to let AuthorizationRequestParseParametersHandler + // throws the right error according to the client configuration + context.request().params().remove(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + throw new InvalidRequestException("Missing or invalid redirect_uri"); + } - } catch (OAuth2Exception e) { - // in case of OAuth2 Exception related to the request object validation, - // we override parameters to use then in redirect (like the state one) - overrideRequestParameters(context, jwt); - return Single.error(e); - } catch (ParseException e) { - return Single.error(new InvalidRequestObjectException()); - } - return Single.just(jwt); - } + final Date nbf = jwtClaimsSet.getNotBeforeTime(); + if (nbf == null || (nbf.getTime() + ONE_HOUR_IN_MILLIS) < jwtClaimsSet.getExpirationTime().getTime()) { + throw new InvalidRequestObjectException("Request object older than 60 minutes"); + } - private void validateRequestObjectClaimsAgainstFapi(RoutingContext context, JWTClaimsSet jwtClaimsSet) throws ParseException { - if (this.domain.usePlainFapiProfile()) { + List state = context.queryParam(io.gravitee.am.common.oauth2.Parameters.STATE); + final String stateClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE); + if (state != null && !state.isEmpty() && + (stateClaim == null || !stateClaim.equals(state.get(0)))) { + throw new InvalidRequestObjectException("Request object must contains valid state claim"); + } - final Date nbf = jwtClaimsSet.getNotBeforeTime(); - if (nbf == null || (nbf.getTime() + ONE_HOUR_IN_MILLIS) < jwtClaimsSet.getExpirationTime().getTime()) { - throw new InvalidRequestObjectException("Request object older than 60 minutes"); - } + final OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); + if (jwtClaimsSet.getAudience() == null || (openIDProviderMetadata != null && + !jwtClaimsSet.getAudience().contains(openIDProviderMetadata.getIssuer()))) { + // the aud claim in the request object shall be, or shall be an array containing, the OP’s Issuer Identifier URL; + throw new InvalidRequestObjectException("Invalid audience claim"); + } - List state = context.queryParam(io.gravitee.am.common.oauth2.Parameters.STATE); - final String stateClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE); - if (state != null && !state.isEmpty() && - (stateClaim == null || !stateClaim.equals(state.get(0)))) { - throw new InvalidRequestObjectException("Request object must contains valid state claim"); - } + List scope = context.queryParam(io.gravitee.am.common.oauth2.Parameters.SCOPE); + final String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); + if (scope != null && !scope.isEmpty() && + (scopeClaim == null || !scopeClaim.equals(scope.get(0)))) { + throw new InvalidRequestObjectException("Request object must contains valid scope claim"); + } - final OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); - if (jwtClaimsSet.getAudience() == null || (openIDProviderMetadata != null && - !jwtClaimsSet.getAudience().contains(openIDProviderMetadata.getIssuer()))) { - // the aud claim in the request object shall be, or shall be an array containing, the OP’s Issuer Identifier URL; - throw new InvalidRequestObjectException("Invalid audience claim"); - } + // String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); + if (scopeClaim != null && scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(Parameters.NONCE))) { + // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#client-requesting-openid-scope + // If the client requests the openid scope, the authorization server shall require the nonce parameter defined + throw new InvalidRequestObjectException("Scope openid expect the nonce parameter defined"); + } else if ((scopeClaim == null || !scopeClaim.contains("openid")) && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE))) { + // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#clients-not-requesting-openid-scope + // If the client does not request the openid scope, the authorization server shall require the state parameter defined + throw new InvalidRequestObjectException("Absence of scope openid expect the state parameter defined"); + } - String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); - if (scopeClaim != null && scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(Parameters.NONCE))) { - // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#client-requesting-openid-scope - // If the client requests the openid scope, the authorization server shall require the nonce parameter defined - throw new InvalidRequestObjectException("Scope openid expect the nonce parameter defined"); - } else if ((scopeClaim == null || !scopeClaim.contains("openid")) && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE))) { - // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#clients-not-requesting-openid-scope - // If the client does not request the openid scope, the authorization server shall require the state parameter defined - throw new InvalidRequestObjectException("Absence of scope openid expect the state parameter defined"); + } catch (OAuth2Exception e) { + // in case of OAuth2 Exception related to the request object validation, + // we override parameters to use then in redirect (like the state one) + overrideRequestParameters(context, jwt); + return Single.error(e); + } catch (ParseException e) { + return Single.error(new InvalidRequestObjectException()); } } + + return Single.just(jwt); } private Maybe handleRequestObjectURI(RoutingContext context) { From 58f36b8ba8b4a7558ff16e770e8ce21f3af28e35 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 6 Aug 2021 19:16:14 +0200 Subject: [PATCH 13/24] feat: implement OAuth 2.0 Pushed Authorization Requests fixes gravitee-io/issues#5969 --- .../oauth2/InvalidRequestUriException.java | 2 +- .../handler/oauth2/OAuth2Provider.java | 20 +- .../PushedAuthorizationRequestEndpoint.java | 100 ++++++ ...rizationRequestParseParametersHandler.java | 1 + ...ationRequestParseRequestObjectHandler.java | 51 ++- .../PushedAuthorizationRequestResponse.java | 50 +++ .../PushedAuthorizationRequestService.java | 51 +++ ...PushedAuthorizationRequestServiceImpl.java | 214 +++++++++++ .../RequestObjectRegistrationEndpoint.java | 3 + .../discovery/OpenIDProviderMetadata.java | 11 + .../impl/OpenIDDiscoveryServiceImpl.java | 2 + .../oidc/spring/OIDCConfiguration.java | 7 + ...PushedAuthorizationRequestServiceTest.java | 339 ++++++++++++++++++ .../PushedAuthorizationRequestRepository.java | 38 ++ .../model/PushedAuthorizationRequest.java | 106 ++++++ ...cPushedAuthorizationRequestRepository.java | 96 +++++ .../model/JdbcPushedAuthorizationRequest.java | 87 +++++ ...gPushedAuthorizationRequestRepository.java | 28 ++ .../src/main/resources/dozer.xml | 10 + .../changelogs/v3_11_0/schema-par.yml | 30 ++ .../src/main/resources/liquibase/master.yml | 2 + .../JdbcRepositoriesTestInitializer.java | 2 + ...thorizationRequestRepositoryPurgeTest.java | 73 ++++ ...oPushedAuthorizationRequestRepository.java | 123 +++++++ .../oauth2/MongoRequestObjectRepository.java | 1 - .../PushedAuthorizationRequestMongo.java | 108 ++++++ ...hedAuthorizationRequestRepositoryTest.java | 94 +++++ 27 files changed, 1629 insertions(+), 20 deletions(-) create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestResponse.java create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java create mode 100644 gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepository.java create mode 100644 gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/PushedAuthorizationRequest.java create mode 100644 gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/JdbcPushedAuthorizationRequestRepository.java create mode 100644 gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcPushedAuthorizationRequest.java create mode 100644 gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/spring/SpringPushedAuthorizationRequestRepository.java create mode 100644 gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml create mode 100644 gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryPurgeTest.java create mode 100644 gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoPushedAuthorizationRequestRepository.java create mode 100644 gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/internal/model/PushedAuthorizationRequestMongo.java create mode 100644 gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryTest.java diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/exception/oauth2/InvalidRequestUriException.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/exception/oauth2/InvalidRequestUriException.java index fc5c351bab..7946cdbb11 100644 --- a/gravitee-am-common/src/main/java/io/gravitee/am/common/exception/oauth2/InvalidRequestUriException.java +++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/exception/oauth2/InvalidRequestUriException.java @@ -36,7 +36,7 @@ public InvalidRequestUriException(String message) { @Override public String getOAuth2ErrorCode() { - return "invalid_request_uris"; + return "invalid_request_uri"; } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index 3ec9ec1e83..b062a1cd48 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -30,6 +30,7 @@ import io.gravitee.am.gateway.handler.oauth2.resources.endpoint.authorization.consent.UserConsentEndpoint; import io.gravitee.am.gateway.handler.oauth2.resources.endpoint.authorization.consent.UserConsentPostEndpoint; import io.gravitee.am.gateway.handler.oauth2.resources.endpoint.introspection.IntrospectionEndpoint; +import io.gravitee.am.gateway.handler.oauth2.resources.endpoint.par.PushedAuthorizationRequestEndpoint; import io.gravitee.am.gateway.handler.oauth2.resources.endpoint.revocation.RevocationTokenEndpoint; import io.gravitee.am.gateway.handler.oauth2.resources.endpoint.token.TokenEndpoint; import io.gravitee.am.gateway.handler.oauth2.resources.handler.ExceptionHandler; @@ -42,9 +43,9 @@ import io.gravitee.am.gateway.handler.oauth2.service.consent.UserConsentService; import io.gravitee.am.gateway.handler.oauth2.service.granter.TokenGranter; import io.gravitee.am.gateway.handler.oauth2.service.introspection.IntrospectionService; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; import io.gravitee.am.gateway.handler.oauth2.service.revocation.RevocationTokenService; import io.gravitee.am.gateway.handler.oauth2.service.token.TokenManager; -import io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.AuthorizationRequestParseRequestObjectHandler; import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDDiscoveryService; import io.gravitee.am.gateway.handler.oidc.service.flow.Flow; import io.gravitee.am.gateway.handler.oidc.service.idtoken.IDTokenService; @@ -60,7 +61,9 @@ import io.vertx.reactivex.core.Vertx; import io.vertx.reactivex.ext.web.Router; import io.vertx.reactivex.ext.web.RoutingContext; -import io.vertx.reactivex.ext.web.handler.*; +import io.vertx.reactivex.ext.web.handler.CSRFHandler; +import io.vertx.reactivex.ext.web.handler.CorsHandler; +import io.vertx.reactivex.ext.web.handler.StaticHandler; import io.vertx.reactivex.ext.web.templ.thymeleaf.ThymeleafTemplateEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -157,6 +160,9 @@ public class OAuth2Provider extends AbstractService implements @Autowired private Environment environment; + @Autowired + private PushedAuthorizationRequestService parService; + @Override protected void doStart() throws Exception { super.doStart(); @@ -200,7 +206,7 @@ private void initRouter() { .handler(new AuthorizationRequestParseProviderConfigurationHandler(openIDDiscoveryService)) .handler(new AuthorizationRequestParseRequiredParametersHandler()) .handler(new AuthorizationRequestParseClientHandler(clientSyncService)) - .handler(new AuthorizationRequestParseRequestObjectHandler(requestObjectService, domain)) + .handler(new AuthorizationRequestParseRequestObjectHandler(requestObjectService, domain, parService)) .handler(new AuthorizationRequestParseIdTokenHintHandler(idTokenService)) .handler(new AuthorizationRequestParseParametersHandler(domain)) .handler(authenticationFlowContextHandler) @@ -258,6 +264,14 @@ private void initRouter() { oauth2Router.route(HttpMethod.GET, "/error") .handler(new ErrorEndpoint(domain.getId(), thymeleafTemplateEngine, clientSyncService)); + // Pushed Authorization Request + oauth2Router.route(HttpMethod.POST,"/par") + .handler(clientAuthHandler) + .handler(new PushedAuthorizationRequestEndpoint(parService)); + + oauth2Router.route("/par") + .handler(new PushedAuthorizationRequestEndpoint.MethodNotAllowedHandler()); + // error handler errorHandler(oauth2Router); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java new file mode 100644 index 0000000000..e223e99ca3 --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.resources.endpoint.par; + +import io.gravitee.am.common.exception.oauth2.InvalidRequestException; +import io.gravitee.am.common.exception.oauth2.MethodNotAllowedException; +import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; +import io.gravitee.am.identityprovider.common.oauth2.utils.URLEncodedUtils; +import io.gravitee.am.model.oidc.Client; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.gravitee.common.http.HttpStatusCode; +import io.gravitee.common.http.MediaType; +import io.gravitee.common.util.LinkedMultiValueMap; +import io.gravitee.common.util.MultiValueMap; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.Json; +import io.vertx.reactivex.core.http.HttpServerRequest; +import io.vertx.reactivex.ext.web.RoutingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class PushedAuthorizationRequestEndpoint implements Handler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PushedAuthorizationRequestEndpoint.class); + + private final PushedAuthorizationRequestService parService; + + public PushedAuthorizationRequestEndpoint(PushedAuthorizationRequestService parService) { + this.parService = parService; + } + + @Override + public void handle(RoutingContext context) { + // Confidential clients or other clients issued client credentials MUST + // authenticate with the authorization server when making requests to the pushed authorization request endpoint. + Client client = context.get(CLIENT_CONTEXT_KEY); + if (client == null) { + throw new InvalidClientException(); + } + + final String contentType = context.request().getHeader(HttpHeaders.CONTENT_TYPE); + if (contentType == null || !contentType.startsWith(URLEncodedUtils.CONTENT_TYPE)) { + throw new InvalidRequestException("Unsupported Content-Type"); + } + + + PushedAuthorizationRequest request = new PushedAuthorizationRequest(); + request.setParameters(extractRequestParameters(context.request())); + request.setClient(client.getClientId()); + + parService.registerParameters(request, client) + .subscribe( + response -> { + context.response() + .setStatusCode(HttpStatusCode.CREATED_201) + .putHeader(io.gravitee.common.http.HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .putHeader(io.gravitee.common.http.HttpHeaders.CACHE_CONTROL, "no-store") + .putHeader(io.gravitee.common.http.HttpHeaders.PRAGMA, "no-cache") + .end(Json.encodePrettily(response)); + }, + throwable -> { + context.fail(throwable); + } + ); + } + + private MultiValueMap extractRequestParameters(HttpServerRequest request) { + MultiValueMap requestParameters = new LinkedMultiValueMap<>(request.params().size()); + request.params().entries().forEach(entry -> requestParameters.add(entry.getKey(), entry.getValue())); + return requestParameters; + } + + public static class MethodNotAllowedHandler implements Handler { + @Override + public void handle(RoutingContext context) { + context.fail(new MethodNotAllowedException()); + } + } +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java index 91e31e4014..e3e1dc1a4a 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java @@ -268,6 +268,7 @@ private void parseGrantTypeParameter(Client client) { private void parseResponseTypeParameter(RoutingContext context, Client client) { String responseType = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.RESPONSE_TYPE); + // Authorization endpoint implies that the client should have response_type if (client.getResponseTypes() == null) { throw new UnauthorizedClientException("Client should have response_type."); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index fefc07a229..c9670788d9 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -24,6 +24,7 @@ import io.gravitee.am.common.jwt.Claims; import io.gravitee.am.common.oidc.Parameters; import io.gravitee.am.common.oidc.Scope; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectService; import io.gravitee.am.model.Domain; @@ -53,7 +54,7 @@ * @author GraviteeSource Team */ public class AuthorizationRequestParseRequestObjectHandler implements Handler { - + private static final String REQUEST_OBJECT_FROM_URI = "ro-from-uri"; private static final String HTTPS_SCHEME = "https"; // When the request parameter is used, the OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax. @@ -72,12 +73,14 @@ public class AuthorizationRequestParseRequestObjectHandler implements Handler scopes = scope != null && !scope.isEmpty() ? new HashSet<>(Arrays.asList(scope.split("\\s+"))) : null; if (scopes == null || !scopes.contains(Scope.OPENID.getKey())) { @@ -114,8 +118,10 @@ public void handle(RoutingContext context) { Maybe requestObject = null; if (context.request().getParam(Parameters.REQUEST) != null) { + context.put(REQUEST_OBJECT_FROM_URI, false); requestObject = handleRequestObjectValue(context); } else if (context.request().getParam(Parameters.REQUEST_URI) != null) { + context.put(REQUEST_OBJECT_FROM_URI, true); requestObject = handleRequestObjectURI(context); } @@ -123,6 +129,7 @@ public void handle(RoutingContext context) { .subscribe( jwt -> { try { + context.put(REQUEST_OBJECT_KEY, jwt); // Check OAuth2 parameters checkOAuthParameters(context, jwt); overrideRequestParameters(context, jwt); @@ -156,7 +163,10 @@ private void checkRequestObjectParameters(RoutingContext context) { URI uri = URI.create(requestUri); // The scheme used in the request_uri value MUST be https or starts with urn:ros: - if (uri.getScheme() == null || (!uri.getScheme().equalsIgnoreCase(HTTPS_SCHEME) && !requestUri.startsWith(RequestObjectService.RESOURCE_OBJECT_URN_PREFIX))) { + if (uri.getScheme() == null || + (!uri.getScheme().equalsIgnoreCase(HTTPS_SCHEME) && + !requestUri.startsWith(RequestObjectService.RESOURCE_OBJECT_URN_PREFIX) && + !requestUri.startsWith(PushedAuthorizationRequestService.PAR_URN_PREFIX))) { throw new InvalidRequestUriException("request_uri parameter scheme must be HTTPS"); } } catch (IllegalArgumentException iae) { @@ -184,15 +194,15 @@ private Maybe handleRequestObjectValue(RoutingContext context) { private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) { if (this.domain.usePlainFapiProfile()) { try { + final boolean fromRequestUri = context.get(REQUEST_OBJECT_FROM_URI); final JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet(); // according to https://openid.net/specs/openid-connect-core-1_0.html#RequestObject // OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax // but FAPI requires that these params are equals, so we test the consistency of these parameters // in addition FAPI requires some claims that are optional in the OIDC core spec (like exp, nbf...) - if (jwtClaimsSet.getExpirationTime() == null || jwtClaimsSet.getExpirationTime().before(new Date())) { - throw new InvalidRequestObjectException("Request object must contains valid exp claim"); + throw generateException(jwtClaimsSet.getExpirationTime() == null && fromRequestUri, "Request object must contains valid exp claim"); } List redirectUri = context.queryParam(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); @@ -207,39 +217,39 @@ private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) final Date nbf = jwtClaimsSet.getNotBeforeTime(); if (nbf == null || (nbf.getTime() + ONE_HOUR_IN_MILLIS) < jwtClaimsSet.getExpirationTime().getTime()) { - throw new InvalidRequestObjectException("Request object older than 60 minutes"); + throw generateException(fromRequestUri, "Request object older than 60 minutes"); } List state = context.queryParam(io.gravitee.am.common.oauth2.Parameters.STATE); final String stateClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE); if (state != null && !state.isEmpty() && (stateClaim == null || !stateClaim.equals(state.get(0)))) { - throw new InvalidRequestObjectException("Request object must contains valid state claim"); + throw generateException(fromRequestUri, "Request object must contains valid state claim"); } final OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); if (jwtClaimsSet.getAudience() == null || (openIDProviderMetadata != null && !jwtClaimsSet.getAudience().contains(openIDProviderMetadata.getIssuer()))) { // the aud claim in the request object shall be, or shall be an array containing, the OP’s Issuer Identifier URL; - throw new InvalidRequestObjectException("Invalid audience claim"); + throw generateException(fromRequestUri, "Invalid audience claim"); } List scope = context.queryParam(io.gravitee.am.common.oauth2.Parameters.SCOPE); final String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); if (scope != null && !scope.isEmpty() && (scopeClaim == null || !scopeClaim.equals(scope.get(0)))) { - throw new InvalidRequestObjectException("Request object must contains valid scope claim"); + throw generateException(fromRequestUri, "Request object must contains valid scope claim"); } // String scopeClaim = jwtClaimsSet.getStringClaim(Claims.scope); if (scopeClaim != null && scopeClaim.contains("openid") && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(Parameters.NONCE))) { // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#client-requesting-openid-scope // If the client requests the openid scope, the authorization server shall require the nonce parameter defined - throw new InvalidRequestObjectException("Scope openid expect the nonce parameter defined"); + throw generateException(fromRequestUri, "Scope openid expect the nonce parameter defined"); } else if ((scopeClaim == null || !scopeClaim.contains("openid")) && StringUtils.isEmpty(jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.STATE))) { // https://openid.net/specs/openid-financial-api-part-1-1_0-final.html#clients-not-requesting-openid-scope // If the client does not request the openid scope, the authorization server shall require the state parameter defined - throw new InvalidRequestObjectException("Absence of scope openid expect the state parameter defined"); + throw generateException(fromRequestUri, "Absence of scope openid expect the state parameter defined"); } } catch (OAuth2Exception e) { @@ -255,6 +265,11 @@ private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) return Single.just(jwt); } + private OAuth2Exception generateException(boolean throwUriException, String msg) { + // according to the request mode (PAR or std), FAPI error code maybe different + return throwUriException ? new InvalidRequestUriException(msg) : new InvalidRequestObjectException(msg); + } + private Maybe handleRequestObjectURI(RoutingContext context) { final String requestUri = context.request().getParam(Parameters.REQUEST_URI); @@ -262,10 +277,16 @@ private Maybe handleRequestObjectURI(RoutingContext context) { // Ensure that the request_uri is not propagated to the next authorization flow step context.request().params().remove(Parameters.REQUEST_URI); - return requestObjectService - .readRequestObjectFromURI(requestUri, context.get(CLIENT_CONTEXT_KEY)) - .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) - .toMaybe(); + if (requestUri.startsWith(PushedAuthorizationRequestService.PAR_URN_PREFIX)) { + return parService.readFromURI(requestUri, context.get(CLIENT_CONTEXT_KEY), context.get(PROVIDER_METADATA_CONTEXT_KEY)) + .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) + .toMaybe(); + } else { + return requestObjectService + .readRequestObjectFromURI(requestUri, context.get(CLIENT_CONTEXT_KEY)) + .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) + .toMaybe(); + } } else { return Maybe.empty(); } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestResponse.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestResponse.java new file mode 100644 index 0000000000..59dde48f25 --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestResponse.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.service.par; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-par-10#section-2.2 + * + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class PushedAuthorizationRequestResponse { + // The request URI corresponding to the authorization request posted. + @JsonProperty("request_uri") + private String requestUri; + + // A JSON number that represents the lifetime of the request URI in seconds as a positive integer + @JsonProperty("expires_in") + private long exp; + + public String getRequestUri() { + return requestUri; + } + + public void setRequestUri(String requestUri) { + this.requestUri = requestUri; + } + + public long getExp() { + return exp; + } + + public void setExp(long exp) { + this.exp = exp; + } +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java new file mode 100644 index 0000000000..362496aa7f --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.service.par; + +import com.nimbusds.jwt.JWT; +import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; +import io.gravitee.am.model.oidc.Client; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.reactivex.Single; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public interface PushedAuthorizationRequestService { + /** + * The URN prefix used by AS when storing the Authorization Request parameters + */ + String PAR_URN_PREFIX = "urn:ietf:params:oauth:request_uri:"; + + /** + * Read authorization parameters from the registered URI. + * If the parameters contains the request one, the JWT is validated first and returned as it. + * If request parameter is missing, JWT is build from other parameters as a PlainJWT + * (with the aud claim targeting initialized with the value provider through the issuer OIDC metadata) + * + * @param requestUri + * @param client + * @return + */ + Single readFromURI(String requestUri, Client client, OpenIDProviderMetadata oidcMetadata); + + /** + * Register a request object for a given Client + * @return + */ + Single registerParameters(PushedAuthorizationRequest par, Client client); +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java new file mode 100644 index 0000000000..d7c14dfbe2 --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java @@ -0,0 +1,214 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.service.par.impl; + +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; +import com.nimbusds.jwt.SignedJWT; +import io.gravitee.am.common.exception.oauth2.InvalidRequestException; +import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; +import io.gravitee.am.common.exception.oauth2.InvalidRequestUriException; +import io.gravitee.am.common.exception.oauth2.OAuth2Exception; +import io.gravitee.am.common.oauth2.Parameters; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestResponse; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; +import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDDiscoveryService; +import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; +import io.gravitee.am.gateway.handler.oidc.service.jwe.JWEService; +import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; +import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService; +import io.gravitee.am.model.Domain; +import io.gravitee.am.model.jose.JWK; +import io.gravitee.am.model.oidc.Client; +import io.gravitee.am.model.oidc.JWKSet; +import io.gravitee.am.repository.oauth2.api.PushedAuthorizationRequestRepository; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.reactivex.*; +import io.reactivex.functions.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.text.ParseException; +import java.time.Instant; +import java.util.Date; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class PushedAuthorizationRequestServiceImpl implements PushedAuthorizationRequestService { + private static Logger LOGGER = LoggerFactory.getLogger(PushedAuthorizationRequestServiceImpl.class); + + /** + * Validity in millis for the request_uri + */ + @Value("${authorization.request_uri.validity:60000}") + private int requestUriValidity = 60000; + + @Autowired + private Domain domain; + + @Autowired + private PushedAuthorizationRequestRepository parRepository; + + @Autowired + private JWSService jwsService; + + @Autowired + private JWEService jweService; + + @Autowired + private JWKService jwkService; + + @Override + public Single readFromURI(String requestUri, Client client, OpenIDProviderMetadata oidcMetadata) { + if (requestUri.startsWith(PAR_URN_PREFIX)) { + // Extract the identifier + String identifier = requestUri.substring(PAR_URN_PREFIX.length()); + + return parRepository.findById(identifier) + .switchIfEmpty(Single.error(new InvalidRequestUriException())) + // request_uri is a one shot use + .flatMap(par -> parRepository.delete(identifier).andThen(Single.just(par))) + .flatMap((Function>) req -> { + if (req.getParameters() != null && + req.getExpireAt() != null && + req.getExpireAt().after(new Date())) { + + final String request = req.getParameters().getFirst(io.gravitee.am.common.oidc.Parameters.REQUEST); + if (request != null) { + return readRequestObject(client, request); + } else if (this.domain.usePlainFapiProfile()) { + return Single.error(new InvalidRequestException("request parameter is missing")); + } else { + // request object isn't specified, create a PlainJWT based on the parameters + final JWTClaimsSet.Builder builder = new JWTClaimsSet + .Builder() + .audience(oidcMetadata.getIssuer()) + .expirationTime(req.getExpireAt()); + req.getParameters().toSingleValueMap().forEach((key, value) -> { + builder.claim(key, value); + }); + return Single.just(new PlainJWT(builder.build())); + } + } + return Single.error(new InvalidRequestUriException()); + }); + } else { + return Single.error(new InvalidRequestException("Invalid request_uri")); + } + } + + @Override + public Single registerParameters(PushedAuthorizationRequest par, Client client) { + par.setClient(client.getId()); // link parameters to the internal client identifier + par.setExpireAt(new Date(Instant.now().plusMillis(requestUriValidity).toEpochMilli())); + + Completable registrationValidation = Completable.fromAction(() -> { + if (!client.getClientId().equals(par.getParameters().getFirst(Parameters.CLIENT_ID))) { + throw new InvalidRequestException(); + } + if (par.getParameters().getFirst(io.gravitee.am.common.oidc.Parameters.REQUEST_URI) != null) { + throw new InvalidRequestException("request_uri not authorized"); + } + }); + + final String request = par.getParameters().getFirst(io.gravitee.am.common.oidc.Parameters.REQUEST); + if (request != null) { + registrationValidation = registrationValidation.andThen(Single.defer(() -> + readRequestObject(client, request))) + .ignoreElement(); + } + + return registrationValidation.andThen(Single.defer(() -> parRepository.create(par))).map(parPersisted -> { + final PushedAuthorizationRequestResponse response = new PushedAuthorizationRequestResponse(); + response.setRequestUri(PAR_URN_PREFIX + parPersisted.getId()); + // the lifetime of the request URI in seconds as a positive integer + final long exp = (parPersisted.getExpireAt().getTime() - Instant.now().toEpochMilli()) / 1000; + response.setExp(exp); + return response; + }); + } + + private Single readRequestObject(Client client, String request) { + return jweService.decrypt(request, client) + .onErrorResumeNext((ex) -> { + if (ex instanceof OAuth2Exception) { + return Single.error(ex); + } + LOGGER.debug("JWT invalid for the request parameter", ex); + return Single.error(new InvalidRequestObjectException()); + }) + .map(jwt -> checkRequestObjectClaims(jwt)) + .map(this::checkRequestObjectAlgorithm) + .flatMap(jwt -> validateSignature((SignedJWT) jwt, client)); + } + + private JWT checkRequestObjectClaims(JWT jwt) { + try { + + if (jwt.getJWTClaimsSet().getStringClaim(io.gravitee.am.common.oidc.Parameters.REQUEST) != null + || jwt.getJWTClaimsSet().getStringClaim(io.gravitee.am.common.oidc.Parameters.REQUEST_URI) != null) { + throw new InvalidRequestObjectException("Claims request and request_uri are forbidden"); + } + + return jwt; + } catch (ParseException e) { + LOGGER.warn("request object received in PAR request is malformed: {}", e.getMessage()); + throw new InvalidRequestObjectException(); + } + } + + private JWT checkRequestObjectAlgorithm(JWT jwt) { + // The authorization server shall verify that the request object is valid, the signature algorithm is not + // none, and the signature is correct as in clause 6.3 of [OIDC]. + if (! (jwt instanceof SignedJWT) || + (jwt.getHeader().getAlgorithm() != null && "none".equalsIgnoreCase(jwt.getHeader().getAlgorithm().getName()))) { + throw new InvalidRequestObjectException("Request object must be signed"); + } + return jwt; + } + + private Single validateSignature(SignedJWT jwt, Client client) { + return jwkService.getKeys(client) + .switchIfEmpty(Maybe.error(new InvalidRequestObjectException())) + .flatMap(new Function>() { + @Override + public MaybeSource apply(JWKSet jwkSet) throws Exception { + return jwkService.getKey(jwkSet, jwt.getHeader().getKeyID()); + } + }) + .switchIfEmpty(Maybe.error(new InvalidRequestObjectException())) + .flatMapSingle(new Function>() { + @Override + public SingleSource apply(JWK jwk) throws Exception { + // 6.3.2. Signed Request Object + // To perform Signature Validation, the alg Header Parameter in the + // JOSE Header MUST match the value of the request_object_signing_alg + // set during Client Registration + if (jwt.getHeader().getAlgorithm().getName().equals(client.getRequestObjectSigningAlg()) && + jwsService.isValidSignature(jwt, jwk)) { + return Single.just(jwt); + } else { + return Single.error(new InvalidRequestObjectException("Invalid signature")); + } + } + }); + } +} \ No newline at end of file diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/RequestObjectRegistrationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/RequestObjectRegistrationEndpoint.java index d602e3f8bd..bf5e5c0acf 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/RequestObjectRegistrationEndpoint.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/resources/endpoint/RequestObjectRegistrationEndpoint.java @@ -38,9 +38,12 @@ /** * See 7. Request object endpoint * + * Deprecated: should use the Pushed Authorization Request endpoint + * * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Team */ +@Deprecated public class RequestObjectRegistrationEndpoint implements Handler { private static final Logger LOGGER = LoggerFactory.getLogger(RequestObjectRegistrationEndpoint.class); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java index 62f58e2beb..9d31dd3adb 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/OpenIDProviderMetadata.java @@ -171,6 +171,9 @@ public String getIssuer() { @JsonProperty("tls_client_certificate_bound_access_tokens") private Boolean tlsClientCertificateBoundAccessTokens = Boolean.FALSE; + @JsonProperty("pushed_authorization_request_endpoint") + private String parEndpoint; + public void setIssuer(String issuer) { this.issuer = issuer; } @@ -534,4 +537,12 @@ public Boolean getTlsClientCertificateBoundAccessTokens() { public void setTlsClientCertificateBoundAccessTokens(Boolean tlsClientCertificateBoundAccessTokens) { this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; } + + public String getParEndpoint() { + return parEndpoint; + } + + public void setParEndpoint(String parEndpoint) { + this.parEndpoint = parEndpoint; + } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java index cc868b65d9..6d3f6c5a9a 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/discovery/impl/OpenIDDiscoveryServiceImpl.java @@ -45,6 +45,7 @@ public class OpenIDDiscoveryServiceImpl implements OpenIDDiscoveryService { private static final String AUTHORIZATION_ENDPOINT = "/oauth/authorize"; private static final String TOKEN_ENDPOINT = "/oauth/token"; + private static final String PAR_ENDPOINT = "/oauth/par"; private static final String USERINFO_ENDPOINT = "/oidc/userinfo"; private static final String JWKS_URI = "/oidc/.well-known/jwks.json"; private static final String REVOCATION_ENDPOINT = "/oauth/revoke"; @@ -80,6 +81,7 @@ public OpenIDProviderMetadata getConfiguration(String basePath) { openIDProviderMetadata.setEndSessionEndpoint(getEndpointAbsoluteURL(basePath, ENDSESSION_ENDPOINT)); openIDProviderMetadata.setRegistrationEndpoint(getEndpointAbsoluteURL(basePath, REGISTRATION_ENDPOINT)); openIDProviderMetadata.setRequestObjectEndpoint(getEndpointAbsoluteURL(basePath, REQUEST_OBJECT_ENDPOINT)); + openIDProviderMetadata.setParEndpoint(getEndpointAbsoluteURL(basePath, PAR_ENDPOINT)); openIDProviderMetadata.setRegistrationRenewSecretEndpoint(openIDProviderMetadata.getRegistrationEndpoint()+"/:client_id/renew_secret"); if(domain.isDynamicClientRegistrationTemplateEnabled()) { openIDProviderMetadata.setRegistrationTemplatesEndpoint(openIDProviderMetadata.getRegistrationEndpoint()+"_templates"); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/spring/OIDCConfiguration.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/spring/OIDCConfiguration.java index 5be126e4af..c78af7d2e1 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/spring/OIDCConfiguration.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/spring/OIDCConfiguration.java @@ -16,6 +16,8 @@ package io.gravitee.am.gateway.handler.oidc.spring; import io.gravitee.am.gateway.handler.api.ProtocolConfiguration; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; +import io.gravitee.am.gateway.handler.oauth2.service.par.impl.PushedAuthorizationRequestServiceImpl; import io.gravitee.am.gateway.handler.oauth2.spring.OAuth2Configuration; import io.gravitee.am.gateway.handler.oidc.service.clientregistration.ClientService; import io.gravitee.am.gateway.handler.oidc.service.clientregistration.DynamicClientRegistrationService; @@ -92,4 +94,9 @@ public JWSService jwsService() { public RequestObjectService requestObjectService() { return new RequestObjectServiceImpl(); } + + @Bean + public PushedAuthorizationRequestService pushedAuthorizationRequestService() { + return new PushedAuthorizationRequestServiceImpl(); + } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java new file mode 100644 index 0000000000..e17c40d54f --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java @@ -0,0 +1,339 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.service.par; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import io.gravitee.am.common.exception.oauth2.InvalidRequestException; +import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; +import io.gravitee.am.common.exception.oauth2.InvalidRequestUriException; +import io.gravitee.am.common.jwt.Claims; +import io.gravitee.am.common.oidc.Parameters; +import io.gravitee.am.gateway.handler.oauth2.service.par.impl.PushedAuthorizationRequestServiceImpl; +import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; +import io.gravitee.am.gateway.handler.oidc.service.jwe.JWEService; +import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; +import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService; +import io.gravitee.am.model.Domain; +import io.gravitee.am.model.jose.JWK; +import io.gravitee.am.model.oidc.Client; +import io.gravitee.am.model.oidc.JWKSet; +import io.gravitee.am.repository.oauth2.api.PushedAuthorizationRequestRepository; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.gravitee.common.util.LinkedMultiValueMap; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.reactivex.observers.TestObserver; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.text.ParseException; +import java.time.Instant; +import java.util.Date; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@RunWith(MockitoJUnitRunner.class) +public class PushedAuthorizationRequestServiceTest { + + @InjectMocks + private PushedAuthorizationRequestServiceImpl cut ; + + @Mock + private PushedAuthorizationRequestRepository repository; + + @Mock + private Domain domain; + + @Mock + private JWSService jwsService; + + @Mock + private JWEService jweService; + + @Mock + private JWKService jwkService; + + @Test + public void shouldNotPersist_ClientIdMismatch() { + final Client client = new Client(); + client.setClientId("clientid"); + + final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("scope", "openid"); + parameters.add("response_type", "code"); + parameters.add("client_id", "otherid"); + par.setParameters(parameters); + + final TestObserver observer = cut.registerParameters(par, client).test(); + + observer.awaitTerminalEvent(); + observer.assertError(InvalidRequestException.class); + verify(repository, never()).create(any()); + } + + @Test + public void shouldPersist_ParametersWithoutRequest() { + final Client client = createClient(); + + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("scope", "openid"); + parameters.add("response_type", "code"); + parameters.add("client_id", client.getClientId()); + + final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setParameters(parameters); + par.setId("parid"); + par.setClient(client.getId()); + + when(repository.create(any())).thenReturn(Single.just(par)); + + final TestObserver observer = cut.registerParameters(par, client).test(); + + observer.awaitTerminalEvent(); + observer.assertNoErrors(); + observer.assertValue(parr -> parr.getExp() > 0 && parr.getRequestUri().equals(PushedAuthorizationRequestService.PAR_URN_PREFIX+par.getId())); + + verify(repository).create(any()); + } + + @Test + public void shouldNotPersist_RequestUriPresent() { + final Client client = createClient(); + + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("request_uri", "urn:toto"); + parameters.add("client_id", client.getClientId()); + + final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setParameters(parameters); + par.setId("parid"); + par.setClient(client.getId()); + + final TestObserver observer = cut.registerParameters(par, client).test(); + + observer.awaitTerminalEvent(); + observer.assertFailure(InvalidRequestException.class); + + verify(repository, never()).create(any()); + } + + @Test + public void shouldNotPersist_RequestMalformed() { + final Client client = createClient(); + + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("request", "invalid json object"); + parameters.add("client_id", client.getClientId()); + + final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setParameters(parameters); + par.setId("parid"); + par.setClient(client.getId()); + + when(jweService.decrypt(any(), any())).thenReturn(Single.error(new ParseException("parse error",1))); + + final TestObserver observer = cut.registerParameters(par, client).test(); + + observer.awaitTerminalEvent(); + observer.assertFailure(InvalidRequestObjectException.class); + + verify(repository, never()).create(any()); + } + + @Test + public void shouldNotPersist_RequestWithUnexpectedClaims() throws Exception { + final Client client = createClient(); + + final String jwtString = "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMTIzNDU2Nzg5MCIsImh0dHA6Ly9vcCJdLCJjbGllbnRfaWQiOiJjbGllbnRpZCIsInJlcXVlc3QiOiJkc2Zkc2YifQ.S6EQZgosP7FlIfyiV85bjeWnEW4yGjf8PlAZiYIZkyIgiHzlFIEnisxc_P42dKcFK8azW6xVw7OiOYLoIEo2QhZqvT4YZWgAjlqZoaMzBs68zkQr10xXrMLK8k-6wsQUONy49f7cR5niauuKYMgeVc4k5qLDvc6p1iKfUZu6VVvv-nhNT3GOacgJqwviofI-ZvBGGr0O8kP13nWf5RRElNgNw06Hnza139KwqEsim7kFDzs9TCrXl-3CzYvYtF-VYTsDTLf9ArkJgsxvs1PSULu0Sq9m5_sokJuV3DiF9daj2v3Zmd0ZYRbr1OSKreseW0fxNGmQZyHaVgtEowUv8g"; + final JWT parse = JWTParser.parse(jwtString); + + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("request", jwtString); + parameters.add("client_id", client.getClientId()); + + final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setParameters(parameters); + par.setId("parid"); + par.setClient(client.getId()); + + when(jweService.decrypt(any(), any())).thenReturn(Single.just(parse)); + + final TestObserver observer = cut.registerParameters(par, client).test(); + + observer.awaitTerminalEvent(); + observer.assertError(e -> e instanceof InvalidRequestObjectException && e.getMessage().equals("Claims request and request_uri are forbidden")); + + verify(repository, never()).create(any()); + } + + private Client createClient() { + final Client client = new Client(); + client.setId("cid"); + client.setClientId("clientid"); + return client; + } + + @Test + public void shouldNot_ReadFromURI_InvalidURI() { + final TestObserver testObserver = cut.readFromURI("invalideuri", createClient(), new OpenIDProviderMetadata()).test(); + + testObserver.awaitTerminalEvent(); + testObserver.assertError(InvalidRequestException.class); + + verify(repository, never()).findById(any()); + } + + @Test + public void shouldReadFromURI_UnknownId() { + final String ID = "parid"; + final String requestUri = PushedAuthorizationRequestService.PAR_URN_PREFIX + ID; + + when(repository.findById(ID)).thenReturn(Maybe.empty()); + + final TestObserver testObserver = cut.readFromURI(requestUri, createClient(), new OpenIDProviderMetadata()).test(); + + testObserver.awaitTerminalEvent(); + testObserver.assertError(InvalidRequestUriException.class); + + verify(repository).findById(eq(ID)); + } + + @Test + public void shouldReadFromURI_ExpiredPAR() { + final String ID = "parid"; + final String requestUri = PushedAuthorizationRequestService.PAR_URN_PREFIX + ID; + + PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setExpireAt(new Date(Instant.now().minusSeconds(10).toEpochMilli())); + + when(repository.findById(ID)).thenReturn(Maybe.just(par)); + + final TestObserver testObserver = cut.readFromURI(requestUri, createClient(), new OpenIDProviderMetadata()).test(); + + testObserver.awaitTerminalEvent(); + testObserver.assertError(InvalidRequestUriException.class); + + verify(repository).findById(eq(ID)); + } + + + @Test + public void shouldReadFromURI_MissingRequest_FAPI() { + final String ID = "parid"; + final String requestUri = PushedAuthorizationRequestService.PAR_URN_PREFIX + ID; + + PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setParameters(new LinkedMultiValueMap<>()); + par.setExpireAt(new Date(Instant.now().plusSeconds(10).toEpochMilli())); + + when(domain.usePlainFapiProfile()).thenReturn(true); + when(repository.findById(ID)).thenReturn(Maybe.just(par)); + + final TestObserver testObserver = cut.readFromURI(requestUri, createClient(), new OpenIDProviderMetadata()).test(); + + testObserver.awaitTerminalEvent(); + testObserver.assertError(InvalidRequestException.class); + + verify(repository).findById(eq(ID)); + } + + @Test + public void shouldReadFromURI_MissingRequestParameter() { + // in this test, Plain JWT is created using the set of parameters + final String ID = "parid"; + final String requestUri = PushedAuthorizationRequestService.PAR_URN_PREFIX + ID; + + PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setExpireAt(new Date(Instant.now().plusSeconds(10).toEpochMilli())); + + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("key1", "value1"); + parameters.add("key2", "value2"); + par.setParameters(parameters); + + when(repository.findById(ID)).thenReturn(Maybe.just(par)); + + final TestObserver testObserver = cut.readFromURI(requestUri, createClient(), new OpenIDProviderMetadata()).test(); + + testObserver.awaitTerminalEvent(); + testObserver.assertNoErrors(); + testObserver.assertValue(jwt -> + jwt.getJWTClaimsSet().getStringClaim("key1") != null && + jwt.getJWTClaimsSet().getStringClaim("key2") != null); + + verify(repository).findById(eq(ID)); + } + + + @Test + public void shouldReadFromURI_RequestParameter() throws Exception { + // in this test, Plain JWT is created using the set of parameters + final String ID = "parid"; + final String requestUri = PushedAuthorizationRequestService.PAR_URN_PREFIX + ID; + + PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setExpireAt(new Date(Instant.now().plusSeconds(10).toEpochMilli())); + + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add(Parameters.REQUEST, "some-jwt"); + par.setParameters(parameters); + + when(repository.findById(ID)).thenReturn(Maybe.just(par)); + + JWTClaimsSet claimsSet = new JWTClaimsSet + .Builder() + .claim(Claims.aud, "https://op/domain/oidc") + .claim(Claims.scope, "openid") + .claim(io.gravitee.am.common.oauth2.Parameters.RESPONSE_TYPE, "code") + .build(); + final SignedJWT signedJwt = mock(SignedJWT.class); + when(signedJwt.getJWTClaimsSet()).thenReturn(claimsSet); + when(jweService.decrypt(any(), any())).thenReturn(Single.just(signedJwt)); + final JWSHeader jwsHeaders = new JWSHeader.Builder(JWSAlgorithm.RS256).build(); + when(signedJwt.getHeader()).thenReturn(jwsHeaders); + + when(jwkService.getKeys(any(Client.class))).thenReturn(Maybe.just(mock(JWKSet.class))); + when(jwkService.getKey(any(), any())).thenReturn(Maybe.just(mock(JWK.class))); + when( jwsService.isValidSignature(any(), any())).thenReturn(true); + + final Client client = createClient(); + client.setRequestObjectSigningAlg(JWSAlgorithm.RS256.getName()); + + final TestObserver testObserver = cut.readFromURI(requestUri, client, new OpenIDProviderMetadata()).test(); + + testObserver.awaitTerminalEvent(); + testObserver.assertNoErrors(); + testObserver.assertValue(jwt -> + jwt.getJWTClaimsSet().getStringClaim(Claims.aud) != null && + jwt.getJWTClaimsSet().getStringClaim(Claims.scope) != null); + + verify(repository).findById(eq(ID)); + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepository.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepository.java new file mode 100644 index 0000000000..fc91b5ecea --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepository.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.oauth2.api; + +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Single; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public interface PushedAuthorizationRequestRepository { + + Maybe findById(String id); + + Single create(PushedAuthorizationRequest par); + + Completable delete(String id); + + default Completable purgeExpiredData() { + return Completable.complete(); + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/PushedAuthorizationRequest.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/PushedAuthorizationRequest.java new file mode 100644 index 0000000000..9a8e721a2a --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/oauth2/model/PushedAuthorizationRequest.java @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.oauth2.model; + +import io.gravitee.common.util.MultiValueMap; + +import java.util.Date; +import java.util.Map; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Teams + */ +public class PushedAuthorizationRequest { + + /** + * Technical ID + */ + private String id; + + /** + * Request domain + */ + private String domain; + + /** + * Technical identifier of the client which store this parameters + */ + private String client; + + /** + * The authorization parameters to preserve + */ + private MultiValueMap parameters; + + /** + * The creation date + */ + private Date createdAt; + + /** + * The expiration date + */ + private Date expireAt; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getClient() { + return client; + } + + public void setClient(String client) { + this.client = client; + } + + public MultiValueMap getParameters() { + return parameters; + } + + public void setParameters(MultiValueMap parameters) { + this.parameters = parameters; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getExpireAt() { + return expireAt; + } + + public void setExpireAt(Date expireAt) { + this.expireAt = expireAt; + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/JdbcPushedAuthorizationRequestRepository.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/JdbcPushedAuthorizationRequestRepository.java new file mode 100644 index 0000000000..c6f9344dac --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/JdbcPushedAuthorizationRequestRepository.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.jdbc.oauth2.api; + +import io.gravitee.am.common.utils.RandomString; +import io.gravitee.am.repository.jdbc.management.AbstractJdbcRepository; +import io.gravitee.am.repository.jdbc.oauth2.api.model.JdbcPushedAuthorizationRequest; +import io.gravitee.am.repository.jdbc.oauth2.api.spring.SpringPushedAuthorizationRequestRepository; +import io.gravitee.am.repository.oauth2.api.PushedAuthorizationRequestRepository; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +import static java.time.ZoneOffset.UTC; +import static org.springframework.data.relational.core.query.Criteria.where; +import static reactor.adapter.rxjava.RxJava2Adapter.monoToCompletable; +import static reactor.adapter.rxjava.RxJava2Adapter.monoToSingle; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@Repository +public class JdbcPushedAuthorizationRequestRepository extends AbstractJdbcRepository implements PushedAuthorizationRequestRepository { + + @Autowired + private SpringPushedAuthorizationRequestRepository parRepository; + + protected PushedAuthorizationRequest toEntity(JdbcPushedAuthorizationRequest entity) { + return mapper.map(entity, PushedAuthorizationRequest.class); + } + + protected JdbcPushedAuthorizationRequest toJdbcEntity(PushedAuthorizationRequest entity) { + return mapper.map(entity, JdbcPushedAuthorizationRequest.class); + } + + @Override + public Maybe findById(String id) { + LOGGER.debug("findById({})", id); + LocalDateTime now = LocalDateTime.now(UTC); + return parRepository.findById(id) + .filter(bean -> bean.getExpireAt() == null || bean.getExpireAt().isAfter(now)) + .map(this::toEntity) + .doOnError(error -> LOGGER.error("Unable to retrieve PushedAuthorizationRequest with id {}", id, error)); + } + + @Override + public Single create(PushedAuthorizationRequest par) { + par.setId(par.getId() == null ? RandomString.generate() : par.getId()); + LOGGER.debug("Create PushedAuthorizationRequest with id {}", par.getId()); + + Mono action = dbClient.insert() + .into(JdbcPushedAuthorizationRequest.class) + .using(toJdbcEntity(par)) + .fetch().rowsUpdated(); + + return monoToSingle(action).flatMap((i) -> parRepository.findById(par.getId()).map(this::toEntity).toSingle()) + .doOnError((error) -> LOGGER.error("Unable to create PushedAuthorizationRequest with id {}", par.getId(), error)); + } + + @Override + public Completable delete(String id) { + LOGGER.debug("delete({})", id); + return parRepository.deleteById(id) + .doOnError(error -> LOGGER.error("Unable to delete PushedAuthorizationRequest with id {}", id, error)); + } + + public Completable purgeExpiredData() { + LOGGER.debug("purgeExpiredData()"); + LocalDateTime now = LocalDateTime.now(UTC); + return monoToCompletable(dbClient.delete() + .from(JdbcPushedAuthorizationRequest.class) + .matching(where("expire_at") + .lessThan(now)).then()) + .doOnError(error -> LOGGER.error("Unable to purge PushedAuthorizationRequest", error)); + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcPushedAuthorizationRequest.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcPushedAuthorizationRequest.java new file mode 100644 index 0000000000..35ecdf511a --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcPushedAuthorizationRequest.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.jdbc.oauth2.api.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@Table("pushed_authorization_requests") +public class JdbcPushedAuthorizationRequest { + @Id + private String id; + private String domain; + private String client; + private String parameters; + @Column("created_at") + private LocalDateTime createdAt; + @Column("expire_at") + private LocalDateTime expireAt; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getClient() { + return client; + } + + public void setClient(String client) { + this.client = client; + } + + public String getParameters() { + return parameters; + } + + public void setParameters(String parameters) { + this.parameters = parameters; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getExpireAt() { + return expireAt; + } + + public void setExpireAt(LocalDateTime expireAt) { + this.expireAt = expireAt; + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/spring/SpringPushedAuthorizationRequestRepository.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/spring/SpringPushedAuthorizationRequestRepository.java new file mode 100644 index 0000000000..1bffada54e --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/spring/SpringPushedAuthorizationRequestRepository.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.jdbc.oauth2.api.spring; + +import io.gravitee.am.repository.jdbc.oauth2.api.model.JdbcPushedAuthorizationRequest; +import org.springframework.data.repository.reactive.RxJava2CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@Repository +public interface SpringPushedAuthorizationRequestRepository extends RxJava2CrudRepository { +} diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/dozer.xml b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/dozer.xml index 31ae345b10..e343bff58a 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/dozer.xml +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/dozer.xml @@ -177,6 +177,16 @@ + + io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest + io.gravitee.am.repository.jdbc.oauth2.api.model.JdbcPushedAuthorizationRequest + + parameters + parameters + io.gravitee.common.util.LinkedMultiValueMap + + + io.gravitee.am.model.Installation io.gravitee.am.repository.jdbc.management.api.model.JdbcInstallation diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml new file mode 100644 index 0000000000..462a6fc0d9 --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml @@ -0,0 +1,30 @@ +databaseChangeLog: + - changeSet: + id: 3.11.0 + author: GraviteeSource Team + changes: + + ## pushed_authorization_requests + ################### + - createTable: + tableName: pushed_authorization_requests + columns: + - column: { name: id, type: nvarchar(64), constraints: { nullable: false } } + - column: { name: domain, type: nvarchar(255), constraints: { nullable: false } } + - column: { name: client, type: nvarchar(64), constraints: { nullable: false } } + - column: { name: parameters, type: clob, constraints: { nullable: false } } + - column: { name: created_at, type: timestamp(6), constraints: { nullable: true } } + - column: { name: expire_at, type: timestamp(6), constraints: { nullable: true } } + + - addPrimaryKey: + constraintName: pk_par + columnNames: id + tableName: pushed_authorization_requests + + - createIndex: + columns: + - column: + name: expire_at + indexName: idx_par_expire + tableName: pushed_authorization_requests + unique: false \ No newline at end of file diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml index 59061672b6..60c6919b0e 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/master.yml @@ -25,4 +25,6 @@ databaseChangeLog: - file: liquibase/changelogs/v3_10_0/schema-add-default-scopes.yml - include: - file: liquibase/changelogs/v3_10_0/schema-add-self-service-account-settings.yml + - include: + - file: liquibase/changelogs/v3_11_0/schema-par.yml diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/jdbc/common/JdbcRepositoriesTestInitializer.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/jdbc/common/JdbcRepositoriesTestInitializer.java index 91ce79c95c..f052bb9d3c 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/jdbc/common/JdbcRepositoriesTestInitializer.java +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/jdbc/common/JdbcRepositoriesTestInitializer.java @@ -110,6 +110,8 @@ public void truncateTables() throws Exception { tables.add("organization_user_attributes"); tables.add("organization_user_addresses"); + tables.add("pushed_authorization_requests"); + io.r2dbc.spi.Connection connection = Flowable.fromPublisher(connectionFactory.create()).blockingFirst(); connection.beginTransaction(); tables.stream().forEach(table -> { diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryPurgeTest.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryPurgeTest.java new file mode 100644 index 0000000000..81a66d66e3 --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryPurgeTest.java @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.oauth2.api; + +import io.gravitee.am.repository.jdbc.oauth2.api.JdbcPushedAuthorizationRequestRepository; +import io.gravitee.am.repository.oauth2.AbstractOAuthTest; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.gravitee.am.repository.oidc.model.RequestObject; +import io.gravitee.common.util.LinkedMultiValueMap; +import io.reactivex.observers.TestObserver; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class PushedAuthorizationRequestRepositoryPurgeTest extends AbstractOAuthTest { + + @Autowired + private JdbcPushedAuthorizationRequestRepository parRepository; + + @Test + public void shouldPurge() { + Instant now = Instant.now(); + PushedAuthorizationRequest object1 = new PushedAuthorizationRequest(); + object1.setDomain("domain"); + object1.setClient("client"); + object1.setParameters(new LinkedMultiValueMap<>()); + object1.setExpireAt(new Date(now.plus(1, ChronoUnit.MINUTES).toEpochMilli())); + + PushedAuthorizationRequest object2 = new PushedAuthorizationRequest(); + object2.setDomain("domain"); + object2.setClient("client"); + object2.setParameters(new LinkedMultiValueMap<>()); + object2.setExpireAt(new Date(now.minus(1, ChronoUnit.MINUTES).toEpochMilli())); + + parRepository.create(object1).test().awaitTerminalEvent(); + parRepository.create(object2).test().awaitTerminalEvent(); + + assertNotNull(parRepository.findById(object1.getId()).blockingGet()); + assertNull(parRepository.findById(object2.getId()).blockingGet()); + + TestObserver testPurge = parRepository.purgeExpiredData().test(); + testPurge.awaitTerminalEvent(); + testPurge.assertNoErrors(); + + assertNotNull(parRepository.findById(object1.getId()).blockingGet()); + assertNull(parRepository.findById(object2.getId()).blockingGet()); + + } + +} diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoPushedAuthorizationRequestRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoPushedAuthorizationRequestRepository.java new file mode 100644 index 0000000000..01fa1bf5cd --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoPushedAuthorizationRequestRepository.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.mongodb.oauth2; + +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MongoCollection; +import io.gravitee.am.common.utils.RandomString; +import io.gravitee.am.repository.mongodb.oauth2.internal.model.PushedAuthorizationRequestMongo; +import io.gravitee.am.repository.oauth2.api.PushedAuthorizationRequestRepository; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.gravitee.common.util.LinkedMultiValueMap; +import io.gravitee.common.util.MultiValueMap; +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import org.bson.Document; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.mongodb.client.model.Filters.eq; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +@Component +public class MongoPushedAuthorizationRequestRepository extends AbstractOAuth2MongoRepository implements PushedAuthorizationRequestRepository { + + private MongoCollection parCollection; + + private static final String FIELD_EXPIRE_AT = "expire_at"; + + @PostConstruct + public void init() { + parCollection = mongoOperations.getCollection("pushed_authorization_requests", PushedAuthorizationRequestMongo.class); + super.init(parCollection); + + // expire after index + super.createIndex(parCollection, new Document(FIELD_EXPIRE_AT, 1), new IndexOptions().expireAfter(0L, TimeUnit.SECONDS)); + } + + @Override + public Maybe findById(String id) { + return Observable + .fromPublisher(parCollection.find(eq(FIELD_ID, id)).limit(1).first()) + .firstElement() + .map(this::convert); + } + + @Override + public Single create(PushedAuthorizationRequest par) { + par.setId(par.getId() == null ? RandomString.generate() : par.getId()); + return Single + .fromPublisher(parCollection.insertOne(convert(par))) + .flatMap(success -> findById(par.getId()).toSingle()); + } + + @Override + public Completable delete(String id) { + return Completable.fromPublisher(parCollection.findOneAndDelete(eq(FIELD_ID, id))); + } + + private PushedAuthorizationRequestMongo convert(PushedAuthorizationRequest par) { + if (par == null) { + return null; + } + + PushedAuthorizationRequestMongo parMongo = new PushedAuthorizationRequestMongo(); + parMongo.setId(par.getId()); + parMongo.setDomain(par.getDomain()); + parMongo.setClient(par.getClient()); + parMongo.setCreatedAt(par.getCreatedAt()); + parMongo.setExpireAt(par.getExpireAt()); + + if (par.getParameters() != null) { + Document document = new Document(); + par.getParameters().forEach((key, value) -> { + document.append(key, value); + }); + parMongo.setParameters(document); + } + + return parMongo; + } + + private PushedAuthorizationRequest convert(PushedAuthorizationRequestMongo parMongo) { + if (parMongo == null) { + return null; + } + + PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setId(parMongo.getId()); + par.setDomain(parMongo.getDomain()); + par.setClient(parMongo.getClient()); + par.setCreatedAt(parMongo.getCreatedAt()); + par.setExpireAt(parMongo.getExpireAt()); + + if (parMongo.getParameters() != null) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parMongo.getParameters().entrySet().forEach(entry -> parameters.put(entry.getKey(), (List) entry.getValue())); + par.setParameters(parameters); + } + + return par; + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoRequestObjectRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoRequestObjectRepository.java index 7278ae0f67..312fe6b6ed 100644 --- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoRequestObjectRepository.java +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/MongoRequestObjectRepository.java @@ -17,7 +17,6 @@ import com.mongodb.client.model.IndexOptions; import com.mongodb.reactivestreams.client.MongoCollection; -import io.gravitee.am.repository.mongodb.oauth2.AbstractOAuth2MongoRepository; import io.gravitee.am.repository.mongodb.oauth2.internal.model.RequestObjectMongo; import io.gravitee.am.repository.oidc.api.RequestObjectRepository; import io.gravitee.am.repository.oidc.model.RequestObject; diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/internal/model/PushedAuthorizationRequestMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/internal/model/PushedAuthorizationRequestMongo.java new file mode 100644 index 0000000000..016345ff35 --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/oauth2/internal/model/PushedAuthorizationRequestMongo.java @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.mongodb.oauth2.internal.model; + +import org.bson.Document; +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.codecs.pojo.annotations.BsonProperty; + +import java.util.Date; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class PushedAuthorizationRequestMongo { + + @BsonId + private String id; + + private String domain; + + @BsonProperty("client") + private String client; + + private Document parameters; + + @BsonProperty("created_at") + private Date createdAt; + + @BsonProperty("expire_at") + private Date expireAt; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getClient() { + return client; + } + + public void setClient(String client) { + this.client = client; + } + + public Document getParameters() { + return parameters; + } + + public void setParameters(Document parameters) { + this.parameters = parameters; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getExpireAt() { + return expireAt; + } + + public void setExpireAt(Date expireAt) { + this.expireAt = expireAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PushedAuthorizationRequestMongo that = (PushedAuthorizationRequestMongo) o; + + return id.equals(that.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryTest.java b/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryTest.java new file mode 100644 index 0000000000..a08a63b8c9 --- /dev/null +++ b/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/PushedAuthorizationRequestRepositoryTest.java @@ -0,0 +1,94 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.repository.oauth2.api; + +import io.gravitee.am.common.utils.RandomString; +import io.gravitee.am.repository.oauth2.AbstractOAuthTest; +import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.gravitee.common.util.LinkedMultiValueMap; +import io.reactivex.observers.TestObserver; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class PushedAuthorizationRequestRepositoryTest extends AbstractOAuthTest { + + @Autowired + private PushedAuthorizationRequestRepository repository; + + @Test + public void shouldNotFindById() { + TestObserver observer = repository.findById("unknown-id").test(); + + observer.awaitTerminalEvent(); + + observer.assertComplete(); + observer.assertValueCount(0); + observer.assertNoErrors(); + } + + @Test + public void shouldFindById() { + PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + final String id = RandomString.generate(); + par.setId(id); + par.setDomain("domain"); + par.setClient("client"); + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("key", "value"); + par.setParameters(parameters); + + repository.create(par).test().awaitTerminalEvent(); + + TestObserver observer = repository.findById(id).test(); + + observer.awaitTerminalEvent(); + + observer.assertComplete(); + observer.assertValueCount(1); + observer.assertNoErrors(); + } + + @Test + public void shouldDelete() { + PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + final String id = RandomString.generate(); + par.setDomain("domain"); + par.setClient("client"); + par.setId(id); + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("key", "value"); + par.setParameters(parameters); + + TestObserver observer = repository + .create(par) + .ignoreElement() + .andThen(repository.findById(id)) + .ignoreElement() + .andThen(repository.delete(id)) + .andThen(repository.findById(id)) + .test(); + + observer.awaitTerminalEvent(); + observer.assertNoValues(); + observer.assertNoErrors(); + } + + +} From 2929a82682d7643b4528af06842702f130f6044b Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 9 Aug 2021 12:33:53 +0200 Subject: [PATCH 14/24] fix: In FAPI mode, excepted client_id, all auth parameters may be provided by the request object fixes gravitee-io/issues#5975 --- .../handler/common/utils/ConstantKeys.java | 5 ++ .../handler/oauth2/OAuth2Provider.java | 2 +- .../authorization/AuthorizationEndpoint.java | 13 +++- .../AbstractAuthorizationRequestHandler.java | 49 +++++++++++++++ ...orizationRequestEndUserConsentHandler.java | 7 ++- ...izationRequestParseIdTokenHintHandler.java | 6 +- ...rizationRequestParseParametersHandler.java | 28 +++++---- ...ationRequestParseRequestObjectHandler.java | 19 +++--- ...RequestParseRequiredParametersHandler.java | 15 ++--- .../handler/authorization/ParamUtils.java | 60 +++++++++++++++++++ .../request/AuthorizationRequestFactory.java | 13 ++-- .../PushedAuthorizationRequestService.java | 9 +++ ...PushedAuthorizationRequestServiceImpl.java | 13 +++- .../endpoint/AuthorizationEndpointTest.java | 9 ++- ...tionRequestParseParametersHandlerTest.java | 2 + 15 files changed, 205 insertions(+), 45 deletions(-) create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AbstractAuthorizationRequestHandler.java create mode 100644 gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java index 6af71f388e..0f63327346 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java @@ -94,4 +94,9 @@ public interface ConstantKeys { // Forgot Password String FORGOT_PASSWORD_FIELDS_KEY = "forgotPwdFormFields"; String FORGOT_PASSWORD_CONFIRM = "forgot_password_confirm"; + + // key used to store & retrieve the request object from the routing context + String REQUEST_OBJECT_KEY = "requestObject"; + // identifier of the Pushed Authorization Parameters + String REQUEST_URI_ID_KEY = "requestUriId"; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index b062a1cd48..50efd133a1 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -213,7 +213,7 @@ private void initRouter() { .handler(authenticationFlowHandler.create()) .handler(new AuthorizationRequestResolveHandler()) .handler(new AuthorizationRequestEndUserConsentHandler(userConsentService)) - .handler(new AuthorizationEndpoint(flow, thymeleafTemplateEngine)) + .handler(new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService)) .failureHandler(new AuthorizationRequestFailureHandler(openIDDiscoveryService, jwtService, jweService)); // Authorization consent endpoint diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java index 13c4f33adb..f68284c582 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java @@ -20,12 +20,14 @@ import io.gravitee.am.gateway.handler.common.vertx.utils.RequestUtils; import io.gravitee.am.gateway.handler.oauth2.exception.AccessDeniedException; import io.gravitee.am.gateway.handler.oauth2.exception.ServerErrorException; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; import io.gravitee.am.gateway.handler.oauth2.service.request.AuthorizationRequest; import io.gravitee.am.gateway.handler.oauth2.service.response.AuthorizationResponse; import io.gravitee.am.gateway.handler.oidc.service.flow.Flow; import io.gravitee.am.model.oidc.Client; import io.gravitee.common.http.HttpHeaders; import io.gravitee.common.http.MediaType; +import io.reactivex.Completable; import io.vertx.core.Handler; import io.vertx.reactivex.core.MultiMap; import io.vertx.reactivex.ext.auth.User; @@ -52,10 +54,12 @@ public class AuthorizationEndpoint implements Handler { private static final String FORM_PARAMETERS = "parameters"; private final Flow flow; private final ThymeleafTemplateEngine engine; + private final PushedAuthorizationRequestService parService; - public AuthorizationEndpoint(Flow flow, ThymeleafTemplateEngine engine) { + public AuthorizationEndpoint(Flow flow, ThymeleafTemplateEngine engine, PushedAuthorizationRequestService parService) { this.flow = flow; this.engine = engine; + this.parService = parService; } @Override @@ -76,7 +80,12 @@ public void handle(RoutingContext context) { // get resource owner io.gravitee.am.model.User endUser = ((io.gravitee.am.gateway.handler.common.vertx.web.auth.user.User) authenticatedUser.getDelegate()).getUser(); - flow.run(request, client, endUser) + final String uriIdentifier = context.get(ConstantKeys.REQUEST_URI_ID_KEY); + parService.deleteRequestUri(uriIdentifier).onErrorResumeNext((err) -> { + logger.warn("Deletion of Pushed Authorization Request with id '{}' failed", uriIdentifier, err); + return Completable.complete(); + }) + .andThen(flow.run(request, client, endUser)) .subscribe( authorizationResponse -> { try { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AbstractAuthorizationRequestHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AbstractAuthorizationRequestHandler.java new file mode 100644 index 0000000000..bbdf526d9e --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AbstractAuthorizationRequestHandler.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization; + +import io.gravitee.am.common.exception.oauth2.InvalidRequestException; +import io.gravitee.am.common.oauth2.Parameters; +import io.gravitee.am.gateway.handler.oauth2.exception.UnsupportedResponseTypeException; +import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; +import io.vertx.reactivex.ext.web.RoutingContext; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public abstract class AbstractAuthorizationRequestHandler { + + protected boolean isJwtAuthRequest(RoutingContext context) { + return !StringUtils.isEmpty(context.request().getParam(io.gravitee.am.common.oidc.Parameters.REQUEST_URI)) || + !StringUtils.isEmpty(context.request().getParam(io.gravitee.am.common.oidc.Parameters.REQUEST)); + } + + protected void checkResponseType(String responseType, OpenIDProviderMetadata openIDProviderMetadata) { + if (responseType == null) { + throw new InvalidRequestException("Missing parameter: " + Parameters.RESPONSE_TYPE); + } + + // get supported response types + List responseTypesSupported = openIDProviderMetadata.getResponseTypesSupported(); + if (!responseTypesSupported.contains(responseType)) { + throw new UnsupportedResponseTypeException("Unsupported response type: " + responseType); + } + } +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestEndUserConsentHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestEndUserConsentHandler.java index e059f6b67d..f80b25964f 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestEndUserConsentHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestEndUserConsentHandler.java @@ -44,6 +44,7 @@ import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.AUTHORIZATION_REQUEST_CONTEXT_KEY; import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY; import static io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest.CONTEXT_PATH; +import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.getOAuthParameter; /** * Once the End-User is authenticated, the Authorization Server MUST obtain an authorization decision before releasing information to the Relying Party. @@ -86,7 +87,7 @@ public void handle(RoutingContext routingContext) { } // if prompt=none and the Client does not have pre-configured consent for the requested Claims, throw interaction_required exception // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest - String prompt = request.params().get(Parameters.PROMPT); + String prompt = getOAuthParameter(routingContext, Parameters.PROMPT); if (prompt != null && Arrays.asList(prompt.split("\\s+")).contains("none")) { routingContext.fail(new InteractionRequiredException("Interaction required")); } else { @@ -96,8 +97,8 @@ public void handle(RoutingContext routingContext) { } // application has forced to prompt consent screen to the user // go to the user consent page - if (request.params().contains(Parameters.PROMPT) - && request.params().get(Parameters.PROMPT).contains("consent")) { + final String prompt = getOAuthParameter(routingContext, Parameters.PROMPT); + if (prompt != null && prompt.contains("consent")) { redirectToConsentPage(routingContext); return; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseIdTokenHintHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseIdTokenHintHandler.java index 60a968be0f..73c50f8d51 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseIdTokenHintHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseIdTokenHintHandler.java @@ -32,6 +32,8 @@ import java.util.Arrays; import java.util.List; +import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.getOAuthParameter; + /** * Silent Re-authentication of subject with ID Token. * @@ -63,8 +65,8 @@ public AuthorizationRequestParseIdTokenHintHandler(IDTokenService idTokenService @Override public void handle(RoutingContext routingContext) { final Client client = routingContext.get(ConstantKeys.CLIENT_CONTEXT_KEY); - final String idTokenHint = routingContext.request().getParam(Parameters.ID_TOKEN_HINT); - final String prompt = routingContext.request().getParam(Parameters.PROMPT); + final String idTokenHint = getOAuthParameter(routingContext, Parameters.ID_TOKEN_HINT); + final String prompt = getOAuthParameter(routingContext, Parameters.PROMPT); // if no id_token_hint parameter, continue; if (idTokenHint == null) { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java index e3e1dc1a4a..17e84bc07e 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java @@ -53,6 +53,7 @@ import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.PROVIDER_METADATA_CONTEXT_KEY; import static io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest.CONTEXT_PATH; +import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.getOAuthParameter; import static io.gravitee.am.service.utils.ResponseTypeUtils.requireNonce; /** @@ -65,7 +66,7 @@ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) * @author GraviteeSource Team */ -public class AuthorizationRequestParseParametersHandler implements Handler { +public class AuthorizationRequestParseParametersHandler extends AbstractAuthorizationRequestHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(AuthorizationRequestParseParametersHandler.class); private final static String LOGIN_ENDPOINT = "/login"; @@ -113,7 +114,7 @@ public void handle(RoutingContext context) { } private void parsePromptParameter(RoutingContext context) { - String prompt = context.request().getParam(Parameters.PROMPT); + String prompt = getOAuthParameter(context, Parameters.PROMPT); if (prompt != null) { // retrieve prompt values (prompt parameter is a space delimited, case sensitive list of ASCII string values) @@ -137,8 +138,8 @@ private void parsePromptParameter(RoutingContext context) { } private void parsePKCEParameter(RoutingContext context, Client client) { - String codeChallenge = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.CODE_CHALLENGE); - String codeChallengeMethod = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.CODE_CHALLENGE_METHOD); + String codeChallenge = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.CODE_CHALLENGE); + String codeChallengeMethod = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.CODE_CHALLENGE_METHOD); if (codeChallenge == null && codeChallengeMethod != null) { throw new InvalidRequestException("Missing parameter: code_challenge"); @@ -178,7 +179,7 @@ protected void parseMaxAgeParameter(RoutingContext context) { return; } - String maxAge = context.request().getParam(Parameters.MAX_AGE); + String maxAge = getOAuthParameter(context, Parameters.MAX_AGE); if (maxAge == null || !maxAge.matches("-?\\d+")) { // none or invalid max age, continue return; @@ -207,7 +208,7 @@ protected void parseMaxAgeParameter(RoutingContext context) { } private void parseClaimsParameter(RoutingContext context) { - String claims = context.request().getParam(Parameters.CLAIMS); + String claims = getOAuthParameter(context, Parameters.CLAIMS); OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); if (claims != null) { try { @@ -233,7 +234,7 @@ private void parseClaimsParameter(RoutingContext context) { } private void parseResponseModeParameter(RoutingContext context) { - String responseMode = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.RESPONSE_MODE); + String responseMode = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.RESPONSE_MODE); OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); if (responseMode == null) { return; @@ -247,8 +248,8 @@ private void parseResponseModeParameter(RoutingContext context) { } private void parseNonceParameter(RoutingContext context) { - String nonce = context.request().getParam(io.gravitee.am.common.oidc.Parameters.NONCE); - String responseType = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.RESPONSE_TYPE); + String nonce = getOAuthParameter(context, io.gravitee.am.common.oidc.Parameters.NONCE); + String responseType = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.RESPONSE_TYPE); // nonce parameter is required for the Hybrid flow if (nonce == null && requireNonce(responseType)) { throw new InvalidRequestException("Missing parameter: nonce is required for Implicit and Hybrid Flow"); @@ -267,7 +268,10 @@ private void parseGrantTypeParameter(Client client) { } private void parseResponseTypeParameter(RoutingContext context, Client client) { - String responseType = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.RESPONSE_TYPE); + String responseType = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.RESPONSE_TYPE); + + // response_type is required and may be provided by query parameter or by request object + checkResponseType(responseType, context.get(PROVIDER_METADATA_CONTEXT_KEY)); // Authorization endpoint implies that the client should have response_type if (client.getResponseTypes() == null) { @@ -283,7 +287,7 @@ private void parseResponseTypeParameter(RoutingContext context, Client client) { // shall require // the response_type value code id_token, or // the response_type value code in conjunction with the response_mode value jwt; - String responseMode = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.RESPONSE_MODE); + String responseMode = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.RESPONSE_MODE); if (!((responseType.equals(io.gravitee.am.common.oidc.ResponseType.CODE_ID_TOKEN)) || (responseType.equals(ResponseType.CODE) && (responseMode != null && responseMode.equalsIgnoreCase("jwt"))))) { throw new InvalidRequestException("Invalid response_type"); @@ -292,7 +296,7 @@ private void parseResponseTypeParameter(RoutingContext context, Client client) { } private void parseRedirectUriParameter(RoutingContext context, Client client) { - String requestedRedirectUri = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + String requestedRedirectUri = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); final List registeredClientRedirectUris = client.getRedirectUris(); final boolean hasRegisteredClientRedirectUris = registeredClientRedirectUris != null && !registeredClientRedirectUris.isEmpty(); final boolean hasRequestedRedirectUri = requestedRedirectUri != null && !requestedRedirectUri.isEmpty(); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index c9670788d9..28904f83b5 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -40,8 +40,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY; -import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.PROVIDER_METADATA_CONTEXT_KEY; +import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.*; /** * The request Authorization Request parameter enables OpenID Connect requests to be passed in a single, @@ -88,10 +87,10 @@ public void handle(RoutingContext context) { // Even if a scope parameter is present in the Request Object value, a scope parameter MUST always be passed // using the OAuth 2.0 request syntax containing the openid scope value to indicate to the underlying OAuth 2.0 // logic that this is an OpenID Connect request. - // This is not the case if the RequestObject comes from a request_uri + // NOTE: In FAPI specification (https://github.com/gravitee-io/issues/issues/5975), scope may come from the RequestObject String scope = context.request().getParam(io.gravitee.am.common.oauth2.Parameters.SCOPE); HashSet scopes = scope != null && !scope.isEmpty() ? new HashSet<>(Arrays.asList(scope.split("\\s+"))) : null; - if (scopes == null || !scopes.contains(Scope.OPENID.getKey())) { + if (!domain.usePlainFapiProfile() && (scopes == null || !scopes.contains(Scope.OPENID.getKey()))) { context.next(); return; } @@ -133,6 +132,7 @@ public void handle(RoutingContext context) { // Check OAuth2 parameters checkOAuthParameters(context, jwt); overrideRequestParameters(context, jwt); + context.put(REQUEST_OBJECT_KEY, jwt); context.next(); } catch (Exception ex) { context.fail(ex); @@ -207,8 +207,8 @@ private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) List redirectUri = context.queryParam(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); final String redirectUriClaim = jwtClaimsSet.getStringClaim(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); - if ( (redirectUri == null) || - (redirectUri != null && (redirectUri.size() != 1 || redirectUriClaim == null || !redirectUriClaim.equals(redirectUri.get(0))))) { + if (redirectUriClaim == null || + (redirectUriClaim != null && redirectUri != null && !redirectUri.isEmpty() && !redirectUriClaim.equals(redirectUri.get(0)))) { // remove redirect_uri provided as parameter and continue to let AuthorizationRequestParseParametersHandler // throws the right error according to the client configuration context.request().params().remove(io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); @@ -280,6 +280,11 @@ private Maybe handleRequestObjectURI(RoutingContext context) { if (requestUri.startsWith(PushedAuthorizationRequestService.PAR_URN_PREFIX)) { return parService.readFromURI(requestUri, context.get(CLIENT_CONTEXT_KEY), context.get(PROVIDER_METADATA_CONTEXT_KEY)) .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) + .map(jwt -> { + final String uriIdentifier = requestUri.substring(PushedAuthorizationRequestService.PAR_URN_PREFIX.length()); + context.put(REQUEST_URI_ID_KEY, uriIdentifier); + return jwt; + }) .toMaybe(); } else { return requestObjectService @@ -308,7 +313,7 @@ private void checkOAuthParameters(RoutingContext context, JWT jwt) { } String reqObjResponseType = (String) claims.get(io.gravitee.am.common.oauth2.Parameters.RESPONSE_TYPE); - if (reqObjResponseType != null && !reqObjResponseType.equals(responseType)) { + if (responseType != null && reqObjResponseType != null && !reqObjResponseType.equals(responseType)) { throw new InvalidRequestObjectException("response_type does not match request parameter"); } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequiredParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequiredParametersHandler.java index b5547b1a2b..e687c0ac1b 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequiredParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequiredParametersHandler.java @@ -44,7 +44,7 @@ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) * @author GraviteeSource Team */ -public class AuthorizationRequestParseRequiredParametersHandler implements Handler { +public class AuthorizationRequestParseRequiredParametersHandler extends AbstractAuthorizationRequestHandler implements Handler { @Override public void handle(RoutingContext context) { @@ -77,14 +77,11 @@ private void parseResponseTypeParameter(RoutingContext context) { String responseType = context.request().getParam(Parameters.RESPONSE_TYPE); OpenIDProviderMetadata openIDProviderMetadata = context.get(PROVIDER_METADATA_CONTEXT_KEY); - if (responseType == null) { - throw new InvalidRequestException("Missing parameter: " + Parameters.RESPONSE_TYPE); - } - - // get supported response types - List responseTypesSupported = openIDProviderMetadata.getResponseTypesSupported(); - if (!responseTypesSupported.contains(responseType)) { - throw new UnsupportedResponseTypeException("Unsupported response type: " + responseType); + if (!isJwtAuthRequest(context)) { + // for non JAR request, response_type is required as query parameter + // otherwise, it can be provided by the request object and will be checked + // later in the flow by io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.AuthorizationRequestParseParametersHandler + checkResponseType(responseType, openIDProviderMetadata); } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java new file mode 100644 index 0000000000..f804a57622 --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization; + +import com.nimbusds.jwt.JWT; +import io.gravitee.am.common.oidc.Parameters; +import io.gravitee.am.gateway.handler.common.utils.ConstantKeys; +import io.vertx.core.json.Json; +import io.vertx.reactivex.ext.web.RoutingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.ParseException; +import java.util.Optional; + +/** + * This utility class is used to extract OAuth parameters either from the query parameters + * or from the RequestObject. + * + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class ParamUtils { + private static Logger LOGGER = LoggerFactory.getLogger(ParamUtils.class); + + public static String getOAuthParameter(RoutingContext context, String paramName) { + Optional value = Optional.empty(); + final JWT requestObject = context.get(ConstantKeys.REQUEST_OBJECT_KEY); + if (requestObject != null) { + try { + // return parameter from the request object first as When the request parameter (or request_uri) is used, + // the OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax. + if (Parameters.CLAIMS.equals(paramName) && requestObject.getJWTClaimsSet().getClaim(paramName) != null) { + value = Optional.ofNullable(Json.encode(requestObject.getJWTClaimsSet().getClaim(paramName))); + } else { + value = Optional.ofNullable(requestObject.getJWTClaimsSet().getStringClaim(paramName)); + } + } catch (ParseException e) { + LOGGER.warn("Unable to extract parameter '{}' from RequestObject", paramName); + } + } + // if parameter is missing from the request object (or if the extract fails) + // return the value provided through query parameters + return value.orElse(context.request().getParam(paramName)); + } + +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/AuthorizationRequestFactory.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/AuthorizationRequestFactory.java index e0976ca92e..ea00e2461c 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/AuthorizationRequestFactory.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/request/AuthorizationRequestFactory.java @@ -39,6 +39,7 @@ import java.util.stream.Stream; import static io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest.CONTEXT_PATH; +import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.getOAuthParameter; /** * @author David BRASSELY (david.brassely at graviteesource.com) @@ -79,17 +80,17 @@ public AuthorizationRequest create(RoutingContext context) { // set OAuth 2.0 information authorizationRequest.setClientId(request.params().get(Parameters.CLIENT_ID)); - authorizationRequest.setResponseType(request.params().get(Parameters.RESPONSE_TYPE)); - authorizationRequest.setRedirectUri(request.params().get(Parameters.REDIRECT_URI)); - String scope = request.params().get(Parameters.SCOPE); + authorizationRequest.setResponseType(getOAuthParameter(context, Parameters.RESPONSE_TYPE)); + authorizationRequest.setRedirectUri(getOAuthParameter(context, Parameters.REDIRECT_URI)); + String scope = getOAuthParameter(context, Parameters.SCOPE); authorizationRequest.setScopes(scope != null && !scope.isEmpty() ? new HashSet<>(Arrays.asList(scope.split("\\s+"))) : null); - authorizationRequest.setState(request.params().get(Parameters.STATE)); - authorizationRequest.setResponseMode(request.params().get(Parameters.RESPONSE_MODE)); + authorizationRequest.setState(getOAuthParameter(context, Parameters.STATE)); + authorizationRequest.setResponseMode(getOAuthParameter(context, Parameters.RESPONSE_MODE)); authorizationRequest.setAdditionalParameters(extractAdditionalParameters(request)); authorizationRequest.setApproved(Boolean.TRUE.equals(context.session().get(ConstantKeys.USER_CONSENT_APPROVED_KEY))); // set OIDC information - String prompt = request.params().get(io.gravitee.am.common.oidc.Parameters.PROMPT); + String prompt = getOAuthParameter(context, io.gravitee.am.common.oidc.Parameters.PROMPT); authorizationRequest.setPrompts(prompt != null ? new HashSet<>(Arrays.asList(prompt.split("\\s+"))) : Collections.emptySet()); context.put(ConstantKeys.AUTHORIZATION_REQUEST_CONTEXT_KEY, authorizationRequest); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java index 362496aa7f..8e63d848e3 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestService.java @@ -19,6 +19,7 @@ import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.model.oidc.Client; import io.gravitee.am.repository.oauth2.model.PushedAuthorizationRequest; +import io.reactivex.Completable; import io.reactivex.Single; /** @@ -48,4 +49,12 @@ public interface PushedAuthorizationRequestService { * @return */ Single registerParameters(PushedAuthorizationRequest par, Client client); + + /** + * Delete the PushedAuthorizationRequest entry from the repository + * + * @param uriIdentifier + * @return + */ + Completable deleteRequestUri(String uriIdentifier); } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java index d7c14dfbe2..3dae43f9ef 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java @@ -43,6 +43,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; import java.text.ParseException; import java.time.Instant; @@ -84,8 +85,6 @@ public Single readFromURI(String requestUri, Client client, OpenIDProviderM return parRepository.findById(identifier) .switchIfEmpty(Single.error(new InvalidRequestUriException())) - // request_uri is a one shot use - .flatMap(par -> parRepository.delete(identifier).andThen(Single.just(par))) .flatMap((Function>) req -> { if (req.getParameters() != null && req.getExpireAt() != null && @@ -211,4 +210,14 @@ public SingleSource apply(JWK jwk) throws Exception { } }); } + + @Override + public Completable deleteRequestUri(String uriIdentifier) { + LOGGER.debug("Delete Pushed Authorization Request with id '{}'", uriIdentifier); + if (StringUtils.isEmpty(uriIdentifier)) { + // if the identifier is null or empty, return successful operation. + return Completable.complete(); + } + return parRepository.delete(uriIdentifier); + } } \ No newline at end of file diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java index db15be1ac6..641136b5e0 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java @@ -25,6 +25,7 @@ import io.gravitee.am.gateway.handler.common.vertx.RxWebTestBase; import io.gravitee.am.gateway.handler.oauth2.resources.endpoint.authorization.AuthorizationEndpoint; import io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.*; +import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; import io.gravitee.am.gateway.handler.oauth2.service.request.AuthorizationRequest; import io.gravitee.am.gateway.handler.oauth2.service.response.*; import io.gravitee.am.gateway.handler.oauth2.service.response.jwt.JWTAuthorizationCodeResponse; @@ -38,6 +39,7 @@ import io.gravitee.am.model.oidc.Client; import io.gravitee.common.http.HttpHeaders; import io.gravitee.common.http.HttpStatusCode; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Single; import io.vertx.core.Handler; @@ -92,11 +94,14 @@ public class AuthorizationEndpointTest extends RxWebTestBase { @Mock private ThymeleafTemplateEngine thymeleafTemplateEngine; + @Mock + private PushedAuthorizationRequestService parService; + @Override public void setUp() throws Exception { super.setUp(); - AuthorizationEndpoint authorizationEndpointHandler = new AuthorizationEndpoint(flow, thymeleafTemplateEngine); + AuthorizationEndpoint authorizationEndpointHandler = new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService); // set openid provider service OpenIDProviderMetadata openIDProviderMetadata = new OpenIDProviderMetadata(); @@ -135,6 +140,8 @@ public void setUp() throws Exception { routingContext.put(CONTEXT_PATH, "/test"); routingContext.next(); }); + + when(parService.deleteRequestUri(any())).thenReturn(Completable.complete()); } @Test diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java index b39e651992..89d2a06863 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java @@ -31,6 +31,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.Arrays; import java.util.Collections; import static org.mockito.Mockito.when; @@ -78,6 +79,7 @@ public void shouldRejectRequest_unsupportedAcrValues() throws Exception { public void shouldAcceptRequest_supportedAcrValues() throws Exception { OpenIDProviderMetadata openIDProviderMetadata = new OpenIDProviderMetadata(); openIDProviderMetadata.setAcrValuesSupported(Collections.singletonList(AcrValues.IN_COMMON_SILVER)); + openIDProviderMetadata.setResponseTypesSupported(Arrays.asList(ResponseType.CODE)); Client client = new Client(); client.setAuthorizedGrantTypes(Collections.singletonList(GrantType.AUTHORIZATION_CODE)); client.setResponseTypes(Collections.singletonList(ResponseType.CODE)); From 2efa5163a35e24e3aa118b238ef8bcb6e79fc4e9 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 9 Aug 2021 18:22:02 +0200 Subject: [PATCH 15/24] fix: check PKCE is used for FAPI using PAR authentication request fixes gravitee-io/issues#5973 --- .../handler/common/utils/ConstantKeys.java | 2 ++ .../gateway/handler/oauth2/OAuth2Provider.java | 2 +- .../authorization/AuthorizationEndpoint.java | 5 ++++- ...thorizationRequestParseParametersHandler.java | 16 +++++++++------- ...rizationRequestParseRequestObjectHandler.java | 7 ++++--- .../resources/request/TokenRequestFactory.java | 1 + .../code/AuthorizationCodeTokenGranter.java | 5 +++-- .../oauth2/service/request/TokenRequest.java | 1 - .../endpoint/AuthorizationEndpointTest.java | 2 +- .../oauth2/api/model/JdbcAuthorizationCode.java | 1 + .../liquibase/changelogs/v3_11_0/schema-par.yml | 2 +- .../api/AuthorizationCodeRepositoryTest.java | 3 ++- 12 files changed, 29 insertions(+), 18 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java index 0f63327346..34312c983d 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/utils/ConstantKeys.java @@ -99,4 +99,6 @@ public interface ConstantKeys { String REQUEST_OBJECT_KEY = "requestObject"; // identifier of the Pushed Authorization Parameters String REQUEST_URI_ID_KEY = "requestUriId"; + String REQUEST_OBJECT_FROM_URI = "request-object-from-uri"; + } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index 50efd133a1..7100f2038a 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -213,7 +213,7 @@ private void initRouter() { .handler(authenticationFlowHandler.create()) .handler(new AuthorizationRequestResolveHandler()) .handler(new AuthorizationRequestEndUserConsentHandler(userConsentService)) - .handler(new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService)) + .handler(new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService, domain)) .failureHandler(new AuthorizationRequestFailureHandler(openIDDiscoveryService, jwtService, jweService)); // Authorization consent endpoint diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java index f68284c582..a2b4c1c6d0 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java @@ -24,6 +24,7 @@ import io.gravitee.am.gateway.handler.oauth2.service.request.AuthorizationRequest; import io.gravitee.am.gateway.handler.oauth2.service.response.AuthorizationResponse; import io.gravitee.am.gateway.handler.oidc.service.flow.Flow; +import io.gravitee.am.model.Domain; import io.gravitee.am.model.oidc.Client; import io.gravitee.common.http.HttpHeaders; import io.gravitee.common.http.MediaType; @@ -55,10 +56,12 @@ public class AuthorizationEndpoint implements Handler { private final Flow flow; private final ThymeleafTemplateEngine engine; private final PushedAuthorizationRequestService parService; + private final Domain domain; - public AuthorizationEndpoint(Flow flow, ThymeleafTemplateEngine engine, PushedAuthorizationRequestService parService) { + public AuthorizationEndpoint(Flow flow, ThymeleafTemplateEngine engine, PushedAuthorizationRequestService parService, Domain domain) { this.flow = flow; this.engine = engine; + this.domain = domain; this.parService = parService; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java index 17e84bc07e..564b005378 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java @@ -46,12 +46,10 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; +import java.util.*; import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.PROVIDER_METADATA_CONTEXT_KEY; +import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.REQUEST_OBJECT_FROM_URI; import static io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest.CONTEXT_PATH; import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.getOAuthParameter; import static io.gravitee.am.service.utils.ResponseTypeUtils.requireNonce; @@ -145,7 +143,8 @@ private void parsePKCEParameter(RoutingContext context, Client client) { throw new InvalidRequestException("Missing parameter: code_challenge"); } - if (codeChallenge == null && client.isForcePKCE()) { + final boolean pkceRequiredByFapi = this.domain.usePlainFapiProfile() && Optional.ofNullable((Boolean)context.get(REQUEST_OBJECT_FROM_URI)).orElse(Boolean.FALSE); + if (codeChallenge == null && (client.isForcePKCE() || pkceRequiredByFapi)) { throw new InvalidRequestException("Missing parameter: code_challenge"); } @@ -153,8 +152,11 @@ private void parsePKCEParameter(RoutingContext context, Client client) { if (codeChallengeMethod != null) { // https://tools.ietf.org/html/rfc7636#section-4.2 // It must be plain or S256 - if (!CodeChallengeMethod.S256.equalsIgnoreCase(codeChallengeMethod) && - !CodeChallengeMethod.PLAIN.equalsIgnoreCase(codeChallengeMethod)) { + // For FAPI, only S256 is allowed for PKCE + // https://openid.net/specs/openid-financial-api-part-2-1_0.html#authorization-server (point 18) + if ((this.domain.usePlainFapiProfile() && !CodeChallengeMethod.S256.equalsIgnoreCase(codeChallengeMethod)) || + (!CodeChallengeMethod.S256.equalsIgnoreCase(codeChallengeMethod) && + !CodeChallengeMethod.PLAIN.equalsIgnoreCase(codeChallengeMethod))) { throw new InvalidRequestException("Invalid parameter: code_challenge_method"); } } else { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index 28904f83b5..99e0194bde 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -52,8 +52,7 @@ * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Team */ -public class AuthorizationRequestParseRequestObjectHandler implements Handler { - private static final String REQUEST_OBJECT_FROM_URI = "ro-from-uri"; +public class AuthorizationRequestParseRequestObjectHandler extends AbstractAuthorizationRequestHandler implements Handler { private static final String HTTPS_SCHEME = "https"; // When the request parameter is used, the OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax. @@ -67,7 +66,9 @@ public class AuthorizationRequestParseRequestObjectHandler implements Handler(Arrays.asList(scope.split("\\s+"))) : null); tokenRequest.setAdditionalParameters(extractAdditionalParameters(request)); + return tokenRequest; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/code/AuthorizationCodeTokenGranter.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/code/AuthorizationCodeTokenGranter.java index 0618917f34..e3c4ab2e9d 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/code/AuthorizationCodeTokenGranter.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/granter/code/AuthorizationCodeTokenGranter.java @@ -29,8 +29,9 @@ import io.gravitee.am.gateway.handler.oauth2.service.request.TokenRequestResolver; import io.gravitee.am.gateway.handler.oauth2.service.token.TokenService; import io.gravitee.am.model.AuthenticationFlowContext; -import io.gravitee.am.model.oidc.Client; +import io.gravitee.am.model.Domain; import io.gravitee.am.model.User; +import io.gravitee.am.model.oidc.Client; import io.gravitee.am.repository.oauth2.model.AuthorizationCode; import io.gravitee.am.service.AuthenticationFlowContextService; import io.gravitee.common.util.MultiValueMap; @@ -92,7 +93,7 @@ protected Single parseRequest(TokenRequest tokenRequest, Client cl .onErrorResumeNext(error -> (exitOnError) ? Maybe.error(error) : Maybe.just(new AuthenticationFlowContext())) .map(ctx -> { checkRedirectUris(tokenRequest1, authorizationCode); - checkPKCE(tokenRequest1, authorizationCode); + checkPKCE( tokenRequest1, authorizationCode); // set resource owner tokenRequest1.setSubject(authorizationCode.getSubject()); // set original scopes diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java index fcbb08b754..0ee9c971ca 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/request/TokenRequest.java @@ -65,7 +65,6 @@ public class TokenRequest extends OAuth2Request { */ private String requestingPartyToken; - public String getUsername() { return username; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java index 641136b5e0..2b1cf3cd22 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java @@ -101,7 +101,7 @@ public class AuthorizationEndpointTest extends RxWebTestBase { public void setUp() throws Exception { super.setUp(); - AuthorizationEndpoint authorizationEndpointHandler = new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService); + AuthorizationEndpoint authorizationEndpointHandler = new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService, domain); // set openid provider service OpenIDProviderMetadata openIDProviderMetadata = new OpenIDProviderMetadata(); diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcAuthorizationCode.java b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcAuthorizationCode.java index 7e5ba7fb7f..30c185df90 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcAuthorizationCode.java +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/java/io/gravitee/am/repository/jdbc/oauth2/api/model/JdbcAuthorizationCode.java @@ -135,4 +135,5 @@ public String getRequestParameters() { public void setRequestParameters(String requestParameters) { this.requestParameters = requestParameters; } + } diff --git a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml index 462a6fc0d9..f660567767 100644 --- a/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml +++ b/gravitee-am-repository/gravitee-am-repository-jdbc/src/main/resources/liquibase/changelogs/v3_11_0/schema-par.yml @@ -1,6 +1,6 @@ databaseChangeLog: - changeSet: - id: 3.11.0 + id: 3.11.0-PAR-Table author: GraviteeSource Team changes: diff --git a/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/AuthorizationCodeRepositoryTest.java b/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/AuthorizationCodeRepositoryTest.java index 9e5f14d231..58ec2025f2 100644 --- a/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/AuthorizationCodeRepositoryTest.java +++ b/gravitee-am-repository/gravitee-am-repository-tests/src/test/java/io/gravitee/am/repository/oauth2/api/AuthorizationCodeRepositoryTest.java @@ -44,7 +44,8 @@ public void shouldStoreCode() { testObserver.assertComplete(); testObserver.assertNoErrors(); - testObserver.assertValue(authorizationCode1 -> authorizationCode1.getCode().equals(code) && authorizationCode1.getContextVersion() == 1); + testObserver.assertValue(authorizationCode1 -> authorizationCode1.getCode().equals(code) + && authorizationCode1.getContextVersion() == 1); } @Test From 98d0866a305941e2731cb43b1f4fb0902806b398 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 9 Aug 2021 18:58:25 +0200 Subject: [PATCH 16/24] fix: add control on redirect_uri during Pushed Authrorization Request registration fixes gravitee-io/issues#5969 --- .../PushedAuthorizationRequestEndpoint.java | 10 ++++ ...rizationRequestParseParametersHandler.java | 34 +---------- .../handler/authorization/ParamUtils.java | 28 +++++++++ ...PushedAuthorizationRequestServiceImpl.java | 60 +++++++++++++++++-- 4 files changed, 96 insertions(+), 36 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java index e223e99ca3..3757bfcbb6 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/par/PushedAuthorizationRequestEndpoint.java @@ -17,7 +17,10 @@ import io.gravitee.am.common.exception.oauth2.InvalidRequestException; import io.gravitee.am.common.exception.oauth2.MethodNotAllowedException; +import io.gravitee.am.common.oauth2.GrantType; +import io.gravitee.am.common.web.UriBuilder; import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException; +import io.gravitee.am.gateway.handler.oauth2.exception.RedirectMismatchException; import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; import io.gravitee.am.identityprovider.common.oauth2.utils.URLEncodedUtils; import io.gravitee.am.model.oidc.Client; @@ -34,7 +37,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; + import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY; +import static io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest.CONTEXT_PATH; +import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.getOAuthParameter; /** * @author Eric LELEU (eric.leleu at graviteesource.com) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java index 564b005378..1141e7bdc6 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java @@ -43,15 +43,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.MalformedURLException; import java.net.URISyntaxException; -import java.net.URL; import java.util.*; import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.PROVIDER_METADATA_CONTEXT_KEY; import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.REQUEST_OBJECT_FROM_URI; import static io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest.CONTEXT_PATH; import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.getOAuthParameter; +import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.redirectMatches; import static io.gravitee.am.service.utils.ResponseTypeUtils.requireNonce; /** @@ -348,37 +347,8 @@ private boolean containsGrantType(List authorizedGrantTypes) { private void checkMatchingRedirectUri(String requestedRedirect, List registeredClientRedirectUris) { if (registeredClientRedirectUris .stream() - .noneMatch(registeredClientUri -> redirectMatches(requestedRedirect, registeredClientUri))) { + .noneMatch(registeredClientUri -> redirectMatches(requestedRedirect, registeredClientUri, this.domain.isRedirectUriStrictMatching() || this.domain.usePlainFapiProfile()))) { throw new RedirectMismatchException("The redirect_uri MUST match the registered callback URL for this application"); } } - - private boolean redirectMatches(String requestedRedirect, String registeredClientUri) { - // if redirect_uri strict matching mode is enabled, do string matching - // FAPI also requires strict matching - if (this.domain.isRedirectUriStrictMatching() || this.domain.usePlainFapiProfile()) { - return requestedRedirect.equals(registeredClientUri); - } - - // nominal case - try { - URL req = new URL(requestedRedirect); - URL reg = new URL(registeredClientUri); - - int requestedPort = req.getPort() != -1 ? req.getPort() : req.getDefaultPort(); - int registeredPort = reg.getPort() != -1 ? reg.getPort() : reg.getDefaultPort(); - - boolean portsMatch = registeredPort == requestedPort; - - if (reg.getProtocol().equals(req.getProtocol()) && - reg.getHost().equals(req.getHost()) && - portsMatch) { - return req.getPath().startsWith(reg.getPath()); - } - } catch (MalformedURLException e) { - - } - - return requestedRedirect.equals(registeredClientUri); - } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java index f804a57622..d77642c88a 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/ParamUtils.java @@ -23,6 +23,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.MalformedURLException; +import java.net.URL; import java.text.ParseException; import java.util.Optional; @@ -57,4 +59,30 @@ public static String getOAuthParameter(RoutingContext context, String paramName) return value.orElse(context.request().getParam(paramName)); } + public static boolean redirectMatches(String requestedRedirect, String registeredClientUri, boolean uriStrictMatch) { + if (uriStrictMatch) { + return requestedRedirect.equals(registeredClientUri); + } + + // nominal case + try { + URL req = new URL(requestedRedirect); + URL reg = new URL(registeredClientUri); + + int requestedPort = req.getPort() != -1 ? req.getPort() : req.getDefaultPort(); + int registeredPort = reg.getPort() != -1 ? reg.getPort() : reg.getDefaultPort(); + + boolean portsMatch = registeredPort == requestedPort; + + if (reg.getProtocol().equals(req.getProtocol()) && + reg.getHost().equals(req.getHost()) && + portsMatch) { + return req.getPath().startsWith(reg.getPath()); + } + } catch (MalformedURLException e) { + + } + + return requestedRedirect.equals(registeredClientUri); + } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java index 3dae43f9ef..39fce253f3 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java @@ -48,6 +48,9 @@ import java.text.ParseException; import java.time.Instant; import java.util.Date; +import java.util.List; + +import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.redirectMatches; /** * @author Eric LELEU (eric.leleu at graviteesource.com) @@ -130,9 +133,13 @@ public Single registerParameters(PushedAutho final String request = par.getParameters().getFirst(io.gravitee.am.common.oidc.Parameters.REQUEST); if (request != null) { - registrationValidation = registrationValidation.andThen(Single.defer(() -> - readRequestObject(client, request))) + registrationValidation = registrationValidation + .andThen(Single.defer(() -> + readRequestObject(client, request) + .map(jwt -> checkRedirectUriParameter(jwt, client)))) .ignoreElement(); + } else { + registrationValidation.andThen(Completable.fromAction(() -> checkRedirectUriParameter(par, client))); } return registrationValidation.andThen(Single.defer(() -> parRepository.create(par))).map(parPersisted -> { @@ -161,12 +168,10 @@ private Single readRequestObject(Client client, String request) { private JWT checkRequestObjectClaims(JWT jwt) { try { - if (jwt.getJWTClaimsSet().getStringClaim(io.gravitee.am.common.oidc.Parameters.REQUEST) != null || jwt.getJWTClaimsSet().getStringClaim(io.gravitee.am.common.oidc.Parameters.REQUEST_URI) != null) { throw new InvalidRequestObjectException("Claims request and request_uri are forbidden"); } - return jwt; } catch (ParseException e) { LOGGER.warn("request object received in PAR request is malformed: {}", e.getMessage()); @@ -211,6 +216,53 @@ public SingleSource apply(JWK jwk) throws Exception { }); } + private PushedAuthorizationRequest checkRedirectUriParameter(PushedAuthorizationRequest request, Client client) { + checkRedirectUri(client, request.getParameters().getFirst(Parameters.REDIRECT_URI)); + return request; + } + + private JWT checkRedirectUriParameter(JWT request, Client client) { + try { + String requestedRedirectUri = request.getJWTClaimsSet().getStringClaim(Parameters.REDIRECT_URI); + checkRedirectUri(client, requestedRedirectUri); + } catch (ParseException e) { + throw new InvalidRequestException("request object is malformed"); + } + return request; + } + + private void checkRedirectUri(Client client, String requestedRedirectUri) { + final List registeredClientRedirectUris = client.getRedirectUris(); + final boolean hasRegisteredClientRedirectUris = registeredClientRedirectUris != null && !registeredClientRedirectUris.isEmpty(); + final boolean hasRequestedRedirectUri = requestedRedirectUri != null && !requestedRedirectUri.isEmpty(); + + // if no requested redirect_uri and no registered client redirect_uris + // throw invalid request exception + if (!hasRegisteredClientRedirectUris && !hasRequestedRedirectUri) { + throw new InvalidRequestException("A redirect_uri must be supplied"); + } + + // if no requested redirect_uri and more than one registered client redirect_uris + // throw invalid request exception + if (!hasRequestedRedirectUri && (registeredClientRedirectUris != null && registeredClientRedirectUris.size() > 1)) { + throw new InvalidRequestException("Unable to find suitable redirect_uri, a redirect_uri must be supplied"); + } + + // if requested redirect_uri doesn't match registered client redirect_uris + // throw redirect mismatch exception + if (hasRequestedRedirectUri && hasRegisteredClientRedirectUris) { + checkMatchingRedirectUri(requestedRedirectUri, registeredClientRedirectUris); + } + } + + private void checkMatchingRedirectUri(String requestedRedirect, List registeredClientRedirectUris) { + if (registeredClientRedirectUris + .stream() + .noneMatch(registeredClientUri -> redirectMatches(requestedRedirect, registeredClientUri, this.domain.isRedirectUriStrictMatching() || this.domain.usePlainFapiProfile()))) { + throw new InvalidRequestObjectException("The redirect_uri MUST match the registered callback URL for this application"); + } + } + @Override public Completable deleteRequestUri(String uriIdentifier) { LOGGER.debug("Delete Pushed Authorization Request with id '{}'", uriIdentifier); From f9657c8cb339b6dc4512d2b705594a4389c32469 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 10 Aug 2021 10:57:06 +0200 Subject: [PATCH 17/24] fix: use authorization.code.validity setting to limit the validity of the JARM response returned by the authorization endpoint fixes gravitee-io/issues#5968 --- .../am/gateway/handler/oauth2/OAuth2Provider.java | 2 +- .../AuthorizationRequestFailureHandler.java | 12 +++++++++--- .../handler/oidc/service/flow/AbstractFlow.java | 9 +++++++-- .../handler/oidc/service/flow/CompositeFlow.java | 1 - .../endpoint/AuthorizationEndpointTest.java | 7 ++++++- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index 7100f2038a..14ffa9b6e7 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -214,7 +214,7 @@ private void initRouter() { .handler(new AuthorizationRequestResolveHandler()) .handler(new AuthorizationRequestEndUserConsentHandler(userConsentService)) .handler(new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService, domain)) - .failureHandler(new AuthorizationRequestFailureHandler(openIDDiscoveryService, jwtService, jweService)); + .failureHandler(new AuthorizationRequestFailureHandler(openIDDiscoveryService, jwtService, jweService, environment)); // Authorization consent endpoint Handler userConsentPrepareContextHandler = new UserConsentPrepareContextHandler(clientSyncService); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java index 126c62050e..12be7a7076 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java @@ -37,6 +37,7 @@ import io.vertx.reactivex.ext.web.RoutingContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; import java.net.URI; import java.net.URISyntaxException; @@ -71,13 +72,18 @@ public class AuthorizationRequestFailureHandler implements Handler responseTypes) { Objects.requireNonNull(responseTypes); @@ -70,8 +72,9 @@ private Single processResponse(AuthorizationResponse auth JWTAuthorizationResponse jwtAuthorizationResponse = JWTAuthorizationResponse.from(authorizationResponse); jwtAuthorizationResponse.setIss(openIDDiscoveryService.getIssuer(authorizationRequest.getOrigin())); jwtAuthorizationResponse.setAud(client.getClientId()); - // There is nothing about expiration. We admit to use the one settled for IdToken validity - jwtAuthorizationResponse.setExp(Instant.now().plusSeconds(client.getIdTokenValiditySeconds()).getEpochSecond()); + // JWT contains Authorization code, this JWT duration should be short + // Because the code is persisted, we align the JWT duration with it + jwtAuthorizationResponse.setExp(Instant.now().plusSeconds(codeValidityInSec).getEpochSecond()); // Sign if needed, else return unsigned JWT return jwtService.encodeAuthorization(jwtAuthorizationResponse.build(), client) @@ -93,5 +96,7 @@ void afterPropertiesSet() { this.openIDDiscoveryService = applicationContext.getBean(OpenIDDiscoveryService.class); this.jwtService = applicationContext.getBean(JWTService.class); this.jweService = applicationContext.getBean(JWEService.class); + final Environment environment = applicationContext.getEnvironment(); + this.codeValidityInSec = environment.getProperty("authorization.code.validity", Integer.class, 60000) / 1000; } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/flow/CompositeFlow.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/flow/CompositeFlow.java index 5ea6488549..681a9f69a5 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/flow/CompositeFlow.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/flow/CompositeFlow.java @@ -24,7 +24,6 @@ import io.gravitee.am.gateway.handler.oidc.service.flow.hybrid.HybridFlow; import io.gravitee.am.gateway.handler.oidc.service.flow.implicit.ImplicitFlow; import io.gravitee.am.gateway.handler.oidc.service.idtoken.IDTokenService; -import io.gravitee.am.model.AuthenticationFlowContext; import io.gravitee.am.model.User; import io.gravitee.am.model.oidc.Client; import io.reactivex.Observable; diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java index 2b1cf3cd22..543e7dcc9f 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java @@ -55,6 +55,7 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import org.springframework.core.env.Environment; import java.util.Arrays; import java.util.Collections; @@ -97,6 +98,9 @@ public class AuthorizationEndpointTest extends RxWebTestBase { @Mock private PushedAuthorizationRequestService parService; + @Mock + private Environment environment; + @Override public void setUp() throws Exception { super.setUp(); @@ -121,6 +125,7 @@ public void setUp() throws Exception { ResponseMode.JWT)); when(openIDDiscoveryService.getConfiguration(anyString())).thenReturn(openIDProviderMetadata); + when(environment.getProperty("authorization.code.validity", Integer.class, 60000)).thenReturn(60000); // set Authorization endpoint routes SessionHandler sessionHandler = SessionHandler.create(LocalSessionStore.create(vertx)); @@ -134,7 +139,7 @@ public void setUp() throws Exception { .handler(new AuthorizationRequestResolveHandler()) .handler(authorizationEndpointHandler); router.route() - .failureHandler(new AuthorizationRequestFailureHandler(openIDDiscoveryService, jwtService, jweService)); + .failureHandler(new AuthorizationRequestFailureHandler(openIDDiscoveryService, jwtService, jweService, environment)); router.route().order(-1).handler(routingContext -> { routingContext.put(CONTEXT_PATH, "/test"); From 280744678dbb407e3db73acd006f56d331c1115a Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 10 Aug 2021 11:42:21 +0200 Subject: [PATCH 18/24] fix: use error details coming from the response JWT (JARM) to display the error page fixes gravitee-io/issues#5976 --- .../vertx/web/endpoint/ErrorEndpoint.java | 40 ++++++++++++++++--- .../am/gateway/handler/root/RootProvider.java | 2 +- .../handler/oauth2/OAuth2Provider.java | 2 +- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/endpoint/ErrorEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/endpoint/ErrorEndpoint.java index b7b6e4b2a6..58012a5e0d 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/endpoint/ErrorEndpoint.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/vertx/web/endpoint/ErrorEndpoint.java @@ -15,13 +15,18 @@ */ package io.gravitee.am.gateway.handler.common.vertx.web.endpoint; +import io.gravitee.am.common.jwt.JWT; import io.gravitee.am.common.oauth2.Parameters; import io.gravitee.am.gateway.handler.common.client.ClientSyncService; +import io.gravitee.am.gateway.handler.common.jwt.JWTService; +import io.gravitee.am.gateway.handler.common.jwt.impl.JWTServiceImpl; import io.gravitee.am.model.oidc.Client; import io.gravitee.am.model.Template; import io.gravitee.am.service.exception.ClientNotFoundException; import io.gravitee.common.http.HttpHeaders; import io.gravitee.common.http.MediaType; +import io.reactivex.Single; +import io.reactivex.functions.Consumer; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; @@ -49,11 +54,13 @@ public class ErrorEndpoint implements Handler { private String domain; private ThymeleafTemplateEngine engine; private ClientSyncService clientSyncService; + private JWTService jwtService; - public ErrorEndpoint(String domain, ThymeleafTemplateEngine engine, ClientSyncService clientSyncService) { + public ErrorEndpoint(String domain, ThymeleafTemplateEngine engine, ClientSyncService clientSyncService, JWTService jwtService) { this.domain = domain; this.engine = engine; this.clientSyncService = clientSyncService; + this.jwtService = jwtService; } @Override @@ -92,13 +99,34 @@ private void renderErrorPage(RoutingContext routingContext, Client client) { // unable to decode UTF-8 encoded query parameter } } - routingContext.put(ERROR_PARAM, error); - routingContext.put(ERROR_DESCRIPTION_PARAM, errorDescription); + final Map errorParams = new HashMap<>(); + errorParams.put(ERROR_PARAM, error); + errorParams.put(ERROR_DESCRIPTION_PARAM, errorDescription); + + Single> singlePageRendering = Single.just(errorParams); + + final String jarm = request.getParam(io.gravitee.am.common.oidc.Parameters.RESPONSE); + if (error == null && jarm != null) { + // extract error details from the JWT provided as response parameter + singlePageRendering = this.jwtService.decode(jarm).map(jwt -> { + Map result = new HashMap<>(); + result.put(ERROR_PARAM, (String) jwt.get(ERROR_PARAM)); + result.put(ERROR_DESCRIPTION_PARAM, (String) jwt.get(ERROR_DESCRIPTION_PARAM)); + return result; + }); + } + + singlePageRendering.subscribe( + params -> render(routingContext, client, params), + // single contains an error due to JWT decoding, return the default error page without error details + (exception) ->render(routingContext, client, errorParams)); + + } + + private void render(RoutingContext routingContext, Client client, Map params) { + params.forEach((k, v) -> routingContext.put(k, v)); // put parameters in context (backward compatibility) - Map params = new HashMap<>(); - params.put(ERROR_PARAM, error); - params.put(ERROR_DESCRIPTION_PARAM, errorDescription); routingContext.put(PARAM_CONTEXT_KEY, params); engine.render(routingContext.data(), getTemplateFileName(client), res -> { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java index 713501d2c0..c3859967d1 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java @@ -354,7 +354,7 @@ protected void doStart() throws Exception { // error route rootRouter.route(HttpMethod.GET, PATH_ERROR) - .handler(new ErrorEndpoint(domain.getId(), thymeleafTemplateEngine, clientSyncService)); + .handler(new ErrorEndpoint(domain.getId(), thymeleafTemplateEngine, clientSyncService, jwtService)); // error handler errorHandler(rootRouter); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index 14ffa9b6e7..7a25ff1961 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -262,7 +262,7 @@ private void initRouter() { // Error endpoint oauth2Router.route(HttpMethod.GET, "/error") - .handler(new ErrorEndpoint(domain.getId(), thymeleafTemplateEngine, clientSyncService)); + .handler(new ErrorEndpoint(domain.getId(), thymeleafTemplateEngine, clientSyncService, jwtService)); // Pushed Authorization Request oauth2Router.route(HttpMethod.POST,"/par") From c802de6ac6a7682b2c81fe7707484fb3b222a355 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 10 Aug 2021 17:31:39 +0200 Subject: [PATCH 19/24] fix: when request object can't be decoded, redirect to the default error page fixes gravitee-io/issues#5967 --- .../AuthorizationRequestFailureHandler.java | 6 ++++++ ...zationRequestParseRequestObjectHandler.java | 18 ++++++++++++++++-- .../PushedAuthorizationRequestServiceImpl.java | 7 ++++--- .../request/impl/RequestObjectServiceImpl.java | 7 ++++--- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java index 12be7a7076..8d92b34270 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java @@ -15,6 +15,7 @@ */ package io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization; +import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; import io.gravitee.am.common.exception.oauth2.OAuth2Exception; import io.gravitee.am.common.oauth2.Parameters; import io.gravitee.am.common.web.UriBuilder; @@ -150,6 +151,11 @@ private void processOAuth2Exception(AuthorizationRequest authorizationRequest, if (oAuth2Exception instanceof RedirectMismatchException) { authorizationRequest.setRedirectUri(defaultErrorURL); } + // InvalidRequestObjectException without the RequestObject present into the Context means that the JWT can't be decoded + // return to the default error page to avoid redirect using wrong response mode + if (oAuth2Exception instanceof InvalidRequestObjectException && context.get(ConstantKeys.REQUEST_OBJECT_KEY) == null) { + authorizationRequest.setRedirectUri(defaultErrorURL); + } // Process error response try { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java index 99e0194bde..22894c6dd8 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseRequestObjectHandler.java @@ -129,11 +129,9 @@ public void handle(RoutingContext context) { .subscribe( jwt -> { try { - context.put(REQUEST_OBJECT_KEY, jwt); // Check OAuth2 parameters checkOAuthParameters(context, jwt); overrideRequestParameters(context, jwt); - context.put(REQUEST_OBJECT_KEY, jwt); context.next(); } catch (Exception ex) { context.fail(ex); @@ -185,6 +183,7 @@ private Maybe handleRequestObjectValue(RoutingContext context) { return requestObjectService .readRequestObject(request, context.get(CLIENT_CONTEXT_KEY)) + .map(jwt -> preserveRequestObject(context, jwt)) .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) .toMaybe(); } else { @@ -192,6 +191,19 @@ private Maybe handleRequestObjectValue(RoutingContext context) { } } + /** + * Keep the requestObject JWT into the RoutingContext for later use. + * This is useful to retrieve parameter either from the JWT or from the request params + * + * @param context + * @param jwt + * @return + */ + private JWT preserveRequestObject(RoutingContext context, JWT jwt) { + context.put(REQUEST_OBJECT_KEY, jwt); + return jwt; + } + private Single validateRequestObjectClaims(RoutingContext context, JWT jwt) { if (this.domain.usePlainFapiProfile()) { try { @@ -280,6 +292,7 @@ private Maybe handleRequestObjectURI(RoutingContext context) { if (requestUri.startsWith(PushedAuthorizationRequestService.PAR_URN_PREFIX)) { return parService.readFromURI(requestUri, context.get(CLIENT_CONTEXT_KEY), context.get(PROVIDER_METADATA_CONTEXT_KEY)) + .map(jwt -> preserveRequestObject(context, jwt)) .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) .map(jwt -> { final String uriIdentifier = requestUri.substring(PushedAuthorizationRequestService.PAR_URN_PREFIX.length()); @@ -290,6 +303,7 @@ private Maybe handleRequestObjectURI(RoutingContext context) { } else { return requestObjectService .readRequestObjectFromURI(requestUri, context.get(CLIENT_CONTEXT_KEY)) + .map(jwt -> preserveRequestObject(context, jwt)) .flatMap(jwt -> validateRequestObjectClaims(context, jwt)) .toMaybe(); } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java index 39fce253f3..7a0fc63885 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java @@ -198,7 +198,7 @@ public MaybeSource apply(JWKSet jwkSet) throws Exception { return jwkService.getKey(jwkSet, jwt.getHeader().getKeyID()); } }) - .switchIfEmpty(Maybe.error(new InvalidRequestObjectException())) + .switchIfEmpty(Maybe.error(new InvalidRequestObjectException("Invalid key ID"))) .flatMapSingle(new Function>() { @Override public SingleSource apply(JWK jwk) throws Exception { @@ -206,8 +206,9 @@ public SingleSource apply(JWK jwk) throws Exception { // To perform Signature Validation, the alg Header Parameter in the // JOSE Header MUST match the value of the request_object_signing_alg // set during Client Registration - if (jwt.getHeader().getAlgorithm().getName().equals(client.getRequestObjectSigningAlg()) && - jwsService.isValidSignature(jwt, jwk)) { + if (!jwt.getHeader().getAlgorithm().getName().equals(client.getRequestObjectSigningAlg())) { + return Single.error(new InvalidRequestObjectException("Invalid request object signing algorithm")); + } else if (jwsService.isValidSignature(jwt, jwk)) { return Single.just(jwt); } else { return Single.error(new InvalidRequestObjectException("Invalid signature")); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java index 209f25dfee..cbc424b828 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java @@ -156,7 +156,7 @@ public MaybeSource apply(JWKSet jwkSet) throws Exception { return jwkService.getKey(jwkSet, jwt.getHeader().getKeyID()); } }) - .switchIfEmpty(Maybe.error(new InvalidRequestObjectException())) + .switchIfEmpty(Maybe.error(new InvalidRequestObjectException("Invalid key ID"))) .flatMapSingle(new Function>() { @Override public SingleSource apply(JWK jwk) throws Exception { @@ -164,8 +164,9 @@ public SingleSource apply(JWK jwk) throws Exception { // To perform Signature Validation, the alg Header Parameter in the // JOSE Header MUST match the value of the request_object_signing_alg // set during Client Registration - if (jwt.getHeader().getAlgorithm().getName().equals(client.getRequestObjectSigningAlg()) && - jwsService.isValidSignature(jwt, jwk)) { + if (!jwt.getHeader().getAlgorithm().getName().equals(client.getRequestObjectSigningAlg())) { + return Single.error(new InvalidRequestObjectException("Invalid request object signing algorithm")); + } else if (jwsService.isValidSignature(jwt, jwk)) { return Single.just(jwt); } else { return Single.error(new InvalidRequestObjectException("Invalid signature")); From e4159c696f2921b14a79b77a609787d8f6e9e448 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 11 Aug 2021 18:01:39 +0200 Subject: [PATCH 20/24] feat: manage tls_client_certificate_bound_access_tokens options during DCR fixes gravitee-io/issues#5985 --- .../handler/oauth2/OAuth2Provider.java | 2 +- .../auth/handler/ClientAuthHandler.java | 5 +-- .../handler/impl/ClientAuthHandlerImpl.java | 34 ++++++++++++++++++- .../ClientCertificateAuthProvider.java | 9 +---- .../ClientSelfSignedAuthProvider.java | 2 -- .../impl/IntrospectionServiceImpl.java | 7 +++- .../service/token/impl/TokenServiceImpl.java | 6 ++-- .../am/gateway/handler/oidc/OIDCProvider.java | 2 +- .../DynamicClientRegistrationRequest.java | 13 +++++++ .../impl/ClientServiceImpl.java | 1 + .../auth/handler/ClientAuthHandlerTest.java | 21 +++++++++++- .../ClientCertificateAuthProviderTest.java | 2 -- .../application/ApplicationOAuthSettings.java | 12 +++++++ .../io/gravitee/am/model/oidc/Client.java | 11 ++++++ .../MongoApplicationRepository.java | 2 ++ .../model/ApplicationOAuthSettingsMongo.java | 9 +++++ .../model/PatchApplicationOAuthSettings.java | 11 ++++++ 17 files changed, 128 insertions(+), 21 deletions(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index 7a25ff1961..ea692cced2 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -187,7 +187,7 @@ private void initRouter() { final Router oauth2Router = Router.router(vertx); // client auth handler - final Handler clientAuthHandler = ClientAuthHandler.create(clientSyncService, clientAssertionService, jwkService); + final Handler clientAuthHandler = ClientAuthHandler.create(clientSyncService, clientAssertionService, jwkService, domain); // static handler staticHandler(oauth2Router); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandler.java index ce6521746e..be723daad5 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandler.java @@ -20,6 +20,7 @@ import io.gravitee.am.gateway.handler.oauth2.resources.auth.provider.*; import io.gravitee.am.gateway.handler.oauth2.service.assertion.ClientAssertionService; import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; +import io.gravitee.am.model.Domain; import io.vertx.core.Handler; import io.vertx.reactivex.ext.web.RoutingContext; @@ -36,7 +37,7 @@ public interface ClientAuthHandler { String GENERIC_ERROR_MESSAGE = "Client authentication failed due to unknown or invalid client"; - static Handler create(ClientSyncService clientSyncService, ClientAssertionService clientAssertionService, JWKService jwkService) { + static Handler create(ClientSyncService clientSyncService, ClientAssertionService clientAssertionService, JWKService jwkService, Domain domain) { List clientAuthProviders = new ArrayList<>(); clientAuthProviders.add(new ClientBasicAuthProvider()); clientAuthProviders.add(new ClientPostAuthProvider()); @@ -44,6 +45,6 @@ static Handler create(ClientSyncService clientSyncService, Clien clientAuthProviders.add(new ClientCertificateAuthProvider()); clientAuthProviders.add(new ClientSelfSignedAuthProvider(jwkService)); clientAuthProviders.add(new ClientNoneAuthProvider()); - return new ClientAuthHandlerImpl(clientSyncService, clientAuthProviders); + return new ClientAuthHandlerImpl(clientSyncService, clientAuthProviders, domain); } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/impl/ClientAuthHandlerImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/impl/ClientAuthHandlerImpl.java index 006ba0f5f1..702690a195 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/impl/ClientAuthHandlerImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/impl/ClientAuthHandlerImpl.java @@ -17,9 +17,11 @@ import io.gravitee.am.common.oauth2.Parameters; import io.gravitee.am.gateway.handler.common.client.ClientSyncService; +import io.gravitee.am.gateway.handler.common.utils.ConstantKeys; import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException; import io.gravitee.am.gateway.handler.oauth2.resources.auth.handler.ClientAuthHandler; import io.gravitee.am.gateway.handler.oauth2.resources.auth.provider.ClientAuthProvider; +import io.gravitee.am.model.Domain; import io.gravitee.am.model.oidc.Client; import io.vertx.core.AsyncResult; import io.vertx.core.Future; @@ -27,11 +29,20 @@ import io.vertx.core.http.HttpHeaders; import io.vertx.reactivex.core.http.HttpServerRequest; import io.vertx.reactivex.ext.web.RoutingContext; +import org.bouncycastle.asn1.x509.GeneralName; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; import java.util.Base64; import java.util.List; import static io.gravitee.am.gateway.handler.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY; +import static io.gravitee.am.gateway.handler.oauth2.resources.auth.provider.CertificateUtils.getThumbprint; /** * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) @@ -40,10 +51,12 @@ public class ClientAuthHandlerImpl implements Handler { private final ClientSyncService clientSyncService; private final List clientAuthProviders; + private final Domain domain; - public ClientAuthHandlerImpl(ClientSyncService clientSyncService, List clientAuthProviders) { + public ClientAuthHandlerImpl(ClientSyncService clientSyncService, List clientAuthProviders, Domain domain) { this.clientSyncService = clientSyncService; this.clientAuthProviders = clientAuthProviders; + this.domain = domain; } @Override @@ -73,6 +86,25 @@ public void handle(RoutingContext routingContext) { // the client might has been upgraded after authentication process, get the new value Client authenticatedClient = authHandler.result(); + + // get SSL certificate thumbprint to bind with access token + try { + SSLSession sslSession = routingContext.request().sslSession(); + if (sslSession != null) { + Certificate[] peerCertificates = sslSession.getPeerCertificates(); + X509Certificate peerCertificate = (X509Certificate) peerCertificates[0]; + routingContext.put(ConstantKeys.PEER_CERTIFICATE_THUMBPRINT, getThumbprint(peerCertificate, "SHA-256")); + } else if (sslSession == null && (authenticatedClient.isTlsClientCertificateBoundAccessTokens() || domain.usePlainFapiProfile())) { + routingContext.fail(new InvalidClientException("Missing or invalid peer certificate")); + return; + } + } catch (SSLPeerUnverifiedException | CertificateEncodingException | NoSuchAlgorithmException ce ) { + if (authenticatedClient.isTlsClientCertificateBoundAccessTokens() || domain.usePlainFapiProfile()) { + routingContext.fail(new InvalidClientException("Missing or invalid peer certificate")); + return; + } + } + // put client in context and continue routingContext.put(CLIENT_CONTEXT_KEY, authenticatedClient); routingContext.next(); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java index 3432aa11bf..ac004eeca8 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProvider.java @@ -16,7 +16,6 @@ package io.gravitee.am.gateway.handler.oauth2.resources.auth.provider; import io.gravitee.am.common.oidc.ClientAuthenticationMethod; -import io.gravitee.am.gateway.handler.common.utils.ConstantKeys; import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException; import io.gravitee.am.model.oidc.Client; import io.vertx.core.AsyncResult; @@ -28,16 +27,12 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; -import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.List; -import static io.gravitee.am.gateway.handler.oauth2.resources.auth.provider.CertificateUtils.getThumbprint; - /** * Client Authentication method : tls_client_auth * @@ -77,13 +72,11 @@ public void handle(Client client, RoutingContext context, Handler thumbprint256.equals(jwk.getX5tS256()) || thumbprint.equals(jwk.getX5t())); if (match) { - context.put(ConstantKeys.PEER_CERTIFICATE_THUMBPRINT, thumbprint256); handler.handle(Future.succeededFuture(client)); } else { handler.handle(Future.failedFuture(new InvalidClientException("Invalid client: invalid self-signed certificate"))); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java index 2c2bb3deb8..2fa52563ca 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/introspection/impl/IntrospectionServiceImpl.java @@ -26,6 +26,8 @@ import io.reactivex.Single; import org.springframework.beans.factory.annotation.Autowired; +import java.util.Map; + /** * @author David BRASSELY (david.brassely at graviteesource.com) * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) @@ -76,7 +78,10 @@ private IntrospectionResponse convert(AccessToken accessToken, User user) { accessToken.getAdditionalInformation().forEach((k, v) -> introspectionResponse.putIfAbsent(k, v)); } - introspectionResponse.setConfirmationMethod(accessToken.getConfirmationMethod()); + final Map cnf = accessToken.getConfirmationMethod(); + if (cnf != null) { + introspectionResponse.setConfirmationMethod(cnf); + } // remove "aud" claim due to some backend APIs unable to verify the "aud" value // see diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java index 03cddfd32b..051b4ab0af 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/token/impl/TokenServiceImpl.java @@ -274,8 +274,10 @@ private JWT createAccessTokenJWT(OAuth2Request request, Client client, User user // set exp claim jwt.setExp(Instant.ofEpochSecond(jwt.getIat()).plusSeconds(client.getAccessTokenValiditySeconds()).getEpochSecond()); - jwt.setConfirmationMethod(Maps.builder().put(JWT.CONFIRMATION_METHOD_X509_THUMBPRINT, request.getConfirmationMethodX5S256()).build()); - + final String cnfValue = request.getConfirmationMethodX5S256(); + if (cnfValue != null) { + jwt.setConfirmationMethod(Maps.builder().put(JWT.CONFIRMATION_METHOD_X509_THUMBPRINT, cnfValue).build()); + } // set claims parameter (only for an access token) // useful for UserInfo Endpoint to request for specific claims MultiValueMap requestParameters = request.parameters(); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java index 347d6bbf2f..d5a3a418f7 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/OIDCProvider.java @@ -255,7 +255,7 @@ private void startOpenIDConnectProtocol() { .handler(dynamicClientAccessEndpoint::renewClientSecret); // client auth handler - final Handler clientAuthHandler = ClientAuthHandler.create(clientSyncService, clientAssertionService, jwkService); + final Handler clientAuthHandler = ClientAuthHandler.create(clientSyncService, clientAssertionService, jwkService, domain); // Request object registration oidcRouter diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationRequest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationRequest.java index 9fcae17a0c..320edb3853 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationRequest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/DynamicClientRegistrationRequest.java @@ -170,6 +170,9 @@ public class DynamicClientRegistrationRequest { @JsonProperty("tls_client_auth_san_email") private Optional tlsClientAuthSanEmail; + @JsonProperty("tls_client_certificate_bound_access_tokens") + private Optional tlsClientCertificateBoundAccessTokens; + /******************************************************************************* * Metadata in same order than the openid JARM specification * https://openid.net//specs/openid-financial-api-jarm.html#client-metadata @@ -509,6 +512,14 @@ public void setTlsClientAuthSanEmail(Optional tlsClientAuthSanEmail) { this.tlsClientAuthSanEmail = tlsClientAuthSanEmail; } + public Optional getTlsClientCertificateBoundAccessTokens() { + return tlsClientCertificateBoundAccessTokens; + } + + public void setTlsClientCertificateBoundAccessTokens(Optional tlsClientCertificateBoundAccessTokens) { + this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; + } + public Optional getAuthorizationSignedResponseAlg() { return authorizationSignedResponseAlg; } @@ -595,6 +606,7 @@ public Client patch(Client client) { SetterUtils.safeSet(client::setTlsClientAuthSanEmail, this.getTlsClientAuthSanEmail()); SetterUtils.safeSet(client::setTlsClientAuthSanIp, this.getTlsClientAuthSanIp()); SetterUtils.safeSet(client::setTlsClientAuthSanUri, this.getTlsClientAuthSanUri()); + SetterUtils.safeSet(client::setTlsClientCertificateBoundAccessTokens, this.getTlsClientCertificateBoundAccessTokens()); /* set OpenID Connect RP-Initiated Logout metadata */ SetterUtils.safeSet(client::setPostLogoutRedirectUris, this.getPostLogoutRedirectUris()); @@ -651,6 +663,7 @@ public Client update(Client client) { SetterUtils.safeSet(client::setTlsClientAuthSanEmail, this.getTlsClientAuthSanEmail()); SetterUtils.safeSet(client::setTlsClientAuthSanIp, this.getTlsClientAuthSanIp()); SetterUtils.safeSet(client::setTlsClientAuthSanUri, this.getTlsClientAuthSanUri()); + SetterUtils.safeSet(client::setTlsClientCertificateBoundAccessTokens, this.getTlsClientCertificateBoundAccessTokens()); /* set OpenID Connect RP-Initiated Logout metadata */ SetterUtils.safeSet(client::setPostLogoutRedirectUris, this.getPostLogoutRedirectUris()); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/ClientServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/ClientServiceImpl.java index b98ede4fd2..d69d5f14dc 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/ClientServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/clientregistration/impl/ClientServiceImpl.java @@ -250,6 +250,7 @@ private ApplicationSettings getSettings(Client client) { oAuthSettings.setTlsClientAuthSanIp(client.getTlsClientAuthSanIp()); oAuthSettings.setTlsClientAuthSanUri(client.getTlsClientAuthSanUri()); oAuthSettings.setTlsClientAuthSubjectDn(client.getTlsClientAuthSubjectDn()); + oAuthSettings.setTlsClientCertificateBoundAccessTokens((client.isTlsClientCertificateBoundAccessTokens())); ApplicationSettings applicationSettings = new ApplicationSettings(); // oauth settings diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandlerTest.java index 74768d8cf6..07317cba65 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandlerTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/handler/ClientAuthHandlerTest.java @@ -21,6 +21,7 @@ import io.gravitee.am.gateway.handler.oauth2.resources.handler.ExceptionHandler; import io.gravitee.am.gateway.handler.oauth2.service.assertion.ClientAssertionService; import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; +import io.gravitee.am.model.Domain; import io.gravitee.am.model.oidc.Client; import io.gravitee.common.http.HttpStatusCode; import io.reactivex.Maybe; @@ -51,12 +52,15 @@ public class ClientAuthHandlerTest extends RxWebTestBase { @Mock private JWKService jwkService; + @Mock + private Domain domain; + @Override public void setUp() throws Exception { super.setUp(); router.post("/oauth/token") - .handler(ClientAuthHandler.create(clientSyncService, clientAssertionService, jwkService)) + .handler(ClientAuthHandler.create(clientSyncService, clientAssertionService, jwkService, domain)) .handler(rc -> rc.response().setStatusCode(200).end()) .failureHandler(new ExceptionHandler()); } @@ -158,6 +162,21 @@ public void shouldInvoke_clientCredentials_privateJWT_privateJWTTokenAuthMethod( HttpStatusCode.OK_200, "OK"); } + + @Test + public void shouldNotInvoke_clientCredentials_privateJWT_privateJWTTokenAuthMethod_MissingSSLCert() throws Exception { + final String clientId = "client-id"; + Client client = mock(Client.class); + when(client.isTlsClientCertificateBoundAccessTokens()).thenReturn(true); + + when(clientAssertionService.assertClient(eq("type"), eq("myToken"), anyString())).thenReturn(Maybe.just(client)); + + testRequest( + HttpMethod.POST, + "/oauth/token?client_assertion_type=type&client_assertion=myToken", + HttpStatusCode.UNAUTHORIZED_401, "Unauthorized"); + } + @Test public void shouldInvoke_clientCredentials_clientSecret_clientSecretJWTTokenAuthMethod() throws Exception { final String clientId = "client-id"; diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java index db65c5861e..163ae978aa 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/auth/provider/ClientCertificateAuthProviderTest.java @@ -103,7 +103,6 @@ public void authorized_client() throws Exception { SSLSession sslSession = mock(SSLSession.class); X509Certificate certificate = mock(X509Certificate.class); - when(certificate.getEncoded()).thenReturn("MYCERTIFICATE".getBytes()); Principal subjectDN = mock(Principal.class); when(client.getTlsClientAuthSubjectDn()).thenReturn("CN=localhost, O=GraviteeSource, C=FR"); @@ -120,7 +119,6 @@ public void authorized_client() throws Exception { latch.countDown(); Assert.assertNotNull(clientAsyncResult); Assert.assertNotNull(clientAsyncResult.result()); - verify(context).put(eq(ConstantKeys.PEER_CERTIFICATE_THUMBPRINT), any()); }); assertTrue(latch.await(10, TimeUnit.SECONDS)); diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/application/ApplicationOAuthSettings.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/application/ApplicationOAuthSettings.java index 0dfdc95907..13e62a2e46 100644 --- a/gravitee-am-model/src/main/java/io/gravitee/am/model/application/ApplicationOAuthSettings.java +++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/application/ApplicationOAuthSettings.java @@ -245,6 +245,8 @@ public class ApplicationOAuthSettings { private String tlsClientAuthSanEmail; + private boolean tlsClientCertificateBoundAccessTokens; + /** * JWS alg algorithm [JWA] REQUIRED for signing Authorization Responses. */ @@ -338,6 +340,7 @@ public ApplicationOAuthSettings(ApplicationOAuthSettings other) { this.tlsClientAuthSanEmail = other.tlsClientAuthSanEmail; this.tlsClientAuthSanIp = other.tlsClientAuthSanIp; this.tlsClientAuthSanUri = other.tlsClientAuthSanUri; + this.tlsClientCertificateBoundAccessTokens = other.tlsClientCertificateBoundAccessTokens; this.authorizationSignedResponseAlg = other.authorizationSignedResponseAlg; this.authorizationEncryptedResponseAlg = other.authorizationEncryptedResponseAlg; this.authorizationEncryptedResponseEnc = other.authorizationEncryptedResponseEnc; @@ -827,6 +830,14 @@ public void setSilentReAuthentication(boolean silentReAuthentication) { this.silentReAuthentication = silentReAuthentication; } + public boolean isTlsClientCertificateBoundAccessTokens() { + return tlsClientCertificateBoundAccessTokens; + } + + public void setTlsClientCertificateBoundAccessTokens(boolean tlsClientCertificateBoundAccessTokens) { + this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; + } + public void copyTo(Client client) { client.setClientId(this.clientId); client.setClientSecret(this.clientSecret); @@ -880,6 +891,7 @@ public void copyTo(Client client) { client.setTlsClientAuthSanEmail(this.tlsClientAuthSanEmail); client.setTlsClientAuthSanIp(this.tlsClientAuthSanIp); client.setTlsClientAuthSanUri(this.tlsClientAuthSanUri); + client.setTlsClientCertificateBoundAccessTokens(this.tlsClientCertificateBoundAccessTokens); client.setAuthorizationSignedResponseAlg(this.authorizationSignedResponseAlg); client.setAuthorizationEncryptedResponseAlg(this.authorizationEncryptedResponseAlg); client.setAuthorizationEncryptedResponseEnc(this.authorizationEncryptedResponseEnc); diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/Client.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/Client.java index 105da70f32..6ea4a9d504 100644 --- a/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/Client.java +++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/oidc/Client.java @@ -143,6 +143,8 @@ public class Client implements Cloneable, Resource, PasswordSettingsAware { private String tlsClientAuthSanEmail; + private boolean tlsClientCertificateBoundAccessTokens; + private String authorizationSignedResponseAlg; private String authorizationEncryptedResponseAlg; @@ -277,6 +279,7 @@ public Client(Client other) { this.mfaSettings = other.mfaSettings; this.singleSignOut = other.singleSignOut; this.silentReAuthentication = other.silentReAuthentication; + this.tlsClientCertificateBoundAccessTokens = other.tlsClientCertificateBoundAccessTokens; } public String getId() { @@ -884,6 +887,14 @@ public void setSilentReAuthentication(boolean silentReAuthentication) { this.silentReAuthentication = silentReAuthentication; } + public boolean isTlsClientCertificateBoundAccessTokens() { + return tlsClientCertificateBoundAccessTokens; + } + + public void setTlsClientCertificateBoundAccessTokens(boolean tlsClientCertificateBoundAccessTokens) { + this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoApplicationRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoApplicationRepository.java index eaf38ba5bd..d6245c89e9 100644 --- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoApplicationRepository.java +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoApplicationRepository.java @@ -313,6 +313,7 @@ private static ApplicationOAuthSettingsMongo convert(ApplicationOAuthSettings ot applicationOAuthSettingsMongo.setTlsClientAuthSanEmail(other.getTlsClientAuthSanEmail()); applicationOAuthSettingsMongo.setTlsClientAuthSanIp(other.getTlsClientAuthSanIp()); applicationOAuthSettingsMongo.setTlsClientAuthSanUri(other.getTlsClientAuthSanUri()); + applicationOAuthSettingsMongo.setTlsClientCertificateBoundAccessTokens(other.isTlsClientCertificateBoundAccessTokens()); applicationOAuthSettingsMongo.setAuthorizationSignedResponseAlg(other.getAuthorizationSignedResponseAlg()); applicationOAuthSettingsMongo.setAuthorizationEncryptedResponseAlg(other.getAuthorizationEncryptedResponseAlg()); applicationOAuthSettingsMongo.setAuthorizationEncryptedResponseEnc(other.getAuthorizationEncryptedResponseEnc()); @@ -381,6 +382,7 @@ private static ApplicationOAuthSettings convert(ApplicationOAuthSettingsMongo ot applicationOAuthSettings.setTlsClientAuthSanEmail(other.getTlsClientAuthSanEmail()); applicationOAuthSettings.setTlsClientAuthSanIp(other.getTlsClientAuthSanIp()); applicationOAuthSettings.setTlsClientAuthSanUri(other.getTlsClientAuthSanUri()); + applicationOAuthSettings.setTlsClientCertificateBoundAccessTokens(other.isTlsClientCertificateBoundAccessTokens()); applicationOAuthSettings.setAuthorizationSignedResponseAlg(other.getAuthorizationSignedResponseAlg()); applicationOAuthSettings.setAuthorizationEncryptedResponseAlg(other.getAuthorizationEncryptedResponseAlg()); applicationOAuthSettings.setAuthorizationEncryptedResponseEnc(other.getAuthorizationEncryptedResponseEnc()); diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ApplicationOAuthSettingsMongo.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ApplicationOAuthSettingsMongo.java index f0ea5c79fe..b95eda21ca 100644 --- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ApplicationOAuthSettingsMongo.java +++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/internal/model/ApplicationOAuthSettingsMongo.java @@ -79,6 +79,7 @@ public class ApplicationOAuthSettingsMongo { private String tlsClientAuthSanUri; private String tlsClientAuthSanIp; private String tlsClientAuthSanEmail; + private boolean tlsClientCertificateBoundAccessTokens; private String authorizationSignedResponseAlg; private String authorizationEncryptedResponseAlg; private String authorizationEncryptedResponseEnc; @@ -566,4 +567,12 @@ public boolean isSilentReAuthentication() { public void setSilentReAuthentication(boolean silentReAuthentication) { this.silentReAuthentication = silentReAuthentication; } + + public boolean isTlsClientCertificateBoundAccessTokens() { + return tlsClientCertificateBoundAccessTokens; + } + + public void setTlsClientCertificateBoundAccessTokens(boolean tlsClientCertificateBoundAccessTokens) { + this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; + } } diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchApplicationOAuthSettings.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchApplicationOAuthSettings.java index d8979093c2..990a657b6c 100644 --- a/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchApplicationOAuthSettings.java +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/model/PatchApplicationOAuthSettings.java @@ -15,6 +15,7 @@ */ package io.gravitee.am.service.model; +import com.fasterxml.jackson.annotation.JsonProperty; import io.gravitee.am.model.TokenClaim; import io.gravitee.am.model.application.ApplicationOAuthSettings; import io.gravitee.am.model.oidc.JWKSet; @@ -81,6 +82,7 @@ public class PatchApplicationOAuthSettings { private Optional tlsClientAuthSanUri; private Optional tlsClientAuthSanIp; private Optional tlsClientAuthSanEmail; + private Optional tlsClientCertificateBoundAccessTokens; private Optional authorizationSignedResponseAlg; private Optional authorizationEncryptedResponseAlg; private Optional authorizationEncryptedResponseEnc; @@ -545,6 +547,14 @@ public void setSilentReAuthentication(Optional silentReAuthentication) this.silentReAuthentication = silentReAuthentication; } + public Optional getTlsClientCertificateBoundAccessTokens() { + return tlsClientCertificateBoundAccessTokens; + } + + public void setTlsClientCertificateBoundAccessTokens(Optional tlsClientCertificateBoundAccessTokens) { + this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; + } + public ApplicationOAuthSettings patch(ApplicationOAuthSettings _toPatch) { // create new object for audit purpose (patch json result) ApplicationOAuthSettings toPatch = _toPatch == null ? new ApplicationOAuthSettings() : new ApplicationOAuthSettings(_toPatch); @@ -601,6 +611,7 @@ public ApplicationOAuthSettings patch(ApplicationOAuthSettings _toPatch) { SetterUtils.safeSet(toPatch::setTlsClientAuthSanEmail, this.getTlsClientAuthSanEmail()); SetterUtils.safeSet(toPatch::setTlsClientAuthSanIp, this.getTlsClientAuthSanIp()); SetterUtils.safeSet(toPatch::setTlsClientAuthSanUri, this.getTlsClientAuthSanUri()); + SetterUtils.safeSet(toPatch::setTlsClientCertificateBoundAccessTokens, this.getTlsClientCertificateBoundAccessTokens()); SetterUtils.safeSet(toPatch::setAuthorizationSignedResponseAlg, this.getAuthorizationSignedResponseAlg()); SetterUtils.safeSet(toPatch::setAuthorizationEncryptedResponseAlg, this.getAuthorizationEncryptedResponseAlg()); SetterUtils.safeSet(toPatch::setAuthorizationEncryptedResponseEnc, this.getAuthorizationEncryptedResponseEnc()); From afac98cf319a2424ab7aec183fa66f2ab80d18d0 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 12 Aug 2021 10:28:21 +0200 Subject: [PATCH 21/24] feat: add control on JWS algorithm for RO and ClientAssertion when FAPI is enabled fixes gravitee-io/issues#5989 --- .../impl/ClientAssertionServiceImpl.java | 14 +++++- ...PushedAuthorizationRequestServiceImpl.java | 7 +++ .../impl/RequestObjectServiceImpl.java | 11 +++++ .../oidc/service/utils/JWAlgorithmUtils.java | 5 ++ .../assertion/ClientAssertionServiceTest.java | 48 +++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java index 11015be5a6..71be2a490d 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java @@ -23,6 +23,7 @@ import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; +import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; import io.gravitee.am.common.oidc.ClientAuthenticationMethod; import io.gravitee.am.gateway.handler.common.client.ClientSyncService; import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException; @@ -32,6 +33,7 @@ import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService; +import io.gravitee.am.model.Domain; import io.gravitee.am.model.oidc.Client; import io.gravitee.am.model.oidc.JWKSet; import io.reactivex.Maybe; @@ -40,13 +42,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import java.text.ParseException; import java.time.Instant; import java.util.Date; import java.util.List; +import static io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils.isCompliantWithFapi; + /** * Client assertion as described for oauth2 assertion framework * and openid client authentication specs @@ -73,6 +76,9 @@ public class ClientAssertionServiceImpl implements ClientAssertionService { @Autowired private OpenIDDiscoveryService openIDDiscoveryService; + @Autowired + private Domain domain; + @Override public Maybe assertClient(String assertionType, String assertion, String basePath) { @@ -134,6 +140,10 @@ private Maybe validateJWT(String assertion, String basePath) { return Maybe.error(NOT_VALID); } + if (this.domain.usePlainFapiProfile() && !isCompliantWithFapi(jwt.getHeader().getAlgorithm().getName())) { + return Maybe.error(new InvalidClientException("JWT Assertion must be signed with PS256")); + } + return Maybe.just(jwt); } catch (ParseException pe) { return Maybe.error(NOT_VALID); @@ -189,6 +199,8 @@ private Maybe validateSignatureWithHMAC(JWT jwt) { String clientId = jwt.getJWTClaimsSet().getSubject(); SignedJWT signedJWT = (SignedJWT) jwt; + + return this.clientSyncService.findByClientId(clientId) .switchIfEmpty(Maybe.error(new InvalidClientException("Missing or invalid client"))) .flatMap(client -> { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java index 7a0fc63885..0f98fa75f3 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java @@ -31,6 +31,7 @@ import io.gravitee.am.gateway.handler.oidc.service.jwe.JWEService; import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService; +import io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils; import io.gravitee.am.model.Domain; import io.gravitee.am.model.jose.JWK; import io.gravitee.am.model.oidc.Client; @@ -51,6 +52,7 @@ import java.util.List; import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.redirectMatches; +import static io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils.isCompliantWithFapi; /** * @author Eric LELEU (eric.leleu at graviteesource.com) @@ -186,6 +188,11 @@ private JWT checkRequestObjectAlgorithm(JWT jwt) { (jwt.getHeader().getAlgorithm() != null && "none".equalsIgnoreCase(jwt.getHeader().getAlgorithm().getName()))) { throw new InvalidRequestObjectException("Request object must be signed"); } + + if (this.domain.usePlainFapiProfile() && !isCompliantWithFapi(jwt.getHeader().getAlgorithm().getName())) { + throw new InvalidRequestObjectException("Request object must be signed with PS256"); + } + return jwt; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java index cbc424b828..d10fc054cb 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/request/impl/RequestObjectServiceImpl.java @@ -27,6 +27,7 @@ import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectRegistrationRequest; import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectRegistrationResponse; import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectService; +import io.gravitee.am.model.Domain; import io.gravitee.am.model.jose.JWK; import io.gravitee.am.model.oidc.Client; import io.gravitee.am.model.oidc.JWKSet; @@ -45,6 +46,8 @@ import java.time.Instant; import java.util.Date; +import static io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils.isCompliantWithFapi; + /** * @author David BRASSELY (david.brassely at graviteesource.com) * @author GraviteeSource Teams @@ -69,6 +72,9 @@ public class RequestObjectServiceImpl implements RequestObjectService { @Autowired private RequestObjectRepository requestObjectRepository; + @Autowired + private Domain domain; + @Override public Single readRequestObject(String request, Client client) { return jweService.decrypt(request, client) @@ -182,6 +188,11 @@ private Completable checkRequestObjectAlgorithm(JWT jwt) { (jwt.getHeader().getAlgorithm() != null && "none".equalsIgnoreCase(jwt.getHeader().getAlgorithm().getName()))) { return Completable.error(new InvalidRequestObjectException("Request object must be signed")); } + + if (this.domain.usePlainFapiProfile() && !isCompliantWithFapi(jwt.getHeader().getAlgorithm().getName())) { + return Completable.error(new InvalidRequestObjectException("Request object must be signed with PS256")); + } + return Completable.complete(); } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/utils/JWAlgorithmUtils.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/utils/JWAlgorithmUtils.java index a3bac06c09..7c6cb084ed 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/utils/JWAlgorithmUtils.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oidc/service/utils/JWAlgorithmUtils.java @@ -44,6 +44,11 @@ public class JWAlgorithmUtils { JWSAlgorithm.HS256.getName(), JWSAlgorithm.HS384.getName(), JWSAlgorithm.HS512.getName() ))); + public static final boolean isCompliantWithFapi(String alg) { + // ES256 is also authorized by FAPI but not managed by AM + return JWSAlgorithm.PS256.getName().equals(alg); + } + /** * https://tools.ietf.org/html/rfc7518#section-4.1 */ diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/ClientAssertionServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/ClientAssertionServiceTest.java index 84156575ca..5d4e893622 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/ClientAssertionServiceTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/ClientAssertionServiceTest.java @@ -33,6 +33,7 @@ import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService; +import io.gravitee.am.model.Domain; import io.gravitee.am.model.jose.JWK; import io.gravitee.am.model.jose.RSAKey; import io.gravitee.am.model.oidc.Client; @@ -91,6 +92,9 @@ public class ClientAssertionServiceTest { @InjectMocks private ClientAssertionService clientAssertionService = new ClientAssertionServiceImpl(); + @Mock + private Domain domain; + @Test public void testAssertionTypeNotValid() { TestObserver testObserver = clientAssertionService.assertClient("",null,null).test(); @@ -342,6 +346,25 @@ public void testRsaJwt_withClientJwks() throws NoSuchAlgorithmException, JOSEExc testObserver.assertValue(client); } + @Test + public void testRsaJwt_withClientJwks_RS256InvalidForFAPI() throws NoSuchAlgorithmException, JOSEException{ + KeyPair rsaKey = generateRsaKeyPair(); + + RSAPrivateKey privateKey = (RSAPrivateKey) rsaKey.getPrivate(); + + String assertion = generateJWT(privateKey); + OpenIDProviderMetadata openIDProviderMetadata = Mockito.mock(OpenIDProviderMetadata.class); + String basePath="/"; + + when(openIDProviderMetadata.getTokenEndpoint()).thenReturn(AUDIENCE); + when(openIDDiscoveryService.getConfiguration(basePath)).thenReturn(openIDProviderMetadata); + + when(domain.usePlainFapiProfile()).thenReturn(true); + TestObserver testObserver = clientAssertionService.assertClient(JWT_BEARER_TYPE,assertion,basePath).test(); + testObserver.awaitTerminalEvent(); + testObserver.assertError(InvalidClientException.class); + } + @Test public void testRsaJwt_withClientJwks_invalidClientAuthMethod() throws NoSuchAlgorithmException, JOSEException{ KeyPair rsaKey = generateRsaKeyPair(); @@ -436,6 +459,31 @@ public void testHmacJwt() throws NoSuchAlgorithmException, JOSEException { testObserver.assertValue(client); } + @Test + public void testHmacJwt_RS256InvalidForFapi() throws NoSuchAlgorithmException, JOSEException { + // Generate random 256-bit (32-byte) shared secret + SecureRandom random = new SecureRandom(); + byte[] sharedSecret = new byte[32]; + random.nextBytes(sharedSecret); + + String clientSecret = new String(sharedSecret, StandardCharsets.UTF_8); + + JWSSigner signer = new MACSigner(clientSecret); + + String assertion = generateJWT(signer); + OpenIDProviderMetadata openIDProviderMetadata = Mockito.mock(OpenIDProviderMetadata.class); + String basePath="/"; + + when(openIDProviderMetadata.getTokenEndpoint()).thenReturn(AUDIENCE); + when(openIDDiscoveryService.getConfiguration(basePath)).thenReturn(openIDProviderMetadata); + + when(domain.usePlainFapiProfile()).thenReturn(true); + + TestObserver testObserver = clientAssertionService.assertClient(JWT_BEARER_TYPE,assertion,basePath).test(); + testObserver.awaitTerminalEvent(); + testObserver.assertError(InvalidClientException.class); + } + @Test public void testHmacJwt_invalidClientAuthMethod() throws NoSuchAlgorithmException, JOSEException { // Generate random 256-bit (32-byte) shared secret From e4fdff0ced4f18a281f001e42fde16c389c7b09d Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 12 Aug 2021 11:53:25 +0200 Subject: [PATCH 22/24] fix: manage PAR endpoint rule regarding private_key_jwt client authentication fixes gravitee-io/issues#5990 --- .../oidc/ClientAuthenticationMethod.java | 5 ++++ .../handler/oauth2/OAuth2Provider.java | 2 +- .../authorization/AuthorizationEndpoint.java | 4 +-- .../impl/ClientAssertionServiceImpl.java | 11 +++++--- ...PushedAuthorizationRequestServiceImpl.java | 26 ++++++++++++++----- .../endpoint/AuthorizationEndpointTest.java | 2 +- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/ClientAuthenticationMethod.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/ClientAuthenticationMethod.java index 3bea2bccd4..9ba2b0922c 100644 --- a/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/ClientAuthenticationMethod.java +++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/oidc/ClientAuthenticationMethod.java @@ -68,6 +68,11 @@ public interface ClientAuthenticationMethod { */ String SELF_SIGNED_TLS_CLIENT_AUTH = "self_signed_tls_client_auth"; + /** + * URN to identify the ClientAssertion using JWT token + */ + String JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + static List supportedValues() { return Arrays.asList( CLIENT_SECRET_BASIC, diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index ea692cced2..8ae95fb070 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -213,7 +213,7 @@ private void initRouter() { .handler(authenticationFlowHandler.create()) .handler(new AuthorizationRequestResolveHandler()) .handler(new AuthorizationRequestEndUserConsentHandler(userConsentService)) - .handler(new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService, domain)) + .handler(new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService)) .failureHandler(new AuthorizationRequestFailureHandler(openIDDiscoveryService, jwtService, jweService, environment)); // Authorization consent endpoint diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java index a2b4c1c6d0..4cd6b0fe90 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/authorization/AuthorizationEndpoint.java @@ -56,12 +56,10 @@ public class AuthorizationEndpoint implements Handler { private final Flow flow; private final ThymeleafTemplateEngine engine; private final PushedAuthorizationRequestService parService; - private final Domain domain; - public AuthorizationEndpoint(Flow flow, ThymeleafTemplateEngine engine, PushedAuthorizationRequestService parService, Domain domain) { + public AuthorizationEndpoint(Flow flow, ThymeleafTemplateEngine engine, PushedAuthorizationRequestService parService) { this.flow = flow; this.engine = engine; - this.domain = domain; this.parService = parService; } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java index 71be2a490d..b68302ae8f 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/assertion/impl/ClientAssertionServiceImpl.java @@ -23,7 +23,6 @@ import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; -import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; import io.gravitee.am.common.oidc.ClientAuthenticationMethod; import io.gravitee.am.gateway.handler.common.client.ClientSyncService; import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException; @@ -48,6 +47,7 @@ import java.util.Date; import java.util.List; +import static io.gravitee.am.common.oidc.ClientAuthenticationMethod.JWT_BEARER; import static io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils.isCompliantWithFapi; /** @@ -61,7 +61,6 @@ public class ClientAssertionServiceImpl implements ClientAssertionService { private static final Logger LOGGER = LoggerFactory.getLogger(ClientAssertionServiceImpl.class); - private static final String JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; private static final InvalidClientException NOT_VALID = new InvalidClientException("assertion is not valid"); @Autowired @@ -136,7 +135,13 @@ private Maybe validateJWT(String assertion, String basePath) { return Maybe.error(new ServerErrorException("Unable to retrieve discovery token endpoint.")); } - if (aud.stream().filter(discovery.getTokenEndpoint()::equals).count()==0) { + // OIDC specifies that "The Audience SHOULD be the URL of the Authorization Server's Token Endpoint." + // https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + // BUT the PAR specification specify the usage of the Issuer value, Token endpoint or PAR endpoint. + // https://tools.ietf.org/id/draft-lodderstedt-oauth-par-00.html#pushed-authorization-request-endpoint + if (aud.stream().filter(discovery.getTokenEndpoint()::equals).count() == 0 && + (discovery.getIssuer() != null && aud.stream().filter(discovery.getIssuer()::equals).count() == 0) && + (discovery.getParEndpoint() != null && aud.stream().filter(discovery.getParEndpoint()::equals).count() == 0)) { return Maybe.error(NOT_VALID); } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java index 0f98fa75f3..3110b25eb8 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java @@ -15,10 +15,7 @@ */ package io.gravitee.am.gateway.handler.oauth2.service.par.impl; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.PlainJWT; -import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.*; import io.gravitee.am.common.exception.oauth2.InvalidRequestException; import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; import io.gravitee.am.common.exception.oauth2.InvalidRequestUriException; @@ -26,12 +23,10 @@ import io.gravitee.am.common.oauth2.Parameters; import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestResponse; import io.gravitee.am.gateway.handler.oauth2.service.par.PushedAuthorizationRequestService; -import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDDiscoveryService; import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.jwe.JWEService; import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService; -import io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils; import io.gravitee.am.model.Domain; import io.gravitee.am.model.jose.JWK; import io.gravitee.am.model.oidc.Client; @@ -51,6 +46,7 @@ import java.util.Date; import java.util.List; +import static io.gravitee.am.common.oidc.ClientAuthenticationMethod.JWT_BEARER; import static io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.ParamUtils.redirectMatches; import static io.gravitee.am.gateway.handler.oidc.service.utils.JWAlgorithmUtils.isCompliantWithFapi; @@ -124,8 +120,11 @@ public Single registerParameters(PushedAutho par.setClient(client.getId()); // link parameters to the internal client identifier par.setExpireAt(new Date(Instant.now().plusMillis(requestUriValidity).toEpochMilli())); + + Completable registrationValidation = Completable.fromAction(() -> { - if (!client.getClientId().equals(par.getParameters().getFirst(Parameters.CLIENT_ID))) { + String clientId = jwtClientAssertion(par) ? getClientIdFromAssertion(par) : par.getParameters().getFirst(Parameters.CLIENT_ID); + if (!client.getClientId().equals(clientId)) { throw new InvalidRequestException(); } if (par.getParameters().getFirst(io.gravitee.am.common.oidc.Parameters.REQUEST_URI) != null) { @@ -154,6 +153,19 @@ public Single registerParameters(PushedAutho }); } + private boolean jwtClientAssertion(PushedAuthorizationRequest par) { + return par.getParameters().getFirst(Parameters.CLIENT_ASSERTION) != null && JWT_BEARER.equals(par.getParameters().getFirst(Parameters.CLIENT_ASSERTION_TYPE)); + } + + private String getClientIdFromAssertion(PushedAuthorizationRequest par) { + try { + return JWTParser.parse(par.getParameters().getFirst(Parameters.CLIENT_ASSERTION)).getJWTClaimsSet().getSubject(); + } catch (ParseException e) { + LOGGER.warn("Unable to parse the Client Assertion to extract the sub claim"); + return null; + } + } + private Single readRequestObject(Client client, String request) { return jweService.decrypt(request, client) .onErrorResumeNext((ex) -> { diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java index 543e7dcc9f..8cb576c419 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java @@ -105,7 +105,7 @@ public class AuthorizationEndpointTest extends RxWebTestBase { public void setUp() throws Exception { super.setUp(); - AuthorizationEndpoint authorizationEndpointHandler = new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService, domain); + AuthorizationEndpoint authorizationEndpointHandler = new AuthorizationEndpoint(flow, thymeleafTemplateEngine, parService); // set openid provider service OpenIDProviderMetadata openIDProviderMetadata = new OpenIDProviderMetadata(); From e38ab7fe297cc532c538cd5f60a03631f82270af Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 13 Aug 2021 17:25:13 +0200 Subject: [PATCH 23/24] chore: make Postman collection pass since the behaviour on RequestObject validation has changed regarding FAPI expectation (redirect to default error page) fixes gravitee-io/issues#3708 --- ...eio-am-openid-core-request-object-collection.json | 12 ++++++------ .../graviteeio-am-openid-fapi-collection.json | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/postman/collections/graviteeio-am-openid-core-request-object-collection.json b/postman/collections/graviteeio-am-openid-core-request-object-collection.json index 166bbd16fb..e67c22b2a2 100644 --- a/postman/collections/graviteeio-am-openid-core-request-object-collection.json +++ b/postman/collections/graviteeio-am-openid-core-request-object-collection.json @@ -1048,11 +1048,11 @@ "", "pm.test(\"Should be a location error\", function() {", " var location = postman.getResponseHeader('Location');", - " let domain = pm.environment.get(\"domain\");", + " let domain = pm.environment.get(\"domainHrid\");", " ", - " tests['Redirect to redirect_uri'] = location.startsWith(\"https://op-test:60001/authz_cb?error\");", - " tests['Contains an error query-parameter'] = location.includes('error=invalid_request');", - " tests['Contains an error description query-parameter'] = location.includes('error_description=Invalid+signature');", + " tests['Redirect to redirect_uri'] = location.startsWith(pm.environment.get('gateway_url') + '/' + domain + '/oauth/error');", + " tests['Contains an error query-parameter'] = location.includes('error=invalid_request_object');", + " tests['Contains an error description query-parameter'] = location.includes('error_description=Invalid+request+object+signing+algorithm');", "});" ], "type": "text/javascript" @@ -2140,9 +2140,9 @@ "", "pm.test(\"Should be a location error\", function() {", " var location = postman.getResponseHeader('Location');", - " let domain = pm.environment.get(\"domain\");", + " let domain = pm.environment.get(\"domainHrid\");", " ", - " tests['Redirect to redirect_uri'] = location.startsWith(\"http://unknown-redirect-uri/?error\");", + " tests['Redirect to redirect_uri'] = location.startsWith(pm.environment.get('gateway_url') + '/' + domain + '/oauth/error');", " tests['Contains an error query-parameter'] = location.includes('error=invalid_request_object');", " tests['Contains an error description query-parameter'] = location.includes('error_description=Malformed+request+object');", "});" diff --git a/postman/collections/graviteeio-am-openid-fapi-collection.json b/postman/collections/graviteeio-am-openid-fapi-collection.json index db8f5cf1c7..e99d0c53cb 100644 --- a/postman/collections/graviteeio-am-openid-fapi-collection.json +++ b/postman/collections/graviteeio-am-openid-fapi-collection.json @@ -1164,7 +1164,7 @@ " pm.expect(body).to.have.property(\"error_description\");", " ", " pm.expect(body.error).to.eql('invalid_request_object');", - " pm.expect(body.error_description).to.eql('Invalid signature');", + " pm.expect(body.error_description).to.eql('Invalid request object signing algorithm');", "});" ], "type": "text/javascript" @@ -1241,7 +1241,7 @@ " pm.expect(body).to.have.property(\"error_description\");", " ", " pm.expect(body.error).to.eql('invalid_request_object');", - " pm.expect(body.error_description).to.eql('Invalid signature');", + " pm.expect(body.error_description).to.eql('Invalid request object signing algorithm');", "});" ], "type": "text/javascript" @@ -1546,9 +1546,9 @@ "", "pm.test(\"Should be a location error\", function() {", " var location = postman.getResponseHeader('Location');", - " let domain = pm.environment.get(\"domain\");", + " let domain = pm.environment.get(\"domainHrid\");", " ", - " tests['Redirect to redirect_uri'] = location.startsWith(\"https://op-test:60001/authz_cb?error\");", + " tests['Redirect to redirect_uri'] = location.startsWith(pm.environment.get('gateway_url') + '/' + domain + '/oauth/error');", " tests['Contains an error query-parameter'] = location.includes('error=invalid_request_object');", "});" ], From fa964c74b859dc39d8c99e0f7863e7a75160a6d9 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 1 Sep 2021 09:01:46 +0200 Subject: [PATCH 24/24] chore: change the test container runner size --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 60b2a94e88..9b6e5843b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -163,7 +163,7 @@ jobs: os_name: "ubuntu" os_version: "2004" os_patch: "202101-01" - machine_size: "large" + machine_size: "xlarge" steps: - attach_workspace: