You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by bb...@apache.org on 2017/11/07 18:50:19 UTC
[03/17] nifi-registry git commit: NIFIREG-33 Add LDAP and JWT auth
support
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
new file mode 100644
index 0000000..e0eddba
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
@@ -0,0 +1,158 @@
+/*
+ * 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.registry.web.security;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.web.security.authentication.NiFiAnonymousUserFilter;
+import org.apache.nifi.registry.web.security.authentication.jwt.JwtAuthenticationFilter;
+import org.apache.nifi.registry.web.security.authentication.jwt.JwtAuthenticationProvider;
+import org.apache.nifi.registry.web.security.authentication.x509.X509AuthenticationFilter;
+import org.apache.nifi.registry.web.security.authentication.x509.X509AuthenticationProvider;
+import org.apache.nifi.registry.web.security.authentication.x509.X509CertificateExtractor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+
+/**
+ * NiFi Web Api Spring security
+ */
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class NiFiRegistrySecurityConfig extends WebSecurityConfigurerAdapter {
+ private static final Logger logger = LoggerFactory.getLogger(NiFiRegistrySecurityConfig.class);
+
+ @Autowired private NiFiRegistryProperties properties;
+
+ @Autowired X509CertificateExtractor certificateExtractor;
+ @Autowired X509PrincipalExtractor principalExtractor;
+ @Autowired private X509AuthenticationProvider x509AuthenticationProvider;
+ private X509AuthenticationFilter x509AuthenticationFilter;
+
+ @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
+ private JwtAuthenticationFilter jwtAuthenticationFilter;
+
+// @Autowired private OtpAuthenticationProvider otpAuthenticationProvider;
+// private OtpAuthenticationFilter otpAuthenticationFilter;
+
+ private NiFiAnonymousUserFilter anonymousAuthenticationFilter;
+
+ public NiFiRegistrySecurityConfig() {
+ super(true); // disable defaults
+ }
+
+ @Override
+ public void configure(WebSecurity webSecurity) throws Exception {
+ // ignore the access endpoints for obtaining the access config, access token
+ // granting, and access status for a given user (note: we are not ignoring the
+ // the /access/download-token endpoints)
+ webSecurity
+ .ignoring()
+ .antMatchers("/access", "/access/config", "/access/token");
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .rememberMe().disable()
+ .authorizeRequests()
+ .anyRequest().fullyAuthenticated()
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+
+ // x509
+ http.addFilterBefore(x509AuthenticationFilter(), AnonymousAuthenticationFilter.class);
+
+ // jwt
+ http.addFilterBefore(jwtFilterBean(), AnonymousAuthenticationFilter.class);
+
+ // otp
+ // http.addFilterBefore(otpFilterBean(), AnonymousAuthenticationFilter.class);
+
+ // anonymous
+ http.anonymous().authenticationFilter(anonymousFilter());
+ }
+
+ @Bean
+ @Override
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ // override xxxBean method so the authentication manager is available in app context (necessary for the method level security)
+ return super.authenticationManagerBean();
+ }
+
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+ auth
+ .authenticationProvider(x509AuthenticationProvider)
+ .authenticationProvider(jwtAuthenticationProvider);
+// .authenticationProvider(otpAuthenticationProvider); // TODO OTP support
+ }
+
+ @Bean
+ public JwtAuthenticationFilter jwtFilterBean() throws Exception {
+ if (jwtAuthenticationFilter == null) {
+ jwtAuthenticationFilter = new JwtAuthenticationFilter();
+ jwtAuthenticationFilter.setProperties(properties);
+ jwtAuthenticationFilter.setAuthenticationManager(authenticationManager());
+ }
+ return jwtAuthenticationFilter;
+ }
+
+// @Bean // TODO OtpAuthenticationFilter
+// public OtpAuthenticationFilter otpFilterBean() throws Exception {
+// if (otpAuthenticationFilter == null) {
+// otpAuthenticationFilter = new OtpAuthenticationFilter();
+// otpAuthenticationFilter.setProperties(properties);
+// otpAuthenticationFilter.setAuthenticationManager(authenticationManager());
+// }
+// return otpAuthenticationFilter;
+// }
+
+ @Bean
+ public X509AuthenticationFilter x509AuthenticationFilter() throws Exception {
+ if (x509AuthenticationFilter == null) {
+ x509AuthenticationFilter = new X509AuthenticationFilter();
+ x509AuthenticationFilter.setProperties(properties);
+ x509AuthenticationFilter.setCertificateExtractor(certificateExtractor);
+ x509AuthenticationFilter.setPrincipalExtractor(principalExtractor);
+ x509AuthenticationFilter.setAuthenticationManager(authenticationManager());
+ }
+ return x509AuthenticationFilter;
+ }
+
+ @Bean
+ public NiFiAnonymousUserFilter anonymousFilter() throws Exception {
+ if (anonymousAuthenticationFilter == null) {
+ anonymousAuthenticationFilter = new NiFiAnonymousUserFilter();
+ }
+ return anonymousAuthenticationFilter;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java
deleted file mode 100644
index 33015fc..0000000
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * 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.registry.web.security;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.authorization.user.NiFiUser;
-import org.apache.nifi.registry.authorization.user.NiFiUserUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- *
- */
-public class ProxiedEntitiesUtils {
- private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class);
-
- public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain";
- public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted";
- public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails";
-
- private static final String GT = ">";
- private static final String ESCAPED_GT = "\\\\>";
- private static final String LT = "<";
- private static final String ESCAPED_LT = "\\\\<";
-
- private static final String ANONYMOUS_CHAIN = "<>";
-
- /**
- * Formats the specified DN to be set as a HTTP header using well known conventions.
- *
- * @param dn raw dn
- * @return the dn formatted as an HTTP header
- */
- public static String formatProxyDn(String dn) {
- return LT + sanitizeDn(dn) + GT;
- }
-
- /**
- * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user.
- * <p>
- * Example:
- * <p>
- * Provided DN: {@code jdoe><alopresto} -> {@code <jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe
- *
- * @param rawDn the unsanitized DN
- * @return the sanitized DN
- */
- private static String sanitizeDn(String rawDn) {
- if (StringUtils.isEmpty(rawDn)) {
- return rawDn;
- } else {
- String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
- if (!sanitizedDn.equals(rawDn)) {
- logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]");
- }
- return sanitizedDn;
- }
- }
-
- /**
- * Reconstitutes the original DN from the sanitized version passed in the proxy chain.
- * <p>
- * Example:
- * <p>
- * {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
- *
- * @param sanitizedDn the sanitized DN
- * @return the original DN
- */
- private static String unsanitizeDn(String sanitizedDn) {
- if (StringUtils.isEmpty(sanitizedDn)) {
- return sanitizedDn;
- } else {
- String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
- if (!unsanitizedDn.equals(sanitizedDn)) {
- logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
- }
- return unsanitizedDn;
- }
- }
-
- /**
- * Tokenizes the specified proxy chain.
- *
- * @param rawProxyChain raw chain
- * @return tokenized proxy chain
- */
- public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) {
- final List<String> proxyChain = new ArrayList<>();
- if (!StringUtils.isEmpty(rawProxyChain)) {
- // Split the String on the >< token
- List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><"));
-
- // Unsanitize each DN and collect back
- elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());
-
- // Remove the leading < from the first element
- elements.set(0, elements.get(0).replaceFirst(LT, ""));
-
- // Remove the trailing > from the last element
- int last = elements.size() - 1;
- String lastElement = elements.get(last);
- if (lastElement.endsWith(GT)) {
- elements.set(last, lastElement.substring(0, lastElement.length() - 1));
- }
-
- proxyChain.addAll(elements);
- }
-
- return proxyChain;
- }
-
- /**
- * Builds the proxy chain for the specified user.
- *
- * @param user The current user
- * @return The proxy chain for that user in String form
- */
- public static String buildProxiedEntitiesChainString(final NiFiUser user) {
- // calculate the dn chain
- List<String> proxyChain = NiFiUserUtils.buildProxiedEntitiesChain(user);
- if (proxyChain.isEmpty()) {
- return ANONYMOUS_CHAIN;
- }
- proxyChain = proxyChain.stream().map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
- return StringUtils.join(proxyChain, "");
- }
-
- public static void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
- if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
- response.setHeader(PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString());
- }
- }
-
- public static void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
- if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
- response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage());
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java
deleted file mode 100644
index fad8cc1..0000000
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.registry.web.security;
-
-import org.springframework.security.core.AuthenticationException;
-
-/**
- *
- */
-public class UntrustedProxyException extends AuthenticationException {
-
- public UntrustedProxyException(String msg) {
- super(msg);
- }
-
- public UntrustedProxyException(String msg, Throwable t) {
- super(msg, t);
- }
-
-}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java
new file mode 100644
index 0000000..434d881
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java
@@ -0,0 +1,264 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authentication.LoginIdentityProvider;
+import org.apache.nifi.registry.security.authentication.LoginIdentityProviderConfigurationContext;
+import org.apache.nifi.registry.security.authentication.LoginIdentityProviderLookup;
+import org.apache.nifi.registry.security.authentication.annotation.LoginIdentityProviderContext;
+import org.apache.nifi.registry.extension.ExtensionManager;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.authentication.generated.IdentityProviders;
+import org.apache.nifi.registry.security.authentication.generated.Property;
+import org.apache.nifi.registry.security.authentication.generated.Provider;
+import org.apache.nifi.registry.security.util.XmlUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.xml.sax.SAXException;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class LoginIdentityProviderFactory implements LoginIdentityProviderLookup, DisposableBean {
+
+ private static final Logger logger = LoggerFactory.getLogger(LoginIdentityProviderFactory.class);
+ private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/identity-providers.xsd";
+ private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.security.authentication.generated";
+ private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
+
+ private static JAXBContext initializeJaxbContext() {
+ try {
+ return JAXBContext.newInstance(JAXB_GENERATED_PATH, LoginIdentityProviderFactory.class.getClassLoader());
+ } catch (JAXBException e) {
+ throw new RuntimeException("Unable to create JAXBContext.");
+ }
+ }
+
+ private NiFiRegistryProperties properties;
+ private ExtensionManager extensionManager;
+ private LoginIdentityProvider loginIdentityProvider;
+ private final Map<String, LoginIdentityProvider> loginIdentityProviders = new HashMap<>();
+
+ @Autowired
+ public LoginIdentityProviderFactory(final NiFiRegistryProperties properties, final ExtensionManager extensionManager) {
+ this.properties = properties;
+ this.extensionManager = extensionManager;
+
+ if (this.properties == null) {
+ throw new IllegalStateException("NiFiRegistryProperties cannot be null");
+ }
+
+ if (this.extensionManager == null) {
+ throw new IllegalStateException("ExtensionManager cannot be null");
+ }
+ }
+
+ @Override
+ public LoginIdentityProvider getLoginIdentityProvider(String identifier) {
+ return loginIdentityProviders.get(identifier);
+ }
+
+ @Bean
+ public LoginIdentityProvider getLoginIdentityProvider() throws Exception {
+ if (loginIdentityProvider == null) {
+ // look up the login identity provider to use
+ final String loginIdentityProviderIdentifier = properties.getProperty(NiFiRegistryProperties.SECURITY_IDENTITY_PROVIDER);
+
+ // ensure the login identity provider class name was specified
+ if (StringUtils.isNotBlank(loginIdentityProviderIdentifier)) {
+ final IdentityProviders loginIdentityProviderConfiguration = loadLoginIdentityProvidersConfiguration();
+
+ // create each login identity provider
+ for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) {
+ loginIdentityProviders.put(provider.getIdentifier(), createLoginIdentityProvider(provider.getIdentifier(), provider.getClazz()));
+ }
+
+ // configure each login identity provider
+ for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) {
+ final LoginIdentityProvider instance = loginIdentityProviders.get(provider.getIdentifier());
+ instance.onConfigured(loadLoginIdentityProviderConfiguration(provider));
+ }
+
+ // get the login identity provider instance
+ loginIdentityProvider = getLoginIdentityProvider(loginIdentityProviderIdentifier);
+
+ // ensure it was found
+ if (loginIdentityProvider == null) {
+ throw new Exception(String.format("The specified login identity provider '%s' could not be found.", loginIdentityProviderIdentifier));
+ }
+ }
+ }
+
+ return loginIdentityProvider;
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ if (loginIdentityProviders != null) {
+ loginIdentityProviders.entrySet().stream().forEach(e -> e.getValue().preDestruction());
+ }
+ }
+
+ private IdentityProviders loadLoginIdentityProvidersConfiguration() throws Exception {
+ final File loginIdentityProvidersConfigurationFile = properties.getIdentityProviderConfigurationFile();
+
+ // load the users from the specified file
+ if (loginIdentityProvidersConfigurationFile.exists()) {
+ try {
+ // find the schema
+ final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ final Schema schema = schemaFactory.newSchema(IdentityProviders.class.getResource(LOGIN_IDENTITY_PROVIDERS_XSD));
+
+ // attempt to unmarshal
+ XMLStreamReader xsr = XmlUtils.createSafeReader(new StreamSource(loginIdentityProvidersConfigurationFile));
+ final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
+ unmarshaller.setSchema(schema);
+ final JAXBElement<IdentityProviders> element = unmarshaller.unmarshal(xsr, IdentityProviders.class);
+ return element.getValue();
+ } catch (SAXException | JAXBException e) {
+ throw new Exception("Unable to load the login identity provider configuration file at: " + loginIdentityProvidersConfigurationFile.getAbsolutePath());
+ }
+ } else {
+ throw new Exception("Unable to find the login identity provider configuration file at " + loginIdentityProvidersConfigurationFile.getAbsolutePath());
+ }
+ }
+
+ private LoginIdentityProvider createLoginIdentityProvider(final String identifier, final String loginIdentityProviderClassName) throws Exception {
+ final LoginIdentityProvider instance;
+
+ final ClassLoader classLoader = extensionManager.getExtensionClassLoader(loginIdentityProviderClassName);
+ if (classLoader == null) {
+ throw new IllegalStateException("Extension not found in any of the configured class loaders: " + loginIdentityProviderClassName);
+ }
+
+ // attempt to load the class
+ Class<?> rawLoginIdentityProviderClass = Class.forName(loginIdentityProviderClassName, true, classLoader);
+ Class<? extends LoginIdentityProvider> loginIdentityProviderClass = rawLoginIdentityProviderClass.asSubclass(LoginIdentityProvider.class);
+
+ // otherwise create a new instance
+ Constructor constructor = loginIdentityProviderClass.getConstructor();
+ instance = (LoginIdentityProvider) constructor.newInstance();
+
+ // method injection
+ performMethodInjection(instance, loginIdentityProviderClass);
+
+ // field injection
+ performFieldInjection(instance, loginIdentityProviderClass);
+
+ // call post construction lifecycle event
+ instance.initialize(new StandardLoginIdentityProviderInitializationContext(identifier, this));
+
+ return instance;
+ }
+
+ private LoginIdentityProviderConfigurationContext loadLoginIdentityProviderConfiguration(final Provider provider) {
+ final Map<String, String> providerProperties = new HashMap<>();
+
+ for (final Property property : provider.getProperty()) {
+ providerProperties.put(property.getName(), property.getValue());
+ }
+
+ return new StandardLoginIdentityProviderConfigurationContext(provider.getIdentifier(), providerProperties);
+ }
+
+ private void performMethodInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+
+ for (final Method method : loginIdentityProviderClass.getMethods()) {
+ if (method.isAnnotationPresent(LoginIdentityProviderContext.class)) {
+ // make the method accessible
+ final boolean isAccessible = method.isAccessible();
+ method.setAccessible(true);
+
+ try {
+ final Class<?>[] argumentTypes = method.getParameterTypes();
+
+ // look for setters (single argument)
+ if (argumentTypes.length == 1) {
+ final Class<?> argumentType = argumentTypes[0];
+
+ // look for well known types
+ if (NiFiRegistryProperties.class.isAssignableFrom(argumentType)) {
+ // nifi properties injection
+ method.invoke(instance, properties);
+ }
+ }
+ } finally {
+ method.setAccessible(isAccessible);
+ }
+ }
+ }
+
+ final Class parentClass = loginIdentityProviderClass.getSuperclass();
+ if (parentClass != null && LoginIdentityProvider.class.isAssignableFrom(parentClass)) {
+ performMethodInjection(instance, parentClass);
+ }
+ }
+
+ private void performFieldInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalArgumentException, IllegalAccessException {
+ for (final Field field : loginIdentityProviderClass.getDeclaredFields()) {
+ if (field.isAnnotationPresent(LoginIdentityProviderContext.class)) {
+ // make the method accessible
+ final boolean isAccessible = field.isAccessible();
+ field.setAccessible(true);
+
+ try {
+ // get the type
+ final Class<?> fieldType = field.getType();
+
+ // only consider this field if it isn't set yet
+ if (field.get(instance) == null) {
+ // look for well known types
+ if (NiFiRegistryProperties.class.isAssignableFrom(fieldType)) {
+ // nifi properties injection
+ field.set(instance, properties);
+ }
+ }
+
+ } finally {
+ field.setAccessible(isAccessible);
+ }
+ }
+ }
+
+ final Class parentClass = loginIdentityProviderClass.getSuperclass();
+ if (parentClass != null && LoginIdentityProvider.class.isAssignableFrom(parentClass)) {
+ performFieldInjection(instance, parentClass);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAnonymousUserFilter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAnonymousUserFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAnonymousUserFilter.java
new file mode 100644
index 0000000..a27fd18
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAnonymousUserFilter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails;
+import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser;
+import org.apache.nifi.registry.web.security.authentication.token.NiFiAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class NiFiAnonymousUserFilter extends AnonymousAuthenticationFilter {
+
+ private static final String ANONYMOUS_KEY = "anonymousNifiKey";
+
+ public NiFiAnonymousUserFilter() {
+ super(ANONYMOUS_KEY);
+ }
+
+ @Override
+ protected Authentication createAuthentication(HttpServletRequest request) {
+ return new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS));
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java
new file mode 100644
index 0000000..24af504
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java
@@ -0,0 +1,156 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException;
+import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ *
+ */
+public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
+
+ private static final Logger log = LoggerFactory.getLogger(NiFiAuthenticationFilter.class);
+
+ private AuthenticationManager authenticationManager;
+ private NiFiRegistryProperties properties;
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
+ final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (log.isDebugEnabled()) {
+ log.debug("Checking secure context token: " + authentication);
+ }
+
+ if (requiresAuthentication((HttpServletRequest) request)) {
+ authenticate((HttpServletRequest) request, (HttpServletResponse) response, chain);
+ } else {
+ chain.doFilter(request, response);
+ }
+
+ }
+
+ private boolean requiresAuthentication(final HttpServletRequest request) {
+ return NiFiUserUtils.getNiFiUser() == null;
+ }
+
+ private void authenticate(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
+ String dnChain = null;
+ try {
+ final Authentication authenticationRequest = attemptAuthentication(request);
+ if (authenticationRequest != null) {
+ // log the request attempt - response details will be logged later
+ log.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", authenticationRequest.toString(), request.getMethod(),
+ request.getRequestURL().toString(), request.getRemoteAddr()));
+
+ // attempt to authorize the user
+ final Authentication authenticated = authenticationManager.authenticate(authenticationRequest);
+ successfulAuthorization(request, response, authenticated);
+ }
+
+ // continue
+ chain.doFilter(request, response);
+ } catch (final AuthenticationException ae) {
+ // invalid authentication - always error out
+ unsuccessfulAuthorization(request, response, ae);
+ }
+ }
+
+ /**
+ * Attempt to extract an authentication attempt from the specified request.
+ *
+ * @param request The request
+ * @return The authentication attempt or null if none is found int he request
+ */
+ public abstract Authentication attemptAuthentication(HttpServletRequest request);
+
+ protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
+ log.info("Authentication success for " + authResult);
+
+ SecurityContextHolder.getContext().setAuthentication(authResult);
+ ProxiedEntitiesUtils.successfulAuthorization(request, response, authResult);
+ }
+
+ protected void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException {
+ // populate the response
+ ProxiedEntitiesUtils.unsuccessfulAuthorization(request, response, ae);
+
+ // set the response status
+ response.setContentType("text/plain");
+
+ // write the response message
+ PrintWriter out = response.getWriter();
+
+ // use the type of authentication exception to determine the response code
+ if (ae instanceof InvalidAuthenticationException) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ out.println(ae.getMessage());
+ } else if (ae instanceof UntrustedProxyException) {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ out.println(ae.getMessage());
+ } else if (ae instanceof AuthenticationServiceException) {
+ log.error(String.format("Unable to authorize: %s", ae.getMessage()), ae);
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ out.println(String.format("Unable to authorize: %s", ae.getMessage()));
+ } else {
+ log.error(String.format("Unable to authorize: %s", ae.getMessage()), ae);
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ out.println("Access is denied.");
+ }
+
+ // log the failure
+ log.warn(String.format("Rejecting access to web api: %s", ae.getMessage()));
+
+ // optionally log the stack trace
+ if (log.isDebugEnabled()) {
+ log.debug(StringUtils.EMPTY, ae);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ public void setAuthenticationManager(AuthenticationManager authenticationManager) {
+ this.authenticationManager = authenticationManager;
+ }
+
+ public void setProperties(NiFiRegistryProperties properties) {
+ this.properties = properties;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java
new file mode 100644
index 0000000..5617e0e
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java
@@ -0,0 +1,84 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.ManagedAuthorizer;
+import org.apache.nifi.registry.security.authorization.UserAndGroups;
+import org.apache.nifi.registry.security.authorization.UserGroupProvider;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.util.IdentityMapping;
+import org.apache.nifi.registry.properties.util.IdentityMappingUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AuthenticationProvider;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Base AuthenticationProvider that provides common functionality to mapping identities.
+ */
+public abstract class NiFiAuthenticationProvider implements AuthenticationProvider {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NiFiAuthenticationProvider.class);
+
+ private NiFiRegistryProperties properties;
+ private Authorizer authorizer;
+ private List<IdentityMapping> mappings;
+
+ /**
+ * @param properties the NiFiProperties instance
+ */
+ public NiFiAuthenticationProvider(final NiFiRegistryProperties properties, final Authorizer authorizer) {
+ this.properties = properties;
+ this.mappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
+ this.authorizer = authorizer;
+ }
+
+ public List<IdentityMapping> getMappings() {
+ return mappings;
+ }
+
+ protected String mapIdentity(final String identity) {
+ return IdentityMappingUtil.mapIdentity(identity, mappings);
+ }
+
+ protected Set<String> getUserGroups(final String identity) {
+ return getUserGroups(authorizer, identity);
+ }
+
+ protected static Set<String> getUserGroups(final Authorizer authorizer, final String userIdentity) {
+ if (authorizer instanceof ManagedAuthorizer) {
+ final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
+ final UserGroupProvider userGroupProvider = managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider();
+ final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(userIdentity);
+ final Set<Group> userGroups = userAndGroups.getGroups();
+
+ if (userGroups == null || userGroups.isEmpty()) {
+ return Collections.EMPTY_SET;
+ } else {
+ return userAndGroups.getGroups().stream().map(group -> group.getName()).collect(Collectors.toSet());
+ }
+ } else {
+ return null;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java
new file mode 100644
index 0000000..3da95c5
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java
@@ -0,0 +1,41 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+
+/**
+ * Base class for authentication request tokens in NiFI.
+ */
+public abstract class NiFiAuthenticationRequestToken extends AbstractAuthenticationToken {
+
+ private final String clientAddress;
+
+ /**
+ * @param clientAddress The address of the client making the request
+ */
+ public NiFiAuthenticationRequestToken(final String clientAddress) {
+ super(null);
+ setAuthenticated(false);
+ this.clientAddress = clientAddress;
+ }
+
+ public String getClientAddress() {
+ return clientAddress;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/ProxiedEntitiesUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/ProxiedEntitiesUtils.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/ProxiedEntitiesUtils.java
new file mode 100644
index 0000000..05687f8
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/ProxiedEntitiesUtils.java
@@ -0,0 +1,163 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ *
+ */
+public class ProxiedEntitiesUtils {
+ private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class);
+
+ public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain";
+ public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted";
+ public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails";
+
+ private static final String GT = ">";
+ private static final String ESCAPED_GT = "\\\\>";
+ private static final String LT = "<";
+ private static final String ESCAPED_LT = "\\\\<";
+
+ private static final String ANONYMOUS_CHAIN = "<>";
+
+ /**
+ * Formats the specified DN to be set as a HTTP header using well known conventions.
+ *
+ * @param dn raw dn
+ * @return the dn formatted as an HTTP header
+ */
+ public static String formatProxyDn(String dn) {
+ return LT + sanitizeDn(dn) + GT;
+ }
+
+ /**
+ * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user.
+ * <p>
+ * Example:
+ * <p>
+ * Provided DN: {@code jdoe><alopresto} -> {@code <jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe
+ *
+ * @param rawDn the unsanitized DN
+ * @return the sanitized DN
+ */
+ private static String sanitizeDn(String rawDn) {
+ if (StringUtils.isEmpty(rawDn)) {
+ return rawDn;
+ } else {
+ String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
+ if (!sanitizedDn.equals(rawDn)) {
+ logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]");
+ }
+ return sanitizedDn;
+ }
+ }
+
+ /**
+ * Reconstitutes the original DN from the sanitized version passed in the proxy chain.
+ * <p>
+ * Example:
+ * <p>
+ * {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
+ *
+ * @param sanitizedDn the sanitized DN
+ * @return the original DN
+ */
+ private static String unsanitizeDn(String sanitizedDn) {
+ if (StringUtils.isEmpty(sanitizedDn)) {
+ return sanitizedDn;
+ } else {
+ String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
+ if (!unsanitizedDn.equals(sanitizedDn)) {
+ logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
+ }
+ return unsanitizedDn;
+ }
+ }
+
+ /**
+ * Tokenizes the specified proxy chain.
+ *
+ * @param rawProxyChain raw chain
+ * @return tokenized proxy chain
+ */
+ public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) {
+ final List<String> proxyChain = new ArrayList<>();
+ if (!StringUtils.isEmpty(rawProxyChain)) {
+ // Split the String on the >< token
+ List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><"));
+
+ // Unsanitize each DN and collect back
+ elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());
+
+ // Remove the leading < from the first element
+ elements.set(0, elements.get(0).replaceFirst(LT, ""));
+
+ // Remove the trailing > from the last element
+ int last = elements.size() - 1;
+ String lastElement = elements.get(last);
+ if (lastElement.endsWith(GT)) {
+ elements.set(last, lastElement.substring(0, lastElement.length() - 1));
+ }
+
+ proxyChain.addAll(elements);
+ }
+
+ return proxyChain;
+ }
+
+ /**
+ * Builds the proxy chain for the specified user.
+ *
+ * @param user The current user
+ * @return The proxy chain for that user in String form
+ */
+ public static String buildProxiedEntitiesChainString(final NiFiUser user) {
+ // calculate the dn chain
+ List<String> proxyChain = NiFiUserUtils.buildProxiedEntitiesChain(user);
+ if (proxyChain.isEmpty()) {
+ return ANONYMOUS_CHAIN;
+ }
+ proxyChain = proxyChain.stream().map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
+ return StringUtils.join(proxyChain, "");
+ }
+
+ public static void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
+ if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
+ response.setHeader(PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString());
+ }
+ }
+
+ public static void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
+ if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) {
+ response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java
new file mode 100644
index 0000000..3a9cdd6
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java
@@ -0,0 +1,52 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.apache.nifi.registry.security.authentication.LoginIdentityProviderConfigurationContext;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ *
+ */
+public class StandardLoginIdentityProviderConfigurationContext implements LoginIdentityProviderConfigurationContext {
+
+ private final String identifier;
+ private final Map<String, String> properties;
+
+ public StandardLoginIdentityProviderConfigurationContext(String identifier, Map<String, String> properties) {
+ this.identifier = identifier;
+ this.properties = properties;
+ }
+
+ @Override
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ @Override
+ public Map<String, String> getProperties() {
+ return Collections.unmodifiableMap(properties);
+ }
+
+ @Override
+ public String getProperty(String property) {
+ return properties.get(property);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java
new file mode 100644
index 0000000..e8fba2e
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java
@@ -0,0 +1,45 @@
+/*
+ * 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.registry.web.security.authentication;
+
+import org.apache.nifi.registry.security.authentication.LoginIdentityProviderInitializationContext;
+import org.apache.nifi.registry.security.authentication.LoginIdentityProviderLookup;
+
+/**
+ *
+ */
+public class StandardLoginIdentityProviderInitializationContext implements LoginIdentityProviderInitializationContext {
+
+ private final String identifier;
+ private final LoginIdentityProviderLookup lookup;
+
+ public StandardLoginIdentityProviderInitializationContext(String identifier, final LoginIdentityProviderLookup lookup) {
+ this.identifier = identifier;
+ this.lookup = lookup;
+ }
+
+ @Override
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ @Override
+ public LoginIdentityProviderLookup getAuthorityProviderLookup() {
+ return lookup;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java
new file mode 100644
index 0000000..016e9cb
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.registry.web.security.authentication.exception;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * Thrown if the authentication of a given request is invalid. For instance,
+ * an expired certificate or token.
+ */
+public class InvalidAuthenticationException extends AuthenticationException {
+
+ public InvalidAuthenticationException(String msg) {
+ super(msg);
+ }
+
+ public InvalidAuthenticationException(String msg, Throwable t) {
+ super(msg, t);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java
new file mode 100644
index 0000000..6245ac2
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.registry.web.security.authentication.exception;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ *
+ */
+public class UntrustedProxyException extends AuthenticationException {
+
+ public UntrustedProxyException(String msg) {
+ super(msg);
+ }
+
+ public UntrustedProxyException(String msg, Throwable t) {
+ super(msg, t);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..1e5c194
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java
@@ -0,0 +1,58 @@
+/*
+ * 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.registry.web.security.authentication.jwt;
+
+import org.apache.commons.lang3.StringUtils;
+
+import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ */
+public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
+
+ private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
+
+ public static final String AUTHORIZATION = "Authorization";
+ public static final String BEARER = "Bearer ";
+
+ @Override
+ public Authentication attemptAuthentication(final HttpServletRequest request) {
+ // only support jwt login when running securely
+ if (!request.isSecure()) {
+ return null;
+ }
+
+ // TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource
+
+ // get the principal out of the user token
+ final String authorization = request.getHeader(AUTHORIZATION);
+
+ // if there is no authorization header, we don't know the user
+ if (authorization == null || !StringUtils.startsWith(authorization, BEARER)) {
+ return null;
+ } else {
+ // Extract the Base64 encoded token from the Authorization header
+ final String token = StringUtils.substringAfterLast(authorization, " ");
+ return new JwtAuthenticationRequestToken(token, request.getRemoteAddr());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java
new file mode 100644
index 0000000..2247923
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java
@@ -0,0 +1,69 @@
+/*
+ * 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.registry.web.security.authentication.jwt;
+
+import io.jsonwebtoken.JwtException;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails;
+import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException;
+import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationProvider;
+import org.apache.nifi.registry.web.security.authentication.token.NiFiAuthenticationToken;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ */
+@Component
+public class JwtAuthenticationProvider extends NiFiAuthenticationProvider {
+
+ private final JwtService jwtService;
+
+ @Autowired
+ public JwtAuthenticationProvider(JwtService jwtService, NiFiRegistryProperties nifiProperties, Authorizer authorizer) {
+ super(nifiProperties, authorizer);
+ this.jwtService = jwtService;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ final JwtAuthenticationRequestToken request = (JwtAuthenticationRequestToken) authentication;
+
+ try {
+ final String jwtPrincipal = jwtService.getAuthenticationFromToken(request.getToken());
+ final String mappedIdentity = mapIdentity(jwtPrincipal);
+ final NiFiUser user = new StandardNiFiUser.Builder()
+ .identity(mappedIdentity)
+ .groups(getUserGroups(mappedIdentity))
+ .clientAddress(request.getClientAddress())
+ .build();
+ return new NiFiAuthenticationToken(new NiFiUserDetails(user));
+ } catch (JwtException e) {
+ throw new InvalidAuthenticationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean supports(Class<?> authentication) {
+ return JwtAuthenticationRequestToken.class.isAssignableFrom(authentication);
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java
new file mode 100644
index 0000000..e8af9ff
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java
@@ -0,0 +1,60 @@
+/*
+ * 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.registry.web.security.authentication.jwt;
+
+
+import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationRequestToken;
+
+/**
+ * This is an authentication request with a given JWT token.
+ */
+public class JwtAuthenticationRequestToken extends NiFiAuthenticationRequestToken {
+
+ private final String token;
+
+ /**
+ * Creates a representation of the jwt authentication request for a user.
+ *
+ * @param token The unique token for this user
+ * @param clientAddress the address of the client making the request
+ */
+ public JwtAuthenticationRequestToken(final String token, final String clientAddress) {
+ super(clientAddress);
+ setAuthenticated(false);
+ this.token = token;
+ }
+
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return token;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ @Override
+ public String toString() {
+ return "<JWT token>";
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
new file mode 100644
index 0000000..49c17ea
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
@@ -0,0 +1,160 @@
+/*
+ * 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.registry.web.security.authentication.jwt;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwsHeader;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.SigningKeyResolverAdapter;
+import io.jsonwebtoken.UnsupportedJwtException;
+import org.apache.commons.lang3.StringUtils;
+
+import org.apache.nifi.registry.exception.AdministrationException;
+import org.apache.nifi.registry.security.key.Key;
+import org.apache.nifi.registry.security.key.KeyService;
+import org.apache.nifi.registry.web.security.authentication.token.LoginAuthenticationToken;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
+
+@Service
+public class JwtService {
+
+ private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class);
+
+ private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
+ private static final String KEY_ID_CLAIM = "kid";
+ private static final String USERNAME_CLAIM = "preferred_username";
+
+ private final KeyService keyService;
+
+ public JwtService(final KeyService keyService) {
+ this.keyService = keyService;
+ }
+
+ public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException {
+ // The library representations of the JWT should be kept internal to this service.
+ try {
+ final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken);
+
+ if (jws == null) {
+ throw new JwtException("Unable to parse token");
+ }
+
+ // Additional validation that subject is present
+ if (StringUtils.isEmpty(jws.getBody().getSubject())) {
+ throw new JwtException("No subject available in token");
+ }
+
+ // TODO: Validate issuer against active registry?
+ if (StringUtils.isEmpty(jws.getBody().getIssuer())) {
+ throw new JwtException("No issuer available in token");
+ }
+ return jws.getBody().getSubject();
+ } catch (JwtException e) {
+ logger.debug("The Base64 encoded JWT: " + base64EncodedToken);
+ final String errorMessage = "There was an error validating the JWT";
+ logger.error(errorMessage, e);
+ throw e;
+ }
+ }
+
+ private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException {
+ try {
+ return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
+ @Override
+ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
+ final String identity = claims.getSubject();
+
+ // Get the key based on the key id in the claims
+ final String keyId = claims.get(KEY_ID_CLAIM, String.class);
+ final Key key = keyService.getKey(keyId);
+
+ // Ensure we were able to find a key that was previously issued by this key service for this user
+ if (key == null || key.getKey() == null) {
+ throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]");
+ }
+
+ return key.getKey().getBytes(StandardCharsets.UTF_8);
+ }
+ }).parseClaimsJws(base64EncodedToken);
+ } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
+ // TODO: Exercise all exceptions to ensure none leak key material to logs
+ final String errorMessage = "Unable to validate the access token.";
+ throw new JwtException(errorMessage, e);
+ }
+ }
+
+ /**
+ * Generates a signed JWT token from the provided (Spring Security) login authentication token.
+ *
+ * @param authenticationToken an instance of the Spring Security token after login credentials have been verified against the respective information source
+ * @return a signed JWT containing the user identity and the identity provider, Base64-encoded
+ * @throws JwtException if there is a problem generating the signed token
+ */
+ public String generateSignedToken(final LoginAuthenticationToken authenticationToken) throws JwtException {
+ if (authenticationToken == null) {
+ throw new IllegalArgumentException("Cannot generate a JWT for a null authentication token");
+ }
+
+ // Set expiration from the token
+ final Calendar expiration = Calendar.getInstance();
+ expiration.setTimeInMillis(authenticationToken.getExpiration());
+
+ final Object principal = authenticationToken.getPrincipal();
+ if (principal == null || StringUtils.isEmpty(principal.toString())) {
+ final String errorMessage = "Cannot generate a JWT for a token with an empty identity issued by " + authenticationToken.getIssuer();
+ logger.error(errorMessage);
+ throw new JwtException(errorMessage);
+ }
+
+ // Create a JWT with the specified authentication
+ final String identity = principal.toString();
+ final String username = authenticationToken.getName();
+
+ try {
+ // Get/create the key for this user
+ final Key key = keyService.getOrCreateKey(identity);
+ final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8);
+
+ logger.trace("Generating JWT for " + authenticationToken);
+
+ // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
+ // Build the token
+ return Jwts.builder().setSubject(identity)
+ .setIssuer(authenticationToken.getIssuer())
+ .setAudience(authenticationToken.getIssuer())
+ .claim(USERNAME_CLAIM, username)
+ .claim(KEY_ID_CLAIM, key.getId())
+ .setExpiration(expiration.getTime())
+ .setIssuedAt(Calendar.getInstance().getTime())
+ .signWith(SIGNATURE_ALGORITHM, keyBytes).compact();
+ } catch (NullPointerException | AdministrationException e) {
+ final String errorMessage = "Could not retrieve the signing key for JWT for " + identity;
+ logger.error(errorMessage, e);
+ throw new JwtException(errorMessage, e);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/90f36dd2/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java
new file mode 100644
index 0000000..08f0637
--- /dev/null
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java
@@ -0,0 +1,123 @@
+/*
+ * 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.registry.web.security.authentication.token;
+
+import org.apache.nifi.registry.security.util.CertificateUtils;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+/**
+ * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token.
+ */
+public class LoginAuthenticationToken extends AbstractAuthenticationToken {
+
+ private final String identity;
+ private final String username;
+ private final long expiration;
+ private final String issuer;
+
+ /**
+ * Creates a representation of the authentication token for a user.
+ *
+ * @param identity The unique identifier for this user
+ * @param expiration The relative time to expiration in milliseconds
+ * @param issuer The IdentityProvider implementation that generated this token
+ */
+ public LoginAuthenticationToken(final String identity, final long expiration, final String issuer) {
+ this(identity, null, expiration, issuer);
+ }
+
+ /**
+ * Creates a representation of the authentication token for a user.
+ *
+ * @param identity The unique identifier for this user (cannot be null or empty)
+ * @param username The preferred username for this user
+ * @param expiration The relative time to expiration in milliseconds
+ * @param issuer The IdentityProvider implementation that generated this token
+ */
+ public LoginAuthenticationToken(final String identity, final String username, final long expiration, final String issuer) {
+ super(null);
+ setAuthenticated(true);
+ this.identity = identity;
+ this.username = username;
+ this.issuer = issuer;
+ Calendar now = Calendar.getInstance();
+ this.expiration = now.getTimeInMillis() + expiration;
+ }
+
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return identity;
+ }
+
+ /**
+ * Returns the expiration instant in milliseconds. This value is an absolute point in time (i.e. Nov
+ * 16, 2015 11:30:00.000 GMT), not a relative time (i.e. 60 minutes). It is calculated by adding the
+ * relative expiration from the constructor to the timestamp at object creation.
+ *
+ * @return the expiration in millis
+ */
+ public long getExpiration() {
+ return expiration;
+ }
+
+ public String getIssuer() {
+ return issuer;
+ }
+
+ @Override
+ public String getName() {
+ if (username == null) {
+ // if the username is a DN this will extract the username or CN... if not will return what was passed
+ return CertificateUtils.extractUsername(identity);
+ } else {
+ return username;
+ }
+ }
+
+ @Override
+ public String toString() {
+ Calendar expirationTime = Calendar.getInstance();
+ expirationTime.setTimeInMillis(getExpiration());
+ long remainingTime = expirationTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis();
+
+ SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS");
+ dateFormat.setTimeZone(expirationTime.getTimeZone());
+ String expirationTimeString = dateFormat.format(expirationTime.getTime());
+
+ return new StringBuilder("LoginAuthenticationToken for ")
+ .append(getName())
+ .append(" issued by ")
+ .append(getIssuer())
+ .append(" expiring at ")
+ .append(expirationTimeString)
+ .append(" [")
+ .append(getExpiration())
+ .append(" ms, ")
+ .append(remainingTime)
+ .append(" ms remaining]")
+ .toString();
+ }
+
+}