Repository: nifi-registry Updated Branches: refs/heads/master 63ddf4129 -> ef8ba127c
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/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 index 49c17ea..4401a15 100644 --- 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 @@ -28,17 +28,20 @@ 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.authentication.AuthenticationResponse; 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.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.concurrent.TimeUnit; +// TODO, look into replacing this JwtService service with Apache Licensed JJWT library @Service public class JwtService { @@ -50,6 +53,7 @@ public class JwtService { private final KeyService keyService; + @Autowired public JwtService(final KeyService keyService) { this.keyService = keyService; } @@ -68,7 +72,7 @@ public class JwtService { throw new JwtException("No subject available in token"); } - // TODO: Validate issuer against active registry? + // TODO: Validate issuer against active IdentityProvider? if (StringUtils.isEmpty(jws.getBody().getIssuer())) { throw new JwtException("No issuer available in token"); } @@ -110,46 +114,48 @@ public class JwtService { /** * 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 + * @param authenticationResponse 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) { + public String generateSignedToken(final AuthenticationResponse authenticationResponse) throws JwtException { + if (authenticationResponse == 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 Calendar now = Calendar.getInstance(); + long expirationMillisRelativeToNow = validateTokenExpiration(authenticationResponse.getExpiration(), authenticationResponse.getIdentity()); + long expirationMillis = now.getTimeInMillis() + expirationMillisRelativeToNow; + final Calendar expiration = new Calendar.Builder().setInstant(expirationMillis).build(); - final Object principal = authenticationToken.getPrincipal(); + final Object principal = authenticationResponse.getIdentity(); 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(); + final String errorMessage = "Cannot generate a JWT for a token with an empty identity issued by " + authenticationResponse.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(); + final String username = authenticationResponse.getUsername(); 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); + logger.trace("Generating JWT for " + describe(authenticationResponse)); // 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()) + .setIssuer(authenticationResponse.getIssuer()) + .setAudience(authenticationResponse.getIssuer()) .claim(USERNAME_CLAIM, username) .claim(KEY_ID_CLAIM, key.getId()) + .setIssuedAt(now.getTime()) .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; @@ -157,4 +163,44 @@ public class JwtService { throw new JwtException(errorMessage, e); } } + + private long validateTokenExpiration(long proposedTokenExpiration, String identity) { + final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); + + if (proposedTokenExpiration > maxExpiration) { + logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", maxExpiration, + proposedTokenExpiration, identity)); + proposedTokenExpiration = maxExpiration; + } else if (proposedTokenExpiration < minExpiration) { + logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", minExpiration, + proposedTokenExpiration, identity)); + proposedTokenExpiration = minExpiration; + } + + return proposedTokenExpiration; + } + + private static String describe(AuthenticationResponse authenticationResponse) { + Calendar expirationTime = Calendar.getInstance(); + expirationTime.setTimeInMillis(authenticationResponse.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(authenticationResponse.getUsername()) + .append(" issued by ") + .append(authenticationResponse.getIssuer()) + .append(" expiring at ") + .append(expirationTimeString) + .append(" [") + .append(authenticationResponse.getExpiration()) + .append(" ms, ") + .append(remainingTime) + .append(" ms remaining]") + .toString(); + } } http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/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 deleted file mode 100644 index 08f0637..0000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java +++ /dev/null @@ -1,123 +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.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(); - } - -} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/NiFiAuthenticationToken.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/NiFiAuthenticationToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/NiFiAuthenticationToken.java deleted file mode 100644 index 19e56c5..0000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/NiFiAuthenticationToken.java +++ /dev/null @@ -1,55 +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.authentication.token; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.userdetails.UserDetails; - -/** - * An authentication token that represents an Authenticated and Authorized user of the NiFi Apis. The authorities are based off the specified UserDetails. - */ -public class NiFiAuthenticationToken extends AbstractAuthenticationToken { - - final UserDetails nifiUserDetails; - - public NiFiAuthenticationToken(final UserDetails nifiUserDetails) { - super(nifiUserDetails.getAuthorities()); - super.setAuthenticated(true); - setDetails(nifiUserDetails); - this.nifiUserDetails = nifiUserDetails; - } - - @Override - public Object getCredentials() { - return nifiUserDetails.getPassword(); - } - - @Override - public Object getPrincipal() { - return nifiUserDetails; - } - - @Override - public final void setAuthenticated(boolean authenticated) { - throw new IllegalArgumentException("Cannot change the authenticated state."); - } - - @Override - public String toString() { - return nifiUserDetails.getUsername(); - } -} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java deleted file mode 100644 index fa0fce2..0000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java +++ /dev/null @@ -1,64 +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.authentication.x509; - -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationFilter; -import org.apache.nifi.registry.web.security.authentication.ProxiedEntitiesUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; - -import javax.servlet.http.HttpServletRequest; -import java.security.cert.X509Certificate; - -/** - * Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate. - */ -public class X509AuthenticationFilter extends NiFiAuthenticationFilter { - - private static final Logger logger = LoggerFactory.getLogger(X509AuthenticationFilter.class); - - private X509CertificateExtractor certificateExtractor; - private X509PrincipalExtractor principalExtractor; - - @Override - public Authentication attemptAuthentication(final HttpServletRequest request) { - // only suppport x509 login when running securely - if (!request.isSecure()) { - return null; - } - - // look for a client certificate - final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(request); - if (certificates == null) { - return null; - } - - return new X509AuthenticationRequestToken(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN), principalExtractor, certificates, request.getRemoteAddr()); - } - - /* setters */ - public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { - this.certificateExtractor = certificateExtractor; - } - - public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { - this.principalExtractor = principalExtractor; - } - -} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java deleted file mode 100644 index 3e935a2..0000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java +++ /dev/null @@ -1,166 +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.authentication.x509; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; -import org.apache.nifi.registry.security.authorization.Authorizer; -import org.apache.nifi.registry.security.authorization.RequestAction; -import org.apache.nifi.registry.security.authorization.Resource; -import org.apache.nifi.registry.security.authorization.UserContextKeys; -import org.apache.nifi.registry.security.authorization.resource.Authorizable; -import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; -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.response.AuthenticationResponse; -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.ProxiedEntitiesUtils; -import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException; -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; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Set; - -@Component -public class X509AuthenticationProvider extends NiFiAuthenticationProvider { - - private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { - @Override - public Authorizable getParentAuthorizable() { - return null; - } - - @Override - public Resource getResource() { - return ResourceFactory.getProxyResource(); - } - }; - - private X509IdentityProvider certificateIdentityProvider; - private Authorizer authorizer; - - @Autowired - public X509AuthenticationProvider( - final X509IdentityProvider certificateIdentityProvider, - final Authorizer authorizer, - final NiFiRegistryProperties properties) { - super(properties, authorizer); - this.certificateIdentityProvider = certificateIdentityProvider; - this.authorizer = authorizer; - } - - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - final X509AuthenticationRequestToken request = (X509AuthenticationRequestToken) authentication; - - // attempt to authenticate if certificates were found - final AuthenticationResponse authenticationResponse; - try { - authenticationResponse = certificateIdentityProvider.authenticate(request.getCertificates()); - } catch (final IllegalArgumentException iae) { - throw new InvalidAuthenticationException(iae.getMessage(), iae); - } - - if (StringUtils.isBlank(request.getProxiedEntitiesChain())) { - final String mappedIdentity = mapIdentity(authenticationResponse.getIdentity()); - return new NiFiAuthenticationToken(new NiFiUserDetails( - new StandardNiFiUser.Builder() - .identity(mappedIdentity) - .groups(getUserGroups(mappedIdentity)) - .clientAddress(request.getClientAddress()) - .build())); - } else { - // build the entire proxy chain if applicable - <end-user><proxy1><proxy2> - final List<String> proxyChain = new ArrayList<>(ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(request.getProxiedEntitiesChain())); - proxyChain.add(authenticationResponse.getIdentity()); - - // add the chain as appropriate to each proxy - NiFiUser proxy = null; - for (final ListIterator<String> chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { - String identity = chainIter.previous(); - - // determine if the user is anonymous - final boolean isAnonymous = StringUtils.isBlank(identity); - if (isAnonymous) { - identity = StandardNiFiUser.ANONYMOUS_IDENTITY; - } else { - identity = mapIdentity(identity); - } - - final Set<String> groups = getUserGroups(identity); - - // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities - String clientAddress = (proxy == null) ? request.getClientAddress() : null; - proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous); - - if (chainIter.hasPrevious()) { - try { - PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy); - } catch (final AccessDeniedException e) { - throw new UntrustedProxyException(String.format("Untrusted proxy %s", identity)); - } - } - } - - return new NiFiAuthenticationToken(new NiFiUserDetails(proxy)); - } - } - - /** - * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values. - * - * @param identity the user's identity - * @param chain the proxied entities - * @param clientAddress the requesting IP address - * @param isAnonymous if true, an anonymous user will be returned (identity will be ignored) - * @return the populated user - */ - protected static NiFiUser createUser(String identity, Set<String> groups, NiFiUser chain, String clientAddress, boolean isAnonymous) { - if (isAnonymous) { - return StandardNiFiUser.populateAnonymousUser(chain, clientAddress); - } else { - return new StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build(); - } - } - - private Map<String, String> getUserContext(final X509AuthenticationRequestToken request) { - final Map<String, String> userContext; - if (!StringUtils.isBlank(request.getClientAddress())) { - userContext = new HashMap<>(); - userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), request.getClientAddress()); - } else { - userContext = null; - } - return userContext; - } - - @Override - public boolean supports(Class<?> authentication) { - return X509AuthenticationRequestToken.class.isAssignableFrom(authentication); - } -} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java deleted file mode 100644 index d5aca23..0000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java +++ /dev/null @@ -1,75 +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.authentication.x509; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationRequestToken; -import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; - -import java.security.cert.X509Certificate; - -/** - * This is an authentication request with a given JWT token. - */ -public class X509AuthenticationRequestToken extends NiFiAuthenticationRequestToken { - - private final String proxiedEntitiesChain; - private final X509PrincipalExtractor principalExtractor; - private final X509Certificate[] certificates; - - /** - * Creates a representation of the jwt authentication request for a user. - * - * @param proxiedEntitiesChain The http servlet request - * @param certificates The certificate chain - */ - public X509AuthenticationRequestToken(final String proxiedEntitiesChain, final X509PrincipalExtractor principalExtractor, final X509Certificate[] certificates, final String clientAddress) { - super(clientAddress); - setAuthenticated(false); - this.proxiedEntitiesChain = proxiedEntitiesChain; - this.principalExtractor = principalExtractor; - this.certificates = certificates; - } - - @Override - public Object getCredentials() { - return null; - } - - @Override - public Object getPrincipal() { - if (StringUtils.isBlank(proxiedEntitiesChain)) { - return principalExtractor.extractPrincipal(certificates[0]); - } else { - return String.format("%s<%s>", proxiedEntitiesChain, principalExtractor.extractPrincipal(certificates[0])); - } - } - - public String getProxiedEntitiesChain() { - return proxiedEntitiesChain; - } - - public X509Certificate[] getCertificates() { - return certificates; - } - - @Override - public String toString() { - return getName(); - } - -} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java deleted file mode 100644 index d748b93..0000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java +++ /dev/null @@ -1,49 +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.authentication.x509; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.X509Certificate; - -/** - * Extracts client certificates from Http requests. - */ -@Component -public class X509CertificateValidator { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - /** - * Extract the client certificate from the specified HttpServletRequest or null if none is specified. - * - * @param certificates the client certificates - * @throws CertificateExpiredException cert is expired - * @throws CertificateNotYetValidException cert is not yet valid - */ - public void validateClientCertificate(final X509Certificate[] certificates) - throws CertificateExpiredException, CertificateNotYetValidException { - - // ensure the cert is valid - certificates[0].checkValidity(); - } - -} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java new file mode 100644 index 0000000..d4be5e9 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java @@ -0,0 +1,131 @@ +/* + * 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.x509; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.web.security.authentication.AuthenticationRequestToken; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider; +import org.apache.nifi.registry.web.security.authentication.AuthenticationSuccessToken; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +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.security.util.ProxiedEntitiesUtils; +import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException; + +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +public class X509IdentityAuthenticationProvider extends IdentityAuthenticationProvider { + + private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getProxyResource(); + } + }; + + public X509IdentityAuthenticationProvider(NiFiRegistryProperties properties, Authorizer authorizer, IdentityProvider identityProvider) { + super(properties, authorizer, identityProvider); + } + + @Override + protected AuthenticationSuccessToken buildAuthenticatedToken( + AuthenticationRequestToken requestToken, + AuthenticationResponse response) { + + AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest(); + + String proxiedEntitiesChain = authenticationRequest.getDetails() != null + ? (String)authenticationRequest.getDetails() + : null; + + if (StringUtils.isBlank(proxiedEntitiesChain)) { + return super.buildAuthenticatedToken(requestToken, response); + } + + // build the entire proxy chain if applicable - <end-user><proxy1><proxy2> + final List<String> proxyChain = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(proxiedEntitiesChain); + proxyChain.add(response.getIdentity()); + + // add the chain as appropriate to each proxy + NiFiUser proxy = null; + for (final ListIterator<String> chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { + String identity = chainIter.previous(); + + // determine if the user is anonymous + final boolean isAnonymous = StringUtils.isBlank(identity); + if (isAnonymous) { + identity = StandardNiFiUser.ANONYMOUS_IDENTITY; + } else { + identity = mapIdentity(identity); + } + + final Set<String> groups = getUserGroups(identity); + + // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities + String clientAddress = (proxy == null) ? requestToken.getClientAddress() : null; + proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous); + + if (chainIter.hasPrevious()) { + try { + PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy); + } catch (final AccessDeniedException e) { + throw new UntrustedProxyException(String.format("Untrusted proxy %s", identity)); + } + } + } + + return new AuthenticationSuccessToken(new NiFiUserDetails(proxy)); + + } + + /** + * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values. + * + * @param identity the user's identity + * @param chain the proxied entities + * @param clientAddress the requesting IP address + * @param isAnonymous if true, an anonymous user will be returned (identity will be ignored) + * @return the populated user + */ + private static NiFiUser createUser(String identity, Set<String> groups, NiFiUser chain, String clientAddress, boolean isAnonymous) { + if (isAnonymous) { + return StandardNiFiUser.populateAnonymousUser(chain, clientAddress); + } else { + return new StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build(); + } + } + + + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java index 692b318..9631efc 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java @@ -16,13 +16,22 @@ */ package org.apache.nifi.registry.web.security.authentication.x509; -import org.apache.nifi.registry.web.response.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.IdentityProviderUsage; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletRequest; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; @@ -32,73 +41,126 @@ import java.util.concurrent.TimeUnit; * Identity provider for extract the authenticating a ServletRequest with a X509Certificate. */ @Component -public class X509IdentityProvider { +public class X509IdentityProvider implements IdentityProvider { private static final Logger logger = LoggerFactory.getLogger(X509IdentityProvider.class); - private final String issuer = getClass().getSimpleName(); + private static final String issuer = X509IdentityProvider.class.getSimpleName(); + + private static final long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The client must connect over HTTPS and must provide a client certificate during the TLS handshake. " + + "Additionally, the client may declare itself a proxy for another user identity by populating the " + + ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN + " HTTP header field with a value of the format " + + "'<end-user-identity><proxy1-identity><proxy2-identity>...<proxyN-identity>'" + + "for all identities in the chain prior to this client. If the " + ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN + + " header is present in the request, this client's identity will be extracted from the client certificate " + + "used for TLS and added to the end of the chain, and then the entire chain will be authorized. Each proxy " + + "will be authorized to have 'write' access to '/proxy', and the originating user identity will be " + + "authorized for access to the resource being accessed in the request."; + } + }; - private X509CertificateValidator certificateValidator; private X509PrincipalExtractor principalExtractor; + private X509CertificateExtractor certificateExtractor; @Autowired - public X509IdentityProvider(X509CertificateValidator certificateValidator, X509PrincipalExtractor principalExtractor) { - this.certificateValidator = certificateValidator; + public X509IdentityProvider(X509PrincipalExtractor principalExtractor, X509CertificateExtractor certificateExtractor) { this.principalExtractor = principalExtractor; + this.certificateExtractor = certificateExtractor; + } + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; } /** - * Authenticates the specified request by checking certificate validity. + * Extracts certificate-based credentials from an {@link HttpServletRequest}. + * + * The resulting {@link AuthenticationRequest} will be populated as: + * - username: principal DN from first client cert + * - credentials: first client certificate (X509Certificate) + * - details: proxied-entities chain (String) * - * @param certificates the client certificates - * @return an authentication response - * @throws IllegalArgumentException the request did not contain a valid certificate (or no certificate) + * @param servletRequest the {@link HttpServletRequest} request that may contain credentials understood by this IdentityProvider + * @return a populated AuthenticationRequest or null if the credentials could not be found. */ - public AuthenticationResponse authenticate(final X509Certificate[] certificates) throws IllegalArgumentException { - // ensure the cert was found + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest servletRequest) { + + // only support x509 login when running securely + if (!servletRequest.isSecure()) { + return null; + } + + // look for a client certificate + final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(servletRequest); if (certificates == null || certificates.length == 0) { - throw new IllegalArgumentException("The specified request does not contain a client certificate."); + return null; } // extract the principal final Object certificatePrincipal = principalExtractor.extractPrincipal(certificates[0]); final String principal = certificatePrincipal.toString(); + // extract the proxiedEntitiesChain header value from the servletRequest + String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + + return new AuthenticationRequest(principal, certificates[0], proxiedEntitiesChainHeader); + + } + + /** + * For a given {@link AuthenticationRequest}, this validates the client certificate and creates a populated {@link AuthenticationResponse}. + * + * The {@link AuthenticationRequest} authenticationRequest paramenter is expected to be populated as: + * - username: principal DN from first client cert + * - credentials: first client certificate (X509Certificate) + * - details: proxied-entities chain (String) + * + * @param authenticationRequest the request, containing identity claim credentials for the IdentityProvider to authenticate and determine an identity + */ + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException { + + if (authenticationRequest == null || authenticationRequest.getUsername() == null) { + return null; + } + + String principal = authenticationRequest.getUsername(); + try { - certificateValidator.validateClientCertificate(certificates); + X509Certificate clientCertificate = (X509Certificate)authenticationRequest.getCredentials(); + validateClientCertificate(clientCertificate); } catch (CertificateExpiredException cee) { final String message = String.format("Client certificate for (%s) is expired.", principal); - logger.info(message, cee); - if (logger.isDebugEnabled()) { - logger.debug("", cee); - } - throw new IllegalArgumentException(message, cee); + logger.warn(message, cee); + throw new InvalidCredentialsException(message, cee); } catch (CertificateNotYetValidException cnyve) { final String message = String.format("Client certificate for (%s) is not yet valid.", principal); - logger.info(message, cnyve); - if (logger.isDebugEnabled()) { - logger.debug("", cnyve); - } - throw new IllegalArgumentException(message, cnyve); + logger.warn(message, cnyve); + throw new InvalidCredentialsException(message, cnyve); } catch (final Exception e) { - logger.info(e.getMessage()); - if (logger.isDebugEnabled()) { - logger.debug("", e); - } - throw new IllegalArgumentException(e.getMessage(), e); + logger.warn(e.getMessage(), e); } // build the authentication response - return new AuthenticationResponse(principal, principal, TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS), issuer); + return new AuthenticationResponse(principal, principal, expiration, issuer); } - /* setters */ - public void setCertificateValidator(X509CertificateValidator certificateValidator) { - this.certificateValidator = certificateValidator; - } + @Override + public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {} - public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { - this.principalExtractor = principalExtractor; + @Override + public void preDestruction() throws SecurityProviderDestructionException {} + + + private void validateClientCertificate(X509Certificate certificate) throws CertificateExpiredException, CertificateNotYetValidException { + certificate.checkValidity(); } } http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/main/xsd/identity-providers.xsd ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/xsd/identity-providers.xsd b/nifi-registry-web-api/src/main/xsd/identity-providers.xsd deleted file mode 100644 index bcca014..0000000 --- a/nifi-registry-web-api/src/main/xsd/identity-providers.xsd +++ /dev/null @@ -1,50 +0,0 @@ -<?xml version="1.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. ---> -<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> - <!-- role --> - <xs:complexType name="Provider"> - <xs:sequence> - <xs:element name="identifier" type="NonEmptyStringType"/> - <xs:element name="class" type="NonEmptyStringType"/> - <xs:element name="property" type="Property" minOccurs="0" maxOccurs="unbounded" /> - </xs:sequence> - </xs:complexType> - - <!-- Name/Value properties--> - <xs:complexType name="Property"> - <xs:simpleContent> - <xs:extension base="xs:string"> - <xs:attribute name="name" type="NonEmptyStringType"/> - <xs:attribute name="encryption" type="xs:string"/> - </xs:extension> - </xs:simpleContent> - </xs:complexType> - - <xs:simpleType name="NonEmptyStringType"> - <xs:restriction base="xs:string"> - <xs:minLength value="1"/> - </xs:restriction> - </xs:simpleType> - - <!-- login identity provider --> - <xs:element name="identityProviders"> - <xs:complexType> - <xs:sequence> - <xs:element name="provider" type="Provider" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - </xs:element> -</xs:schema> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/ef8ba127/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java index d6b94c2..bdd8e11 100644 --- a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java +++ b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java @@ -45,6 +45,7 @@ import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -71,20 +72,23 @@ import static org.junit.Assert.assertTrue; @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") public class SecureLdapIT extends IntegrationTestBase { + private static final String tokenLoginPath = "access/token/login"; + private static final String tokenIdentityProviderPath = "access/token/identity-provider"; + @TestConfiguration @Profile("ITSecureLdap") public static class LdapTestConfiguration { - static AuthorizerFactory af; + static AuthorizerFactory authorizerFactory; @Primary @Bean @DependsOn({"directoryServer"}) // Can't load LdapUserGroupProvider until the embedded LDAP server, which creates the "directoryServer" bean, is running public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties properties, ExtensionManager extensionManager) { - if (af == null) { - af = new AuthorizerFactory(properties, extensionManager); + if (authorizerFactory == null) { + authorizerFactory = new AuthorizerFactory(properties, extensionManager); } - return af.getAuthorizer(); + return authorizerFactory.getAuthorizer(); } } @@ -93,11 +97,9 @@ public class SecureLdapIT extends IntegrationTestBase { @Before public void generateAuthToken() { - final Form form = new Form() - .param("username", "nifiadmin") - .param("password", "password"); + final Form form = encodeCredentialsForURLFormParams("nifiadmin", "password"); final String token = client - .target(createURL("access/token")) + .target(createURL(tokenLoginPath)) .request() .post(Entity.form(form), String.class); adminAuthToken = token; @@ -121,12 +123,10 @@ public class SecureLdapIT extends IntegrationTestBase { "\"status\":\"ACTIVE\"" + "}"; - // When: the /access/token endpoint is queried - final Form form = new Form() - .param("username", "nobel") - .param("password", "password"); + // When: the /access/token/login endpoint is queried + final Form form = encodeCredentialsForURLFormParams("nobel", "password"); final Response tokenResponse = client - .target(createURL("access/token")) + .target(createURL(tokenLoginPath)) .request() .post(Entity.form(form), Response.class); @@ -154,6 +154,52 @@ public class SecureLdapIT extends IntegrationTestBase { } @Test + public void testTokenGenerationWithIdentityProvider() throws Exception { + + // Given: the client and server have been configured correctly for LDAP authentication + String expectedJwtPayloadJson = "{" + + "\"sub\":\"nobel\"," + + "\"preferred_username\":\"nobel\"," + + "\"iss\":\"LdapIdentityProvider\"," + + "\"aud\":\"LdapIdentityProvider\"" + + "}"; + String expectedAccessStatusJson = "{" + + "\"identity\":\"nobel\"," + + "\"status\":\"ACTIVE\"" + + "}"; + + // When: the /access/token/identity-provider endpoint is queried + final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); + final Response tokenResponse = client + .target(createURL(tokenIdentityProviderPath)) + .request() + .header("Authorization", "Basic " + basicAuthCredentials) + .post(null, Response.class); + + // Then: the server returns 200 OK with an access token + assertEquals(201, tokenResponse.getStatus()); + String token = tokenResponse.readEntity(String.class); + assertTrue(StringUtils.isNotEmpty(token)); + String[] jwtParts = token.split("\\."); + assertEquals(3, jwtParts.length); + String jwtPayload = new String(Base64.decodeBase64(jwtParts[1]), "UTF-8"); + JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false); + + // When: the token is returned in the Authorization header + final Response accessResponse = client + .target(createURL("access")) + .request() + .header("Authorization", "Bearer " + token) + .get(Response.class); + + // Then: the server acknowledges the client has access + assertEquals(200, accessResponse.getStatus()); + String accessStatus = accessResponse.readEntity(String.class); + JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false); + + } + + @Test public void testUsers() throws Exception { // Given: the client and server have been configured correctly for LDAP authentication @@ -240,15 +286,13 @@ public class SecureLdapIT extends IntegrationTestBase { // Given: the server has been configured with an initial admin "nifiadmin" and a user with no accessPolicies "nobel" String nobelId = getTenantIdentifierByIdentity("nobel"); String chemistsId = getTenantIdentifierByIdentity("chemists"); // a group containing user "nobel" - final Form form = new Form() - .param("username", "nobel") - .param("password", "password"); + + final Form form = encodeCredentialsForURLFormParams("nobel", "password"); final String nobelAuthToken = client - .target(createURL("access/token")) + .target(createURL(tokenLoginPath)) .request() .post(Entity.form(form), String.class); - // When: nifiadmin creates a bucket final Bucket bucket = new Bucket(); bucket.setName("Integration Test Bucket"); @@ -382,4 +426,15 @@ public class SecureLdapIT extends IntegrationTestBase { return matchedTenant != null ? matchedTenant.getIdentifier() : null; } + private static Form encodeCredentialsForURLFormParams(String username, String password) { + return new Form() + .param("username", username) + .param("password", password); + } + + private static String encodeCredentialsForBasicAuth(String username, String password) { + final String credentials = username + ":" + password; + final String base64credentials = new String(java.util.Base64.getEncoder().encode(credentials.getBytes(Charset.forName("UTF-8")))); + return base64credentials; + } }
