mtien-apache commented on a change in pull request #4593:
URL: https://github.com/apache/nifi/pull/4593#discussion_r511174832



##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
##########
@@ -329,24 +359,221 @@ public Response oidcExchange(@Context HttpServletRequest 
httpServletRequest, @Co
     )
     public void oidcLogout(@Context HttpServletRequest httpServletRequest, 
@Context HttpServletResponse httpServletResponse) throws Exception {
         if (!httpServletRequest.isSecure()) {
-            throw new IllegalStateException("User authentication/authorization 
is only supported when running over HTTPS.");
+            throw new IllegalStateException(AUTHENTICATION_NOT_ENABLED_MSG);
         }
 
         if (!oidcService.isOidcEnabled()) {
-            throw new IllegalStateException("OpenId Connect is not 
configured.");
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
         }
 
-        URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
-        String postLogoutRedirectUri = generateResourceUri("..", "nifi", 
"logout-complete");
+        // Get the oidc discovery url
+        String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
+
+        // Determine the logout method
+        String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
+
+        switch (logoutMethod) {
+            case REVOKE_ACCESS_TOKEN_LOGOUT:
+            case ID_TOKEN_LOGOUT:
+                // Make a request to the IdP
+                URI authorizationURI = 
oidcRequestAuthorizationCode(httpServletResponse, getOidcLogoutCallback());
+                httpServletResponse.sendRedirect(authorizationURI.toString());
+                break;
+            case STANDARD_LOGOUT:
+            default:
+                // Get the OIDC end session endpoint
+                URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
+                String postLogoutRedirectUri = generateResourceUri( "..", 
"nifi", "logout-complete");
+
+                if (endSessionEndpoint == null) {
+                    httpServletResponse.sendRedirect(postLogoutRedirectUri);
+                } else {
+                    URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
+                            .queryParam("post_logout_redirect_uri", 
postLogoutRedirectUri)
+                            .build();
+                    httpServletResponse.sendRedirect(logoutUri.toString());
+                }
+                break;
+        }
+    }
 
-        if (endSessionEndpoint == null) {
-            // handle the case, where the OpenID Provider does not have an end 
session endpoint
-            httpServletResponse.sendRedirect(postLogoutRedirectUri);
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.WILDCARD)
+    @Path("oidc/logoutCallback")
+    @ApiOperation(
+            value = "Redirect/callback URI for processing the result of the 
OpenId Connect logout sequence.",
+            notes = NON_GUARANTEED_ENDPOINT
+    )
+    public void oidcLogoutCallback(@Context HttpServletRequest 
httpServletRequest, @Context HttpServletResponse httpServletResponse) throws 
Exception {
+        // only consider user specific access over https
+        if (!httpServletRequest.isSecure()) {
+            forwardToMessagePage(httpServletRequest, httpServletResponse, 
AUTHENTICATION_NOT_ENABLED_MSG);
+            return;
+        }
+
+        // ensure oidc is enabled
+        if (!oidcService.isOidcEnabled()) {
+            forwardToMessagePage(httpServletRequest, httpServletResponse, 
OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
+            return;
+        }
+
+        final String oidcRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+        if (oidcRequestIdentifier == null) {
+            forwardToMessagePage(httpServletRequest, httpServletResponse, "The 
login request identifier was not found in the request. Unable to continue.");
+            return;
+        }
+
+        final com.nimbusds.openid.connect.sdk.AuthenticationResponse 
oidcResponse;
+        try {
+            oidcResponse = AuthenticationResponseParser.parse(getRequestUri());
+        } catch (final ParseException e) {
+            logger.error("Unable to parse the redirect URI from the OpenId 
Connect Provider. Unable to continue logout process.");
+
+            // remove the oidc request cookie
+            removeOidcRequestCookie(httpServletResponse);
+
+            // forward to the error page
+            forwardToMessagePage(httpServletRequest, httpServletResponse, 
"Unable to parse the redirect URI from the OpenId Connect Provider. Unable to 
continue logout process.");
+            return;
+        }
+
+        if (oidcResponse.indicatesSuccess()) {
+            final AuthenticationSuccessResponse successfulOidcResponse = 
(AuthenticationSuccessResponse) oidcResponse;
+
+            // confirm state
+            final State state = successfulOidcResponse.getState();
+            if (state == null || 
!oidcService.isStateValid(oidcRequestIdentifier, state)) {
+                logger.error("The state value returned by the OpenId Connect 
Provider does not match the stored state. Unable to continue login process.");
+
+                // remove the oidc request cookie
+                removeOidcRequestCookie(httpServletResponse);
+
+                // forward to the error page
+                forwardToMessagePage(httpServletRequest, httpServletResponse, 
"Purposed state does not match the stored state. Unable to continue login 
process.");
+                return;
+            }
+
+            // Get the oidc discovery url
+            String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
+
+            // Determine which logout method to use
+            String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
+
+            // Get the authorization code and grant
+            final AuthorizationCode authorizationCode = 
successfulOidcResponse.getAuthorizationCode();
+            final AuthorizationGrant authorizationGrant = new 
AuthorizationCodeGrant(authorizationCode, URI.create(getOidcLogoutCallback()));
+
+            switch (logoutMethod) {
+                case REVOKE_ACCESS_TOKEN_LOGOUT:
+                    // Use the Revocation endpoint + access token
+                    final String accessToken;
+                    try {
+                        // Return the access token
+                        accessToken = 
oidcService.exchangeAuthorizationCodeForAccessToken(authorizationGrant);
+                    } catch (final Exception e) {
+                        logger.error("Unable to exchange authorization for the 
Access token: " + e.getMessage(), e);
+
+                        // Remove the oidc request cookie
+                        removeOidcRequestCookie(httpServletResponse);
+
+                        // Forward to the error page
+                        forwardToMessagePage(httpServletRequest, 
httpServletResponse, "Unable to exchange " +
+                                "authorization for ID token: " + 
e.getMessage());
+                        return;
+                    }
+
+                    // Build the revoke URI and send the POST request
+                    URI revokeEndpoint = getRevokeEndpoint();
+
+                    if (revokeEndpoint != null) {
+                        try {
+                            int timeout = 30_000; // ms
+                            RequestConfig config = RequestConfig.custom()
+                                    .setConnectTimeout(timeout)
+                                    .setConnectionRequestTimeout(timeout)
+                                    .setSocketTimeout(timeout)
+                                    .build();
+
+                            CloseableHttpClient httpClient = HttpClientBuilder
+                                    .create()
+                                    .setDefaultRequestConfig(config)
+                                    .build();
+                            HttpPost httpPost = new HttpPost(revokeEndpoint);
+
+                            List<NameValuePair> params = new ArrayList<>();
+                            params.add(new BasicNameValuePair("token", 
accessToken));
+                            httpPost.setEntity(new 
UrlEncodedFormEntity(params));
+
+                            try (CloseableHttpResponse response = 
httpClient.execute(httpPost)) {
+                                httpClient.close();
+
+                                if (response.getStatusLine().getStatusCode() 
== HTTPResponse.SC_OK) {
+                                    // Redirect to logout page
+                                    logger.debug("You are logged out of the 
OpenId Connect Provider.");
+                                    String postLogoutRedirectUri = 
generateResourceUri("..", "nifi", "logout-complete");
+                                    
httpServletResponse.sendRedirect(postLogoutRedirectUri);
+                                } else {
+                                    logger.error("There was an error logging 
out of the OpenId Connect Provider. " +
+                                            "Response status: " + 
response.getStatusLine().getStatusCode());
+                                }
+                            }
+
+                        } catch (final IOException e) {
+                            logger.error("There was an error logging out of 
the OpenId Connect Provider: "
+                                    + e.getMessage(), e);
+
+                            // Remove the oidc request cookie
+                            removeOidcRequestCookie(httpServletResponse);
+
+                            // Forward to the error page
+                            forwardToMessagePage(httpServletRequest, 
httpServletResponse,
+                                    "There was an error logging out of the 
OpenId Connect Provider: "
+                                            + e.getMessage());
+                        }
+                    }
+                    break;
+                case ID_TOKEN_LOGOUT:
+                    // Use the end session endpoint + ID Token
+                    final String idToken;
+                    try {
+                        // Return the ID Token
+                        idToken = 
oidcService.exchangeAuthorizationCodeForIdToken(authorizationGrant);
+                    } catch (final Exception e) {
+                        logger.error("Unable to exchange authorization for the 
Access token: " + e.getMessage(), e);
+
+                        // Remove the oidc request cookie
+                        removeOidcRequestCookie(httpServletResponse);
+
+                        // Forward to the error page
+                        forwardToMessagePage(httpServletRequest, 
httpServletResponse, "Unable to exchange authorization for ID token: " + 
e.getMessage());

Review comment:
       Fixed.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java
##########
@@ -295,37 +312,101 @@ public ClientID getClientId() {
         if (!isOidcEnabled()) {
             throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
         }
-
         return clientId;
     }
 
     @Override
-    public String exchangeAuthorizationCode(final AuthorizationGrant 
authorizationGrant) throws IOException {
+    public LoginAuthenticationToken 
exchangeAuthorizationCodeforLoginAuthenticationToken(final AuthorizationGrant 
authorizationGrant) throws IOException {
         // Check if OIDC is enabled
         if (!isOidcEnabled()) {
             throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
         }
 
-        // Build ClientAuthentication
-        final ClientAuthentication clientAuthentication = 
createClientAuthentication();
-
         try {
-            // Build the token request
-            final HTTPRequest tokenHttpRequest = 
createTokenHTTPRequest(authorizationGrant, clientAuthentication);
-            return authorizeClient(tokenHttpRequest);
+            // Authenticate and authorize the client request
+            final TokenResponse response = authorizeClient(authorizationGrant);
+
+            // Convert response to Login Authentication Token
+            // We only want to do this for login
+            return 
convertOIDCTokenToLoginAuthenticationToken((OIDCTokenResponse) response);
 
         } catch (final ParseException | JOSEException | BadJOSEException | 
java.text.ParseException e) {
             throw new RuntimeException("Unable to parse the response from the 
Token request: " + e.getMessage());
         }
     }
 
-    private String authorizeClient(HTTPRequest tokenHttpRequest) throws 
ParseException, IOException, BadJOSEException, JOSEException, 
java.text.ParseException {
+    @Override
+    public String exchangeAuthorizationCodeForAccessToken(final 
AuthorizationGrant authorizationGrant) throws Exception {
+        // Check if OIDC is enabled
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        try {
+            // Authenticate and authorize the client request
+            final TokenResponse response = authorizeClient(authorizationGrant);
+            return getAccessTokenString((OIDCTokenResponse) response);
+
+        } catch (final ParseException | IOException | java.text.ParseException 
| InvalidHashException e) {
+            throw new RuntimeException("Unable to parse the response from the 
Token request: " + e.getMessage());

Review comment:
       Fixed.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy
##########
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.security.oidc
+
+
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
+import com.nimbusds.oauth2.sdk.id.Issuer
+import com.nimbusds.openid.connect.sdk.SubjectType
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.SignatureAlgorithm
+import org.apache.nifi.admin.service.KeyService
+import org.apache.nifi.key.Key
+import org.apache.nifi.util.NiFiProperties
+import org.apache.nifi.web.security.jwt.JwtService
+import org.apache.nifi.web.security.token.LoginAuthenticationToken
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.util.concurrent.TimeUnit
+
+@RunWith(JUnit4.class)
+class OidcServiceGroovyTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(OidcServiceGroovyTest.class)
+
+    private static final Key SIGNING_KEY = new Key(id: 1, identity: 
"signingKey", key: "mock-signing-key-value")
+    private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
+            isOidcEnabled                 : true,
+            getOidcDiscoveryUrl           : "https://localhost/oidc";,
+            isLoginIdentityProviderEnabled: false,
+            isKnoxSsoEnabled              : false,
+            getOidcConnectTimeout         : "1000",
+            getOidcReadTimeout            : "1000",
+            getOidcClientId               : "expected_client_id",
+            getOidcClientSecret           : "expected_client_secret",
+            getOidcClaimIdentifyingUser   : "username",
+            getOidcPreferredJwsAlgorithm  : ""
+    ]
+
+    // Mock collaborators
+    private static NiFiProperties mockNiFiProperties
+    private static JwtService mockJwtService = [:] as JwtService
+    private static StandardOidcIdentityProvider soip
+
+    private static final String MOCK_REQUEST_IDENTIFIER = 
"mock-request-identifier"
+    private static final String MOCK_JWT = 
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZS" +

Review comment:
       @exceptionfactory Although generating the value is a reasonable, the 
hard-coded string is sufficient for this purpose.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to