Repository: gravitee-io/graviteeio-access-management (java)

Issue:
[oidc] Request with prompt=none when not logged in
As part of OpenID Connect certification :

`Request with prompt=none when not logged in`

`1.401 | fault | Expected error, didn't get it`



Pull request description:
feat(oidc): handle prompt=none parameter when user is not logged in
Closes #300

Patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/AuthenticationErrorException.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/AuthenticationErrorException.java
new file mode 100644
index 0000000000..d08a777394
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/AuthenticationErrorException.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.gateway.handler.oauth2.exception;
+
+/**
+ * An Authentication Error Response is an OAuth 2.0 Authorization Error Response message returned from the OP's Authorization Endpoint
+ * in response to the Authorization Request message sent by the RP.
+ *
+ * See <a href="https://openid.net/specs/openid-connect-core-1_0.html#AuthError">3.1.2.6. Authentication Error Response</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public abstract class AuthenticationErrorException extends OAuth2Exception {
+
+    public AuthenticationErrorException() { super(); }
+
+    public AuthenticationErrorException(String message) {
+        super(message);
+    }
+
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/InteractionRequiredException.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/InteractionRequiredException.java
new file mode 100644
index 0000000000..782b70dc00
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/InteractionRequiredException.java
@@ -0,0 +1,42 @@
+/**
+ * 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.exception;
+
+/**
+ * The Authorization Server requires End-User interaction of some form to proceed.
+ * This error MAY be returned when the prompt parameter value in the Authentication Request is none,
+ * but the Authentication Request cannot be completed without displaying a user interface for End-User interaction.
+ *
+ * See <a href="https://openid.net/specs/openid-connect-core-1_0.html#AuthError">3.1.2.6. Authentication Error Response</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class InteractionRequiredException extends AuthenticationErrorException {
+
+    public InteractionRequiredException() {
+        super();
+    }
+
+    public InteractionRequiredException(String message) {
+        super(message);
+    }
+
+    @Override
+    public String getOAuth2ErrorCode() {
+        return "interaction_required";
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/LoginRequiredException.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/LoginRequiredException.java
new file mode 100644
index 0000000000..3b607cfc49
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/LoginRequiredException.java
@@ -0,0 +1,42 @@
+/**
+ * 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.exception;
+
+/**
+ * The Authorization Server requires End-User authentication.
+ * This error MAY be returned when the prompt parameter value in the Authentication Request is none,
+ * but the Authentication Request cannot be completed without displaying a user interface for End-User authentication.
+ *
+ * See <a href="https://openid.net/specs/openid-connect-core-1_0.html#AuthError">3.1.2.6. Authentication Error Response</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class LoginRequiredException extends AuthenticationErrorException {
+
+    public LoginRequiredException() {
+        super();
+    }
+
+    public LoginRequiredException(String message) {
+        super(message);
+    }
+
+    @Override
+    public String getOAuth2ErrorCode() {
+        return "login_required";
+    }
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/utils/OIDCParameters.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/utils/OIDCParameters.java
new file mode 100644
index 0000000000..6008f6af0d
--- /dev/null
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/oauth2/utils/OIDCParameters.java
@@ -0,0 +1,27 @@
+/**
+ * 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.utils;
+
+/**
+ * See <a href="https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml">OAuth 2.0 parameters</a>
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface OIDCParameters {
+
+    String PROMPT = "prompt";
+}
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java
index 3341269757..86b2af4ece 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/OAuth2Router.java
@@ -28,12 +28,12 @@ import io.gravitee.am.gateway.handler.vertx.auth.handler.ClientCredentialsAuthHa
 import io.gravitee.am.gateway.handler.vertx.auth.handler.RedirectAuthHandler;
 import io.gravitee.am.gateway.handler.vertx.auth.provider.ClientAuthenticationProvider;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.ErrorHandlerEndpoint;
-import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.introspection.CheckTokenEndpointHandler;
-import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.introspection.IntrospectionEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationApprovalEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationRequestParseHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.UserApprovalEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.introspection.CheckTokenEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.introspection.IntrospectionEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.revocation.RevocationTokenEndpointHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.token.TokenEndpointHandler;
 import io.gravitee.am.model.Domain;
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java
index 0dd5d41e0c..e2b91ee2b7 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationEndpointHandler.java
@@ -19,12 +19,14 @@ import io.gravitee.am.gateway.handler.oauth2.approval.ApprovalService;
 import io.gravitee.am.gateway.handler.oauth2.client.ClientService;
 import io.gravitee.am.gateway.handler.oauth2.code.AuthorizationCodeService;
 import io.gravitee.am.gateway.handler.oauth2.exception.AccessDeniedException;
+import io.gravitee.am.gateway.handler.oauth2.exception.InteractionRequiredException;
 import io.gravitee.am.gateway.handler.oauth2.exception.InvalidRequestException;
 import io.gravitee.am.gateway.handler.oauth2.exception.ServerErrorException;
 import io.gravitee.am.gateway.handler.oauth2.granter.TokenGranter;
 import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequest;
 import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequestResolver;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
+import io.gravitee.am.gateway.handler.oauth2.utils.OIDCParameters;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.request.AuthorizationRequestFactory;
 import io.gravitee.am.gateway.handler.vertx.utils.UriBuilderRequest;
 import io.gravitee.am.model.Domain;
@@ -36,6 +38,8 @@ import io.vertx.reactivex.ext.web.RoutingContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Arrays;
+
 /**
  * The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.
  * The authorization server MUST first verify the identity of the resource owner.
@@ -91,10 +95,19 @@ public class AuthorizationEndpointHandler extends AbstractAuthorizationEndpointH
                 .subscribe(authorizationRequest -> {
                     try {
                         if (!authorizationRequest.isApproved()) {
-                            // TODO should we put this data inside repository to handle cluster environment ?
-                            context.session().put(OAuth2Constants.AUTHORIZATION_REQUEST, authorizationRequest);
-                            String approvalPage = UriBuilderRequest.resolveProxyRequest(context.request(),"/" + domain.getPath() + "/oauth/confirm_access", null, false, false);
-                            doRedirect(context.response(), approvalPage);
+                            // check prompt value
+                            // 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
+                            // else redirect to consent user approval page
+                            String prompt = authorizationRequest.getRequestParameters().getFirst(OIDCParameters.PROMPT);
+                            if (prompt != null && Arrays.asList(prompt.split("\\s+")).contains("none")) {
+                                context.fail(new InteractionRequiredException());
+                            } else {
+                                // TODO should we put this data inside repository to handle cluster environment ?
+                                context.session().put(OAuth2Constants.AUTHORIZATION_REQUEST, authorizationRequest);
+                                String approvalPage = UriBuilderRequest.resolveProxyRequest(context.request(),"/" + domain.getPath() + "/oauth/confirm_access", null, false, false);
+                                doRedirect(context.response(), approvalPage);
+                            }
                         } else {
                             doRedirect(context.response(), buildRedirectUri(authorizationRequest));
                         }
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationRequestParseHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationRequestParseHandler.java
index c66d7836b4..b1114bebc7 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationRequestParseHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/main/java/io/gravitee/am/gateway/handler/vertx/handler/oauth2/endpoint/authorization/AuthorizationRequestParseHandler.java
@@ -16,13 +16,18 @@
 package io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization;
 
 import io.gravitee.am.gateway.handler.oauth2.exception.InvalidRequestException;
+import io.gravitee.am.gateway.handler.oauth2.exception.LoginRequiredException;
 import io.gravitee.am.gateway.handler.oauth2.exception.UnsupportedResponseTypeException;
 import io.gravitee.am.gateway.handler.oauth2.request.AuthorizationRequest;
 import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
+import io.gravitee.am.gateway.handler.oauth2.utils.OIDCParameters;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.request.AuthorizationRequestFactory;
 import io.vertx.core.Handler;
 import io.vertx.reactivex.ext.web.RoutingContext;
 
+import java.util.Arrays;
+import java.util.List;
+
 /**
  * The authorization server validates the request to ensure that all required parameters are present and valid.
  * If the request is valid, the authorization server authenticates the resource owner and obtains
@@ -53,9 +58,28 @@ public class AuthorizationRequestParseHandler implements Handler<RoutingContext>
             throw new InvalidRequestException("A client id is required");
         }
 
+        // proceed prompt parameter
+        parsePromptParameter(context);
+
         context.next();
     }
 
+    protected final void parsePromptParameter(RoutingContext context) {
+        String prompt = context.request().getParam(OIDCParameters.PROMPT);
+
+        if (prompt != null) {
+            // retrieve prompt values (prompt parameter is a space delimited, case sensitive list of ASCII string values)
+            // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+            List<String> promptValues = Arrays.asList(prompt.split("\\s+"));
+
+            // The Authorization Server MUST NOT display any authentication or consent user interface pages.
+            // An error is returned if an End-User is not already authenticated.
+            if (promptValues.contains("none") && context.user() == null) {
+                throw new LoginRequiredException();
+            }
+        }
+    }
+
     public static AuthorizationRequestParseHandler create() {
         return new AuthorizationRequestParseHandler();
     }


Test patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/AuthorizationEndpointHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/AuthorizationEndpointHandlerTest.java
index 3657def704..e3ab1b2ebb 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/AuthorizationEndpointHandlerTest.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/src/test/java/io/gravitee/am/gateway/handler/vertx/endpoint/AuthorizationEndpointHandlerTest.java
@@ -26,6 +26,7 @@ import io.gravitee.am.gateway.handler.oauth2.utils.OAuth2Constants;
 import io.gravitee.am.gateway.handler.vertx.RxWebTestBase;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationEndpointFailureHandler;
 import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationEndpointHandler;
+import io.gravitee.am.gateway.handler.vertx.handler.oauth2.endpoint.authorization.AuthorizationRequestParseHandler;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
 import io.gravitee.am.repository.oauth2.model.AuthorizationCode;
@@ -79,8 +80,9 @@ public class AuthorizationEndpointHandlerTest  extends RxWebTestBase {
     public void setUp() throws Exception {
         super.setUp();
         SessionHandler sessionHandler = SessionHandler.create(LocalSessionStore.create(vertx));
+        AuthorizationRequestParseHandler authorizationRequestParseHandler = AuthorizationRequestParseHandler.create();
         router.route("/oauth/authorize").handler(sessionHandler);
-        router.route(HttpMethod.GET, "/oauth/authorize").handler(authorizationEndpointHandler);
+        router.route(HttpMethod.GET, "/oauth/authorize").handler(authorizationRequestParseHandler).handler(authorizationEndpointHandler);
         router.route().failureHandler(new AuthorizationEndpointFailureHandler(domain, clientService));
     }
 
@@ -111,7 +113,7 @@ public class AuthorizationEndpointHandlerTest  extends RxWebTestBase {
 
         testRequest(
                 HttpMethod.GET,
-                "/oauth/authorize?client_id=client-id&redirect_uri=http://localhost:9999/callback",
+                "/oauth/authorize?response_type=code&client_id=client-id&redirect_uri=http://localhost:9999/callback",
                 null,
                 resp -> {
                     String location = resp.headers().get("location");
@@ -141,7 +143,7 @@ public class AuthorizationEndpointHandlerTest  extends RxWebTestBase {
 
         testRequest(
                 HttpMethod.GET,
-                "/oauth/authorize?client_id=client-id&redirect_uri=http://localhost:9999/callback",
+                "/oauth/authorize?response_type=code&client_id=client-id&redirect_uri=http://localhost:9999/callback",
                 null,
                 resp -> {
                     String location = resp.headers().get("location");
@@ -172,7 +174,7 @@ public class AuthorizationEndpointHandlerTest  extends RxWebTestBase {
 
         testRequest(
                 HttpMethod.GET,
-                "/oauth/authorize?client_id=client-id&redirect_uri=http://localhost:9999/callback",
+                "/oauth/authorize?response_type=code&client_id=client-id&redirect_uri=http://localhost:9999/callback",
                 null,
                 resp -> {
                     String location = resp.headers().get("location");
@@ -298,4 +300,26 @@ public class AuthorizationEndpointHandlerTest  extends RxWebTestBase {
                 HttpStatusCode.FOUND_302, "Found", null);
     }
 
+    @Test
+    public void shouldNotInvokeAuthorizationEndpoint_noUser_prompt_none() throws Exception {
+        final Client client = new Client();
+        client.setId("client-id");
+        client.setClientId("client-id");
+        client.setRedirectUris(Collections.singletonList("http://localhost:9999/callback"));
+
+        when(domain.getPath()).thenReturn("test");
+        when(clientService.findByClientId("client-id")).thenReturn(Maybe.just(client));
+
+        testRequest(
+                HttpMethod.GET,
+                "/oauth/authorize?response_type=code&client_id=client-id&redirect_uri=http://localhost:9999/callback&prompt=none",
+                null,
+                resp -> {
+                    String location = resp.headers().get("location");
+                    assertNotNull(location);
+                    assertEquals("http://localhost:9999/callback?error=login_required", location);
+                },
+                HttpStatusCode.FOUND_302, "Found", null);
+    }
+
 }