[ https://issues.apache.org/jira/browse/NIFI-4210?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16111503#comment-16111503
]
ASF GitHub Bot commented on NIFI-4210:
--------------------------------------
Github user mcgilman commented on a diff in the pull request:
https://github.com/apache/nifi/pull/2047#discussion_r130961276
--- Diff: nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java
---
@@ -0,0 +1,339 @@
+/*
+ * 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.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.util.DefaultResourceRetriever;
+import com.nimbusds.jose.util.ResourceRetriever;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenErrorResponse;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
+import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
+import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
+import net.minidev.json.JSONObject;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.security.jwt.JwtService;
+import org.apache.nifi.web.security.token.LoginAuthenticationToken;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static com.nimbusds.openid.connect.sdk.claims.UserInfo.EMAIL_CLAIM_NAME;
+
+/**
+ * OidcProvider for managing the OpenId Connect Authorization flow.
+ */
+public class StandardOidcIdentityProvider implements OidcIdentityProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProvider.class);
+
+ private NiFiProperties properties;
+ private JwtService jwtService;
+ private OIDCProviderMetadata oidcProviderMetadata;
+ private int oidcConnectTimeout;
+ private int oidcReadTimeout;
+ private IDTokenValidator tokenValidator;
+ private ClientID clientId;
+ private Secret clientSecret;
+
+ /**
+ * Creates a new StandardOidcIdentityProvider.
+ *
+ * @param jwtService jwt service
+ * @param properties properties
+ */
+ public StandardOidcIdentityProvider(final JwtService jwtService, final NiFiProperties
properties) {
+ this.properties = properties;
+ this.jwtService = jwtService;
+
+ // attempt to process the oidc configuration if configured
+ if (properties.isOidcEnabled()) {
+ // oidc connect timeout
+ final String rawConnectTimeout = properties.getOidcConnectTimeout();
+ try {
+ oidcConnectTimeout = (int) FormatUtils.getTimeDuration(rawConnectTimeout,
TimeUnit.MILLISECONDS);
+ } catch (final Exception e) {
+ logger.warn("Failed to parse value of property '{}' as a valid time period.
Value was '{}'. Ignoring this value and using the default value of '{}'",
+ NiFiProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT, rawConnectTimeout,
NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
+ oidcConnectTimeout = (int) FormatUtils.getTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT,
TimeUnit.MILLISECONDS);
+ }
+
+ // oidc read timeout
+ final String rawReadTimeout = properties.getOidcReadTimeout();
+ try {
+ oidcReadTimeout = (int) FormatUtils.getTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
+ } catch (final Exception e) {
+ logger.warn("Failed to parse value of property '{}' as a valid time period.
Value was '{}'. Ignoring this value and using the default value of '{}'",
+ NiFiProperties.SECURITY_USER_OIDC_READ_TIMEOUT, rawReadTimeout,
NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
+ oidcReadTimeout = (int) FormatUtils.getTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT,
TimeUnit.MILLISECONDS);
+ }
+
+ // client id
+ final String rawClientId = properties.getOidcClientId();
+ if (StringUtils.isBlank(rawClientId)) {
+ throw new RuntimeException("Client ID is required when configuring an
OIDC Provider.");
+ }
+ clientId = new ClientID(rawClientId);
+
+ // client secret
+ final String rawClientSecret = properties.getOidcClientSecret();
+ if (StringUtils.isBlank(rawClientSecret)) {
+ throw new RuntimeException("Client secret is required when configured
an OIDC Provider.");
+ }
+ clientSecret = new Secret(rawClientSecret);
+
+ try {
+ // retrieve the oidc provider metadata
+ oidcProviderMetadata = retrieveOidcProviderMetadata(properties.getOidcDiscoveryUrl());
+ } catch (IOException | ParseException e) {
+ throw new RuntimeException("Unable to retrieve OpenId Connect Provider
metadata from: " + properties.getOidcDiscoveryUrl(), e);
+ }
+
+ // ensure the oidc provider supports basic or post client auth
+ final List<ClientAuthenticationMethod> clientAuthenticationMethods
= oidcProviderMetadata.getTokenEndpointAuthMethods();
+ if (clientAuthenticationMethods == null
+ || (!clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+ && !clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)))
{
+
+ throw new RuntimeException(String.format("OpenId Connect Provider does
not support %s or %s",
+ ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(),
+ ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()));
+ }
+
+ // extract the supported json web signature algorithms
+ final List<JWSAlgorithm> allowedAlgorithms = oidcProviderMetadata.getIDTokenJWSAlgs();
+ if (allowedAlgorithms == null || allowedAlgorithms.isEmpty()) {
+ throw new RuntimeException("The OpenId Connect Provider does not support
any JWS algorithms.");
+ }
+
+ try {
+ // get the preferred json web signature algorithm
+ final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm();
+
+ final JWSAlgorithm preferredJwsAlgorithm;
+ if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
+ preferredJwsAlgorithm = oidcProviderMetadata.getIDTokenJWSAlgs().get(0);
+ } else {
+ if ("none".equals(rawPreferredJwsAlgorithm)) {
+ preferredJwsAlgorithm = null;
+ } else {
+ preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
+ }
+ }
+
+ if (preferredJwsAlgorithm == null) {
+ tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(),
clientId);
+ } else if (JWSAlgorithm.HS256.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS384.equals(preferredJwsAlgorithm)
|| JWSAlgorithm.HS512.equals(preferredJwsAlgorithm)) {
+ tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(),
clientId, preferredJwsAlgorithm, clientSecret);
+ } else {
+ final ResourceRetriever retriever = new DefaultResourceRetriever(oidcConnectTimeout,
oidcReadTimeout);
+ tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(),
clientId, preferredJwsAlgorithm, oidcProviderMetadata.getJWKSetURI().toURL(), retriever);
+ }
+ } catch (final Exception e) {
+ throw new RuntimeException("Unable to create the ID token validator for
the configured OpenId Connect Provider: " + e.getMessage(), e);
+ }
+ }
+ }
+
+ private OIDCProviderMetadata retrieveOidcProviderMetadata(final String discoveryUri)
throws IOException, ParseException {
+ final URL url = new URL(discoveryUri);
+ final HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.GET, url);
+ httpRequest.setConnectTimeout(oidcConnectTimeout);
+ httpRequest.setReadTimeout(oidcReadTimeout);
+
+ final HTTPResponse httpResponse = httpRequest.send();
+
+ if (httpResponse.getStatusCode() != 200) {
+ throw new IOException("Unable to download OpenId Connect Provider metadata
from " + url + ": Status code " + httpResponse.getStatusCode());
+ }
+
+ final JSONObject jsonObject = httpResponse.getContentAsJSONObject();
+ return OIDCProviderMetadata.parse(jsonObject);
+ }
+
+ @Override
+ public boolean isOidcEnabled() {
+ return properties.isOidcEnabled();
+ }
+
+ @Override
+ public URI getAuthorizationEndpoint() {
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+
+ return oidcProviderMetadata.getAuthorizationEndpointURI();
+ }
+
+ @Override
+ public Scope getScope() {
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+
+ final Scope scope = new Scope("openid");
+
+ // if this provider supports email scope, include it to prevent a subsequent
request to the user endpoint
+ if (oidcProviderMetadata.getScopes() != null && oidcProviderMetadata.getScopes().contains(OIDCScopeValue.EMAIL))
{
+ scope.add("email");
+ }
+ return scope;
+ }
+
+ @Override
+ 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 {
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+
+ final ClientAuthentication clientAuthentication;
+ if (oidcProviderMetadata.getTokenEndpointAuthMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_POST))
{
+ clientAuthentication = new ClientSecretPost(clientId, clientSecret);
+ } else {
+ clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
+ }
+
+ try {
+ // build the token request
+ final TokenRequest request = new TokenRequest(oidcProviderMetadata.getTokenEndpointURI(),
clientAuthentication, authorizationGrant, getScope());
+ final HTTPRequest tokenHttpRequest = request.toHTTPRequest();
+ tokenHttpRequest.setConnectTimeout(oidcConnectTimeout);
+ tokenHttpRequest.setReadTimeout(oidcReadTimeout);
+
+ // get the token response
+ final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send());
+
+ if (response.indicatesSuccess()) {
+ final OIDCTokenResponse oidcTokenResponse = (OIDCTokenResponse) response;
+ final OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens();
+ final JWT oidcJwt = oidcTokens.getIDToken();
+
+ // validate the token - no nonce required for authorization code flow
+ final IDTokenClaimsSet claimsSet = tokenValidator.validate(oidcJwt, null);
+
+ // attempt to extract the email from the id token if possible
+ String email = claimsSet.getStringClaim(EMAIL_CLAIM_NAME);
+ if (email == null) {
--- End diff --
Got it.
> Add OpenId Connect support for authenticating users
> ---------------------------------------------------
>
> Key: NIFI-4210
> URL: https://issues.apache.org/jira/browse/NIFI-4210
> Project: Apache NiFi
> Issue Type: Improvement
> Components: Core Framework, Core UI
> Reporter: Matt Gilman
> Assignee: Matt Gilman
>
> Add support for authenticating users with the OpenId Connection specification. Evaluate
whether a new extension point is necessary to allow for a given provider to supply custom
code for instance to implement custom token validation.
--
This message was sent by Atlassian JIRA
(v6.4.14#64029)
|