From a652280fbb45c94b6ba9be5e7f4f9cd8c708f5ca Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Wed, 28 Jul 2021 16:45:39 -0500 Subject: [PATCH] NIFI-8766 Implemented RS512 Algorithm for JWT Signing - Replaced per-user symmetric-key HS256 with shared and rotated RSA asymmetric-key RS512 implementation - Added nifi.security.user.jws.key.rotation.period property for RSA Key Pair rotation - Added JSON Web Tokens section to Administration Guide - Implemented persistent storage of RSA Public Keys for verification using Local State Manager - Implemented JWT revocation on logout with persistence using Local State Manager - Refactored JWT implementation using Spring Security OAuth2 and Nimbus JWT - Refactored Spring Security Provider configuration using Java instead of XML - Removed H2 storage of per-user keys - Upgraded nimbus-jose-jwt from 7.9 to 9.11.2 NIFI-8766 Corrected AuthenticationException handling in AccessResource.getAccessStatus - Added nifi.user.security.jws.key.rotation.period to default nifi.properties - Updated logging statements and clarified configuration and method documentation NIFI-8766 Changed Algorithm to PS512 and updated documentation Signed-off-by: Nathan Gough This closes #5262. --- nifi-assembly/NOTICE | 6 +- .../org/apache/nifi/util/NiFiProperties.java | 7 + .../main/asciidoc/administration-guide.adoc | 22 + .../nifi/admin/IdpDataSourceFactoryBean.java | 2 +- .../nifi/admin/KeyDataSourceFactoryBean.java | 147 ---- .../org/apache/nifi/admin/dao/DAOFactory.java | 2 - .../nifi/admin/dao/impl/DAOFactoryImpl.java | 7 - .../nifi/admin/dao/impl/StandardKeyDAO.java | 175 ----- .../service/impl/StandardKeyService.java | 165 ----- .../main/java/org/apache/nifi/key/Key.java | 78 -- .../resources/nifi-administration-context.xml | 16 - .../ThreadPoolRequestReplicator.java | 7 +- .../nifi-framework/nifi-resources/pom.xml | 1 + .../src/main/resources/conf/nifi.properties | 1 + .../nifi/web/CsrfCookieRequestMatcher.java | 6 +- .../nifi/web/NiFiWebApiConfiguration.java | 7 +- .../web/NiFiWebApiSecurityConfiguration.java | 48 +- .../apache/nifi/web/api/AccessResource.java | 90 ++- .../nifi/web/api/ApplicationResource.java | 4 +- .../nifi/web/api/OIDCAccessResource.java | 18 +- .../main/resources/nifi-web-api-context.xml | 6 +- .../accesscontrol/ITAccessTokenEndpoint.java | 422 ----------- .../nifi-web/nifi-web-security/pom.xml | 21 +- .../security/NiFiAuthenticationFilter.java | 34 +- .../AuthenticationSecurityConfiguration.java | 82 +++ ...wtAuthenticationSecurityConfiguration.java | 208 ++++++ ...osAuthenticationSecurityConfiguration.java | 46 ++ ...oxAuthenticationSecurityConfiguration.java | 57 ++ ...dcAuthenticationSecurityConfiguration.java | 49 ++ ...mlAuthenticationSecurityConfiguration.java | 72 ++ ...09AuthenticationSecurityConfiguration.java | 84 +++ .../security/http/SecurityCookieName.java} | 24 +- .../web/security/http/SecurityHeader.java} | 24 +- .../security/jwt/JwtAuthenticationFilter.java | 50 -- .../jwt/JwtAuthenticationProvider.java | 83 --- .../jwt/JwtAuthenticationRequestToken.java | 59 -- .../nifi/web/security/jwt/JwtService.java | 208 ------ .../security/jwt/NiFiBearerTokenResolver.java | 70 -- .../StandardJwtAuthenticationConverter.java | 80 ++ .../security/jwt/jws/JwsSignerContainer.java | 51 ++ .../security/jwt/jws/JwsSignerProvider.java} | 28 +- .../SignerListener.java} | 19 +- .../security/jwt/jws/SigningKeyListener.java | 32 + .../jwt/jws/StandardJWSKeySelector.java | 54 ++ .../jwt/jws/StandardJwsSignerProvider.java | 67 ++ .../key/StandardVerificationKeySelector.java | 85 +++ .../jwt/key/VerificationKeyListener.java | 32 + .../jwt/key/VerificationKeySelector.java | 33 + .../jwt/key/command/KeyExpirationCommand.java | 45 ++ .../jwt/key/command/KeyGenerationCommand.java | 78 ++ .../StandardVerificationKeyService.java | 187 +++++ .../jwt/key/service/VerificationKey.java | 64 ++ .../key/service/VerificationKeyService.java} | 48 +- .../jwt/provider/BearerTokenProvider.java | 32 + .../provider/StandardBearerTokenProvider.java | 110 +++ .../jwt/provider/SupportedClaim.java} | 52 +- .../resolver/StandardBearerTokenResolver.java | 61 ++ .../jwt/revocation/JwtLogoutListener.java | 29 + .../jwt/revocation/JwtRevocationService.java} | 36 +- .../revocation/JwtRevocationValidator.java | 52 ++ .../revocation/StandardJwtLogoutListener.java | 50 ++ .../StandardJwtRevocationService.java | 110 +++ .../command/RevocationExpirationCommand.java | 45 ++ .../security/knox/KnoxServiceFactoryBean.java | 2 +- .../oidc/StandardOidcIdentityProvider.java | 17 +- .../saml/impl/StandardSAMLStateManager.java | 22 +- .../LoginIdentityProviderFactoryBean.java | 3 - .../resources/nifi-web-security-context.xml | 119 --- .../oidc/OidcServiceGroovyTest.groovy | 41 +- ...ndardOidcIdentityProviderGroovyTest.groovy | 51 +- .../jwt/JwtAuthenticationProviderTest.java | 282 -------- .../nifi/web/security/jwt/JwtServiceTest.java | 682 ------------------ .../jwt/NiFiBearerTokenResolverTest.java | 124 ---- .../nifi/web/security/jwt/TestKeyService.java | 71 -- ...tandardJwtAuthenticationConverterTest.java | 121 ++++ .../jws/StandardJwsSignerProviderTest.java | 70 ++ .../key/command/KeyGenerationCommandTest.java | 76 ++ .../StandardVerificationKeyServiceTest.java | 133 ++++ .../StandardBearerTokenProviderTest.java | 109 +++ .../StandardBearerTokenResolverTest.java | 77 ++ .../JwtRevocationValidatorTest.java | 70 ++ .../StandardJwtLogoutListenerTest.java | 78 ++ .../StandardJwtRevocationServiceTest.java | 108 +++ .../impl/TestStandardSAMLStateManager.java | 10 +- .../nifi-framework-bundle/pom.xml | 35 +- 85 files changed, 3045 insertions(+), 3144 deletions(-) delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/KeyDataSourceFactoryBean.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KerberosAuthenticationSecurityConfiguration.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KnoxAuthenticationSecurityConfiguration.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcAuthenticationSecurityConfiguration.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/X509AuthenticationSecurityConfiguration.java rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/{nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java => nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java} (61%) rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/{nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java => nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java} (58%) delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProvider.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverter.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerContainer.java rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/{nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java => nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerProvider.java} (54%) rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/{BearerTokenResolver.java => jws/SignerListener.java} (65%) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/SigningKeyListener.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJWSKeySelector.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProvider.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/StandardVerificationKeySelector.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeyListener.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeySelector.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyExpirationCommand.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyService.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKey.java rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/{nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java => nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKeyService.java} (53%) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/BearerTokenProvider.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/{nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java => nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/SupportedClaim.java} (51%) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolver.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtLogoutListener.java rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/{nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java => nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationService.java} (55%) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidator.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListener.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationService.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/command/RevocationExpirationCommand.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverterTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProviderTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyServiceTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolverTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidatorTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListenerTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationServiceTest.java diff --git a/nifi-assembly/NOTICE b/nifi-assembly/NOTICE index 25ae170d46..55ebeda516 100644 --- a/nifi-assembly/NOTICE +++ b/nifi-assembly/NOTICE @@ -1904,10 +1904,10 @@ The following binary components are provided under the Apache Software License v Converter: Jackson 2.5.0 Copyright 2018, Jake Wharton - (ASLv2) Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:jar:6.0.1 - https://bitbucket.org/connect2id/nimbus-jose-jwt) + (ASLv2) Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt - https://connect2id.com/products/nimbus-jose-jwt) The following NOTICE information applies: - Nimbus JOSE+JWT 6.0.1 - Copyright 2018, Connect2id Ltd. + Nimbus JOSE+JWT + Copyright 2021, Connect2id Ltd. (ASLv2) OkHttp Logging Interceptor (com.squareup.okhttp3:logging-interceptor:jar:3.12.2) The following NOTICE information applies: diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java index c68bb5b740..bf7f6a43a6 100644 --- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java +++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java @@ -28,6 +28,7 @@ import java.net.InetSocketAddress; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -170,6 +171,7 @@ public class NiFiProperties extends ApplicationProperties { public static final String SECURITY_GROUP_MAPPING_PATTERN_PREFIX = "nifi.security.group.mapping.pattern."; public static final String SECURITY_GROUP_MAPPING_VALUE_PREFIX = "nifi.security.group.mapping.value."; public static final String SECURITY_GROUP_MAPPING_TRANSFORM_PREFIX = "nifi.security.group.mapping.transform."; + public static final String SECURITY_USER_JWS_KEY_ROTATION_PERIOD = "nifi.security.user.jws.key.rotation.period"; // oidc public static final String SECURITY_USER_OIDC_DISCOVERY_URL = "nifi.security.user.oidc.discovery.url"; @@ -366,6 +368,7 @@ public class NiFiProperties extends ApplicationProperties { public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_TRUSTSTORE_STRATEGY = "JDK"; public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT = "30 secs"; public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT = "30 secs"; + private static final String DEFAULT_SECURITY_USER_JWS_KEY_ROTATION_PERIOD = "PT1H"; public static final String DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION = "true"; // cluster common defaults @@ -798,6 +801,10 @@ public class NiFiProperties extends ApplicationProperties { return getProperty(SECURITY_AUTO_RELOAD_INTERVAL, DEFAULT_SECURITY_AUTO_RELOAD_INTERVAL); } + public Duration getSecurityUserJwsKeyRotationPeriod() { + return Duration.parse(getProperty(SECURITY_USER_JWS_KEY_ROTATION_PERIOD, DEFAULT_SECURITY_USER_JWS_KEY_ROTATION_PERIOD)); + } + // getters for cluster protocol properties // public String getClusterProtocolHeartbeatInterval() { return getProperty(CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL, diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 8c7897cc7c..72e636f2f2 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -489,6 +489,28 @@ To enable authentication via Apache Knox the following properties must be config this listing. The audience that is populated in the token can be configured in Knox. |================================================================================================================================================== +[[json_web_token]] +=== JSON Web Tokens + +NiFi uses JSON Web Tokens to provide authenticated access after the initial login process. Generated JSON Web Tokens include the authenticated user identity +as well as the issuer and expiration from the configured Login Identity Provider. + +NiFi uses generated RSA Key Pairs with a key size of 4096 bits to support the `PS512` algorithm for JSON Web Signatures. The system stores RSA +Public Keys using the configured local State Provider and retains the RSA Private Key in memory. This approach supports signature verification +for the expiration configured in the Login Identity Provider without persisting the private key. + +JSON Web Token support includes revocation on logout using JSON Web Token Identifiers. The system denies access for expired tokens based on the +Login Identity Provider configuration, but revocation invalidates the token prior to expiration. The system stores revoked identifiers using the +configured local State Provider and runs a scheduled command to delete revoked identifiers after the associated expiration. + +The following settings can be configured in _nifi.properties_ to control JSON Web Token signing. + +[options="header"] +|================================================================================================================================================== +| Property Name | Description +|`nifi.security.user.jws.key.rotation.period` | JSON Web Signature Key Rotation Period defines how often the system generates a new RSA Key Pair, expressed as an ISO 8601 duration. The default is one hour: `PT1H` +|================================================================================================================================================== + [[multi-tenant-authorization]] == Multi-Tenant Authorization diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/IdpDataSourceFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/IdpDataSourceFactoryBean.java index d0545e4ee0..d0078e2c46 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/IdpDataSourceFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/IdpDataSourceFactoryBean.java @@ -31,7 +31,7 @@ import java.sql.Statement; public class IdpDataSourceFactoryBean implements FactoryBean { - private static final Logger logger = LoggerFactory.getLogger(KeyDataSourceFactoryBean.class); + private static final Logger logger = LoggerFactory.getLogger(IdpDataSourceFactoryBean.class); private static final String NF_USERNAME_PASSWORD = "nf"; private static final int MAX_CONNECTIONS = 5; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/KeyDataSourceFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/KeyDataSourceFactoryBean.java deleted file mode 100644 index 83479532c0..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/KeyDataSourceFactoryBean.java +++ /dev/null @@ -1,147 +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.admin; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.util.NiFiProperties; -import org.h2.jdbcx.JdbcConnectionPool; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.FactoryBean; - -import java.io.File; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; - -public class KeyDataSourceFactoryBean implements FactoryBean { - - private static final Logger logger = LoggerFactory.getLogger(KeyDataSourceFactoryBean.class); - private static final String NF_USERNAME_PASSWORD = "nf"; - private static final int MAX_CONNECTIONS = 5; - - // database file name - private static final String USER_KEYS_DATABASE_FILE_NAME = "nifi-user-keys"; - - // ---------- - // keys table - // ---------- - - private static final String CREATE_KEY_TABLE = "CREATE TABLE KEY (" - + "ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT, " - + "IDENTITY VARCHAR2(4096) NOT NULL UNIQUE, " - + "KEY VARCHAR2(100) NOT NULL" - + ")"; - - private JdbcConnectionPool connectionPool; - - private NiFiProperties properties; - - @Override - public Object getObject() throws Exception { - if (connectionPool == null) { - - // locate the repository directory - String repositoryDirectoryPath = properties.getProperty(NiFiProperties.REPOSITORY_DATABASE_DIRECTORY); - - // ensure the repository directory is specified - if (repositoryDirectoryPath == null) { - throw new NullPointerException("Database directory must be specified."); - } - - // create a handle to the repository directory - File repositoryDirectory = new File(repositoryDirectoryPath); - - // create a handle to the database directory and file - File databaseFile = new File(repositoryDirectory, USER_KEYS_DATABASE_FILE_NAME); - String databaseUrl = getDatabaseUrl(databaseFile); - - // create the pool - connectionPool = JdbcConnectionPool.create(databaseUrl, NF_USERNAME_PASSWORD, NF_USERNAME_PASSWORD); - connectionPool.setMaxConnections(MAX_CONNECTIONS); - - Connection connection = null; - ResultSet rs = null; - Statement statement = null; - try { - // get a connection - connection = connectionPool.getConnection(); - connection.setAutoCommit(false); - - // create a statement for creating/updating the database - statement = connection.createStatement(); - - // determine if the key table need to be created - rs = connection.getMetaData().getTables(null, null, "KEY", null); - if (!rs.next()) { - statement.execute(CREATE_KEY_TABLE); - } - - // commit any changes - connection.commit(); - } catch (SQLException sqle) { - RepositoryUtils.rollback(connection, logger); - throw sqle; - } finally { - RepositoryUtils.closeQuietly(rs); - RepositoryUtils.closeQuietly(statement); - RepositoryUtils.closeQuietly(connection); - } - } - - return connectionPool; - } - - private String getDatabaseUrl(File databaseFile) { - String databaseUrl = "jdbc:h2:" + databaseFile + ";AUTOCOMMIT=OFF;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=3"; - String databaseUrlAppend = properties.getProperty(NiFiProperties.H2_URL_APPEND); - if (StringUtils.isNotBlank(databaseUrlAppend)) { - databaseUrl += databaseUrlAppend; - } - return databaseUrl; - } - - @Override - public Class getObjectType() { - return JdbcConnectionPool.class; - } - - @Override - public boolean isSingleton() { - return true; - } - - public void setProperties(NiFiProperties properties) { - this.properties = properties; - } - - public void shutdown() { - // shutdown the connection pool - if (connectionPool != null) { - try { - connectionPool.dispose(); - } catch (Exception e) { - logger.warn("Unable to dispose of connection pool: " + e.getMessage()); - if (logger.isDebugEnabled()) { - logger.warn(StringUtils.EMPTY, e); - } - } - } - } - -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/DAOFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/DAOFactory.java index 3ea634e402..fe71fa5766 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/DAOFactory.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/DAOFactory.java @@ -23,8 +23,6 @@ public interface DAOFactory { ActionDAO getActionDAO(); - KeyDAO getKeyDAO(); - IdpCredentialDAO getIdpCredentialDAO(); IdpUserGroupDAO getIdpUserGroupDAO(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/DAOFactoryImpl.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/DAOFactoryImpl.java index ecfe2e7dc5..978cef83d8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/DAOFactoryImpl.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/DAOFactoryImpl.java @@ -20,7 +20,6 @@ import org.apache.nifi.admin.dao.ActionDAO; import org.apache.nifi.admin.dao.DAOFactory; import org.apache.nifi.admin.dao.IdpCredentialDAO; import org.apache.nifi.admin.dao.IdpUserGroupDAO; -import org.apache.nifi.admin.dao.KeyDAO; import java.sql.Connection; @@ -40,12 +39,6 @@ public class DAOFactoryImpl implements DAOFactory { return new StandardActionDAO(connection); } - @Override - public KeyDAO getKeyDAO() { - return new StandardKeyDAO(connection); - } - - @Override public IdpCredentialDAO getIdpCredentialDAO() { return new StandardIdpCredentialDAO(connection); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java deleted file mode 100644 index 28d090de7d..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java +++ /dev/null @@ -1,175 +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.admin.dao.impl; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.UUID; -import org.apache.nifi.admin.RepositoryUtils; -import org.apache.nifi.admin.dao.DataAccessException; -import org.apache.nifi.admin.dao.KeyDAO; -import org.apache.nifi.key.Key; - -/** - * - */ -public class StandardKeyDAO implements KeyDAO { - - private static final String SELECT_KEY_FOR_USER_BY_ID = "SELECT ID, IDENTITY, KEY " - + "FROM KEY " - + "WHERE ID = ?"; - - private static final String SELECT_KEY_FOR_USER_BY_IDENTITY = "SELECT ID, IDENTITY, KEY " - + "FROM KEY " - + "WHERE IDENTITY = ?"; - - private static final String INSERT_KEY = "INSERT INTO KEY (" - + "IDENTITY, KEY" - + ") VALUES (" - + "?, ?" - + ")"; - - private static final String DELETE_KEYS = "DELETE FROM KEY " - + "WHERE ID = ?"; - - private final Connection connection; - - public StandardKeyDAO(Connection connection) { - this.connection = connection; - } - - @Override - public Key findKeyById(int id) { - Key key = null; - - PreparedStatement statement = null; - ResultSet rs = null; - try { - // add each authority for the specified user - statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_ID); - statement.setInt(1, id); - - // execute the query - rs = statement.executeQuery(); - - // if the key was found, add it - if (rs.next()) { - key = new Key(); - key.setId(rs.getInt("ID")); - key.setIdentity(rs.getString("IDENTITY")); - key.setKey(rs.getString("KEY")); - } - } catch (SQLException sqle) { - throw new DataAccessException(sqle); - } finally { - RepositoryUtils.closeQuietly(rs); - RepositoryUtils.closeQuietly(statement); - } - - return key; - } - - @Override - public Key findLatestKeyByIdentity(String identity) { - if (identity == null) { - throw new IllegalArgumentException("Specified identity cannot be null."); - } - - Key key = null; - - PreparedStatement statement = null; - ResultSet rs = null; - try { - // add each authority for the specified user - statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_IDENTITY); - statement.setString(1, identity); - - // execute the query - rs = statement.executeQuery(); - - // if the key was found, add it - if (rs.next()) { - key = new Key(); - key.setId(rs.getInt("ID")); - key.setIdentity(rs.getString("IDENTITY")); - key.setKey(rs.getString("KEY")); - } - } catch (SQLException sqle) { - throw new DataAccessException(sqle); - } finally { - RepositoryUtils.closeQuietly(rs); - RepositoryUtils.closeQuietly(statement); - } - - return key; - } - - @Override - public Key createKey(final String identity) { - PreparedStatement statement = null; - ResultSet rs = null; - try { - final String keyValue = UUID.randomUUID().toString(); - - // add each authority for the specified user - statement = connection.prepareStatement(INSERT_KEY, Statement.RETURN_GENERATED_KEYS); - statement.setString(1, identity); - statement.setString(2, keyValue); - - // insert the key - int updateCount = statement.executeUpdate(); - rs = statement.getGeneratedKeys(); - - // verify the results - if (updateCount == 1 && rs.next()) { - final Key key = new Key(); - key.setId(rs.getInt(1)); - key.setIdentity(identity); - key.setKey(keyValue); - return key; - } else { - throw new DataAccessException("Unable to add key for user."); - } - } catch (SQLException sqle) { - throw new DataAccessException(sqle); - } finally { - RepositoryUtils.closeQuietly(rs); - RepositoryUtils.closeQuietly(statement); - } - } - - @Override - public Integer deleteKey(Integer keyId) { - PreparedStatement statement = null; - try { - // add each authority for the specified user - statement = connection.prepareStatement(DELETE_KEYS); - statement.setInt(1, keyId); - return statement.executeUpdate(); - } catch (SQLException sqle) { - throw new DataAccessException(sqle); - } catch (DataAccessException dae) { - throw dae; - } finally { - RepositoryUtils.closeQuietly(statement); - } - } - -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java deleted file mode 100644 index 8f4198aed3..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java +++ /dev/null @@ -1,165 +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.admin.service.impl; - -import org.apache.nifi.admin.dao.DataAccessException; -import org.apache.nifi.admin.service.AdministrationException; -import org.apache.nifi.admin.service.KeyService; -import org.apache.nifi.admin.service.action.DeleteKeyAction; -import org.apache.nifi.admin.service.action.GetKeyByIdAction; -import org.apache.nifi.admin.service.action.GetOrCreateKeyAction; -import org.apache.nifi.admin.service.transaction.Transaction; -import org.apache.nifi.admin.service.transaction.TransactionBuilder; -import org.apache.nifi.admin.service.transaction.TransactionException; -import org.apache.nifi.key.Key; -import org.apache.nifi.util.NiFiProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * This key service manages user JWT signing keys in the H2 user database. - */ -public class StandardKeyService implements KeyService { - - private static final Logger logger = LoggerFactory.getLogger(StandardKeyService.class); - - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final Lock readLock = lock.readLock(); - private final Lock writeLock = lock.writeLock(); - - private TransactionBuilder transactionBuilder; - private NiFiProperties properties; - - @Override - public Key getKey(int id) { - Transaction transaction = null; - Key key; - - readLock.lock(); - try { - // start the transaction - transaction = transactionBuilder.start(); - - // get the key - GetKeyByIdAction addActions = new GetKeyByIdAction(id); - key = transaction.execute(addActions); - - // commit the transaction - transaction.commit(); - } catch (TransactionException | DataAccessException te) { - rollback(transaction); - throw new AdministrationException(te); - } catch (Throwable t) { - rollback(transaction); - throw t; - } finally { - closeQuietly(transaction); - readLock.unlock(); - } - - return key; - } - - @Override - public Key getOrCreateKey(String identity) { - Transaction transaction = null; - Key key; - - writeLock.lock(); - try { - // start the transaction - transaction = transactionBuilder.start(); - - // get or create a key - GetOrCreateKeyAction addActions = new GetOrCreateKeyAction(identity); - key = transaction.execute(addActions); - - // commit the transaction - transaction.commit(); - } catch (TransactionException | DataAccessException te) { - rollback(transaction); - throw new AdministrationException(te); - } catch (Throwable t) { - rollback(transaction); - throw t; - } finally { - closeQuietly(transaction); - writeLock.unlock(); - } - - return key; - } - - @Override - public void deleteKey(Integer keyId) { - Transaction transaction = null; - - writeLock.lock(); - try { - // start the transaction - transaction = transactionBuilder.start(); - - // delete the keys - DeleteKeyAction deleteKey = new DeleteKeyAction(keyId); - Integer rowsDeleted = transaction.execute(deleteKey); - - // commit the transaction if we found one and only one matching keyId/user identity - if (rowsDeleted == 1) { - transaction.commit(); - } else { - rollback(transaction); - throw new AdministrationException("Unable to find user key for key ID " + keyId + " to remove token."); - } - } catch (TransactionException | DataAccessException te) { - rollback(transaction); - throw new AdministrationException(te); - } catch (Throwable t) { - rollback(transaction); - throw t; - } finally { - closeQuietly(transaction); - writeLock.unlock(); - } - } - - private void rollback(final Transaction transaction) { - if (transaction != null) { - transaction.rollback(); - } - } - - private void closeQuietly(final Transaction transaction) { - if (transaction != null) { - try { - transaction.close(); - } catch (final IOException ioe) { - } - } - } - - public void setTransactionBuilder(TransactionBuilder transactionBuilder) { - this.transactionBuilder = transactionBuilder; - } - - public void setProperties(NiFiProperties properties) { - this.properties = properties; - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java deleted file mode 100644 index ce1c6d5b3b..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java +++ /dev/null @@ -1,78 +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.key; - -import java.io.Serializable; - -/** - * An signing key for a NiFi user. - */ -public class Key implements Serializable { - - private int id; - private String identity; - private String key; - - /** - * The key id. - * - * @return the id - */ - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - /** - * The identity of the user this key is associated with. - * - * @return the identity - */ - public String getIdentity() { - return identity; - } - - public void setIdentity(String identity) { - this.identity = identity; - } - - /** - * The signing key. - * - * @return the signing key - */ - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public Key(int id, String identity, String key) { - this.id = id; - this.identity = identity; - this.key = key; - } - - public Key() { - } - -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/resources/nifi-administration-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/resources/nifi-administration-context.xml index baf089fd96..d7facce913 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/resources/nifi-administration-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/resources/nifi-administration-context.xml @@ -18,11 +18,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> - - - - - @@ -33,11 +28,6 @@ - - - - - @@ -48,12 +38,6 @@ - - - - - - diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java index 6665095581..4eddc8a2ee 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java @@ -40,7 +40,8 @@ import org.apache.nifi.reporting.Severity; import org.apache.nifi.util.ComponentIdGenerator; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.security.ProxiedEntitiesUtils; -import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver; +import org.apache.nifi.web.security.http.SecurityCookieName; +import org.apache.nifi.web.security.http.SecurityHeader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -244,13 +245,13 @@ public class ThreadPoolRequestReplicator implements RequestReplicator { // remove the access token if present, since the user is already authenticated... authorization // will happen when the request is replicated using the proxy chain above - headers.remove(NiFiBearerTokenResolver.AUTHORIZATION); + headers.remove(SecurityHeader.AUTHORIZATION.getHeader()); // if knox sso cookie name is set, remove any authentication cookie since this user is already authenticated // and will be included in the proxied entities chain above... authorization will happen when the // request is replicated removeCookie(headers, nifiProperties.getKnoxCookieName()); - removeCookie(headers, NiFiBearerTokenResolver.JWT_COOKIE_NAME); + removeCookie(headers, SecurityCookieName.AUTHORIZATION_BEARER.getName()); // remove the host header headers.remove("Host"); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml index 6f438228c8..0a00789486 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml @@ -162,6 +162,7 @@ single-user-authorizer false single-user-provider + PT1H diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties index 272d15fb3e..18a00a4077 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties @@ -194,6 +194,7 @@ nifi.security.truststorePasswd=${nifi.security.truststorePasswd} nifi.security.user.authorizer=${nifi.security.user.authorizer} nifi.security.allow.anonymous.authentication=${nifi.security.allow.anonymous.authentication} nifi.security.user.login.identity.provider=${nifi.security.user.login.identity.provider} +nifi.security.user.jws.key.rotation.period=${nifi.security.user.jws.key.rotation.period} nifi.security.ocsp.responder.url=${nifi.security.ocsp.responder.url} nifi.security.ocsp.responder.certificate=${nifi.security.ocsp.responder.certificate} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/CsrfCookieRequestMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/CsrfCookieRequestMatcher.java index a10dbf3e6e..474236efbc 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/CsrfCookieRequestMatcher.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/CsrfCookieRequestMatcher.java @@ -16,7 +16,7 @@ */ package org.apache.nifi.web; -import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver; +import org.apache.nifi.web.security.http.SecurityCookieName; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.util.WebUtils; import javax.servlet.http.HttpServletRequest; @@ -25,8 +25,6 @@ import javax.servlet.http.HttpServletRequest; * Request Matcher checks for the existence of a cookie with the configured name */ public class CsrfCookieRequestMatcher implements RequestMatcher { - private static final String DEFAULT_CSRF_COOKIE_NAME = NiFiBearerTokenResolver.JWT_COOKIE_NAME; - /** * Matches request based on the presence of a cookie found using the configured name * @@ -35,6 +33,6 @@ public class CsrfCookieRequestMatcher implements RequestMatcher { */ @Override public boolean matches(final HttpServletRequest httpServletRequest) { - return WebUtils.getCookie(httpServletRequest, DEFAULT_CSRF_COOKIE_NAME) != null; + return WebUtils.getCookie(httpServletRequest, SecurityCookieName.AUTHORIZATION_BEARER.getName()) != null; } } \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiConfiguration.java index 2a714f87ec..6e7bbf0b1c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiConfiguration.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiConfiguration.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.web; +import org.apache.nifi.web.security.configuration.AuthenticationSecurityConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportResource; @@ -24,13 +25,15 @@ import org.springframework.context.annotation.ImportResource; * */ @Configuration -@Import({NiFiWebApiSecurityConfiguration.class}) +@Import({ + NiFiWebApiSecurityConfiguration.class, + AuthenticationSecurityConfiguration.class +}) @ImportResource({"classpath:nifi-context.xml", "classpath:nifi-administration-context.xml", "classpath:nifi-authorizer-context.xml", "classpath:nifi-cluster-manager-context.xml", "classpath:nifi-cluster-protocol-context.xml", - "classpath:nifi-web-security-context.xml", "classpath:nifi-web-api-context.xml"}) public class NiFiWebApiConfiguration { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java index 359a03fb71..d639a03fca 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java @@ -19,9 +19,9 @@ package org.apache.nifi.web; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter; import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider; -import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter; -import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider; -import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver; +import org.apache.nifi.web.security.http.SecurityCookieName; +import org.apache.nifi.web.security.http.SecurityHeader; +import org.apache.nifi.web.security.jwt.resolver.StandardBearerTokenResolver; import org.apache.nifi.web.security.knox.KnoxAuthenticationFilter; import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider; import org.apache.nifi.web.security.oidc.OIDCEndpoints; @@ -29,7 +29,6 @@ import org.apache.nifi.web.security.saml.SAMLEndpoints; import org.apache.nifi.web.security.x509.X509AuthenticationFilter; import org.apache.nifi.web.security.x509.X509AuthenticationProvider; import org.apache.nifi.web.security.x509.X509CertificateExtractor; -import org.apache.nifi.web.security.x509.X509IdentityProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,6 +40,9 @@ 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.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.security.web.csrf.CsrfFilter; @@ -64,10 +66,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte private X509AuthenticationFilter x509AuthenticationFilter; private X509CertificateExtractor certificateExtractor; private X509PrincipalExtractor principalExtractor; - private X509IdentityProvider certificateIdentityProvider; private X509AuthenticationProvider x509AuthenticationProvider; - - private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationProvider jwtAuthenticationProvider; private KnoxAuthenticationFilter knoxAuthenticationFilter; @@ -105,7 +104,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte SAMLEndpoints.LOGIN_CONSUMER, SAMLEndpoints.LOGIN_EXCHANGE, // the logout sequence will be protected by a request identifier set in a Cookie so these - // paths need to be listed here in order to pass through our normal authn filters + // paths need to be listed here in order to pass through our normal authentication filters SAMLEndpoints.SINGLE_LOGOUT_REQUEST, SAMLEndpoints.SINGLE_LOGOUT_CONSUMER, SAMLEndpoints.LOCAL_LOGOUT, @@ -115,8 +114,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte @Override protected void configure(HttpSecurity http) throws Exception { NiFiCsrfTokenRepository csrfRepository = new NiFiCsrfTokenRepository(); - csrfRepository.setHeaderName(NiFiBearerTokenResolver.AUTHORIZATION); - csrfRepository.setCookieName(NiFiBearerTokenResolver.JWT_COOKIE_NAME); + csrfRepository.setHeaderName(SecurityHeader.AUTHORIZATION.getHeader()); + csrfRepository.setCookieName(SecurityCookieName.AUTHORIZATION_BEARER.getName()); http .cors().and() @@ -129,7 +128,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte http.addFilterBefore(x509FilterBean(), AnonymousAuthenticationFilter.class); // jwt - http.addFilterBefore(jwtFilterBean(), AnonymousAuthenticationFilter.class); + http.addFilterBefore(bearerTokenAuthenticationFilter(), AnonymousAuthenticationFilter.class); // knox http.addFilterBefore(knoxFilterBean(), AnonymousAuthenticationFilter.class); @@ -167,16 +166,6 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte .authenticationProvider(anonymousAuthenticationProvider); } - @Bean - public JwtAuthenticationFilter jwtFilterBean() throws Exception { - if (jwtAuthenticationFilter == null) { - jwtAuthenticationFilter = new JwtAuthenticationFilter(); - jwtAuthenticationFilter.setProperties(properties); - jwtAuthenticationFilter.setAuthenticationManager(authenticationManager()); - } - return jwtAuthenticationFilter; - } - @Bean public KnoxAuthenticationFilter knoxFilterBean() throws Exception { if (knoxAuthenticationFilter == null) { @@ -199,6 +188,18 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte return x509AuthenticationFilter; } + @Bean + public BearerTokenAuthenticationFilter bearerTokenAuthenticationFilter() throws Exception { + final BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(authenticationManager()); + filter.setBearerTokenResolver(bearerTokenResolver()); + return filter; + } + + @Bean + public BearerTokenResolver bearerTokenResolver() { + return new StandardBearerTokenResolver(); + } + @Bean public NiFiAnonymousAuthenticationFilter anonymousFilterBean() throws Exception { if (anonymousAuthenticationFilter == null) { @@ -243,9 +244,4 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { this.principalExtractor = principalExtractor; } - - @Autowired - public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) { - this.certificateIdentityProvider = certificateIdentityProvider; - } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java index bc19f3f90f..7d3794778b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java @@ -16,7 +16,6 @@ */ package org.apache.nifi.web.api; -import io.jsonwebtoken.JwtException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; @@ -43,16 +42,14 @@ import org.apache.nifi.web.security.InvalidAuthenticationException; import org.apache.nifi.web.security.LogoutException; import org.apache.nifi.web.security.ProxiedEntitiesUtils; import org.apache.nifi.web.security.UntrustedProxyException; -import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider; -import org.apache.nifi.web.security.jwt.JwtAuthenticationRequestToken; -import org.apache.nifi.web.security.jwt.JwtService; -import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver; +import org.apache.nifi.web.security.http.SecurityCookieName; +import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; +import org.apache.nifi.web.security.jwt.revocation.JwtLogoutListener; import org.apache.nifi.web.security.kerberos.KerberosService; import org.apache.nifi.web.security.knox.KnoxService; import org.apache.nifi.web.security.logout.LogoutRequest; import org.apache.nifi.web.security.logout.LogoutRequestManager; import org.apache.nifi.web.security.token.LoginAuthenticationToken; -import org.apache.nifi.web.security.token.NiFiAuthenticationToken; import org.apache.nifi.web.security.x509.X509AuthenticationProvider; import org.apache.nifi.web.security.x509.X509AuthenticationRequestToken; import org.apache.nifi.web.security.x509.X509CertificateExtractor; @@ -61,6 +58,9 @@ import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.web.util.WebUtils; @@ -103,7 +103,9 @@ public class AccessResource extends ApplicationResource { private LoginIdentityProvider loginIdentityProvider; private JwtAuthenticationProvider jwtAuthenticationProvider; - private JwtService jwtService; + private JwtLogoutListener jwtLogoutListener; + private BearerTokenProvider bearerTokenProvider; + private BearerTokenResolver bearerTokenResolver; private KnoxService knoxService; private KerberosService kerberosService; protected LogoutRequestManager logoutRequestManager; @@ -246,28 +248,29 @@ public class AccessResource extends ApplicationResource { // if there is not certificate, consider a token if (certificates == null) { // look for an authorization token in header or cookie - final String authorization = new NiFiBearerTokenResolver().resolve(httpServletRequest); + final String bearerToken = bearerTokenResolver.resolve(httpServletRequest); // if there is no authorization header, we don't know the user - if (authorization == null) { + if (bearerToken == null) { accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name()); accessStatus.setMessage("No credentials supplied, unknown user."); } else { try { // authenticate the token - final JwtAuthenticationRequestToken jwtRequest = new JwtAuthenticationRequestToken(authorization, httpServletRequest.getRemoteAddr()); - final NiFiAuthenticationToken authenticationResponse = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(jwtRequest); - final NiFiUser nifiUser = ((NiFiUserDetails) authenticationResponse.getDetails()).getNiFiUser(); + final BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(bearerToken); + final Authentication authentication = jwtAuthenticationProvider.authenticate(authenticationToken); + final NiFiUserDetails userDetails = (NiFiUserDetails) authentication.getPrincipal(); + final String identity = userDetails.getUsername(); // set the user identity - accessStatus.setIdentity(nifiUser.getIdentity()); + accessStatus.setIdentity(identity); // attempt authorize to /flow accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name()); accessStatus.setMessage("You are already logged in."); - } catch (final InvalidAuthenticationException iae) { - if (WebUtils.getCookie(httpServletRequest, NiFiBearerTokenResolver.JWT_COOKIE_NAME) != null) { - removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME); + } catch (final AuthenticationException iae) { + if (WebUtils.getCookie(httpServletRequest, SecurityCookieName.AUTHORIZATION_BEARER.getName()) != null) { + removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName()); } throw iae; @@ -281,7 +284,7 @@ public class AccessResource extends ApplicationResource { final X509AuthenticationRequestToken x509Request = new X509AuthenticationRequestToken( proxiedEntitiesChain, proxiedEntityGroups, principalExtractor, certificates, httpServletRequest.getRemoteAddr()); - final NiFiAuthenticationToken authenticationResponse = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(x509Request); + final Authentication authenticationResponse = x509AuthenticationProvider.authenticate(x509Request); final NiFiUser nifiUser = ((NiFiUserDetails) authenticationResponse.getDetails()).getNiFiUser(); // set the user identity @@ -351,8 +354,7 @@ public class AccessResource extends ApplicationResource { String authorizationHeaderValue = httpServletRequest.getHeader(KerberosService.AUTHORIZATION_HEADER_NAME); if (!kerberosService.isValidKerberosHeader(authorizationHeaderValue)) { - final Response response = generateNotAuthorizedResponse().header(KerberosService.AUTHENTICATION_CHALLENGE_HEADER_NAME, KerberosService.AUTHORIZATION_NEGOTIATE).build(); - return response; + return generateNotAuthorizedResponse().header(KerberosService.AUTHENTICATION_CHALLENGE_HEADER_NAME, KerberosService.AUTHORIZATION_NEGOTIATE).build(); } else { try { // attempt to authenticate @@ -363,18 +365,13 @@ public class AccessResource extends ApplicationResource { } final String expirationFromProperties = properties.getKerberosAuthenticationExpiration(); - long expiration = FormatUtils.getTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS); + long expiration = Math.round(FormatUtils.getPreciseTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS)); final String rawIdentity = authentication.getName(); String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); expiration = validateTokenExpiration(expiration, mappedIdentity); - // create the authentication token final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, "KerberosService"); - - // generate JWT for response - final String token = jwtService.generateSignedToken(loginAuthenticationToken); - - // build the response + final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken); final URI uri = URI.create(generateResourceUri("access", "kerberos")); return generateTokenResponse(generateCreatedResponse(uri, token), token); } catch (final AuthenticationException e) { @@ -447,10 +444,7 @@ public class AccessResource extends ApplicationResource { throw new AdministrationException(iae.getMessage(), iae); } - // generate JWT for response - final String token = jwtService.generateSignedToken(loginAuthenticationToken); - - // build the response + final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken); final URI uri = URI.create(generateResourceUri("access", "token")); return generateTokenResponse(generateCreatedResponse(uri, token), token); } @@ -482,10 +476,12 @@ public class AccessResource extends ApplicationResource { } try { - logger.info("Logging out " + mappedUserIdentity); - logOutUser(httpServletRequest); - removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME); - logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity); + logger.info("Logout Started [{}]", mappedUserIdentity); + logger.debug("Removing Authorization Cookie [{}]", mappedUserIdentity); + removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName()); + + final String bearerToken = bearerTokenResolver.resolve(httpServletRequest); + jwtLogoutListener.logout(bearerToken); // create a LogoutRequest and tell the LogoutRequestManager about it for later retrieval final LogoutRequest logoutRequest = new LogoutRequest(UUID.randomUUID().toString(), mappedUserIdentity); @@ -500,11 +496,8 @@ public class AccessResource extends ApplicationResource { httpServletResponse.addCookie(cookie); return generateOkResponse().build(); - } catch (final JwtException e) { - logger.error("JWT processing failed for [{}], due to: ", mappedUserIdentity, e.getMessage(), e); - return Response.serverError().build(); } catch (final LogoutException e) { - logger.error("Logout failed for user [{}] due to: ", mappedUserIdentity, e.getMessage(), e); + logger.error("Logout Failed Identity [{}]", mappedUserIdentity, e); return Response.serverError().build(); } } @@ -547,9 +540,9 @@ public class AccessResource extends ApplicationResource { } if (logoutRequest == null) { - logger.warn("Logout request did not exist for identifier: " + logoutRequestIdentifier); + logger.warn("Logout Request [{}] not found", logoutRequestIdentifier); } else { - logger.info("Completed logout request for " + logoutRequest.getMappedUserIdentity()); + logger.info("Logout Request [{}] Completed [{}]", logoutRequestIdentifier, logoutRequest.getMappedUserIdentity()); } // remove the cookie if it existed @@ -588,14 +581,22 @@ public class AccessResource extends ApplicationResource { this.loginIdentityProvider = loginIdentityProvider; } - public void setJwtService(JwtService jwtService) { - this.jwtService = jwtService; + public void setBearerTokenProvider(final BearerTokenProvider bearerTokenProvider) { + this.bearerTokenProvider = bearerTokenProvider; + } + + public void setBearerTokenResolver(final BearerTokenResolver bearerTokenResolver) { + this.bearerTokenResolver = bearerTokenResolver; } public void setJwtAuthenticationProvider(JwtAuthenticationProvider jwtAuthenticationProvider) { this.jwtAuthenticationProvider = jwtAuthenticationProvider; } + public void setJwtLogoutListener(final JwtLogoutListener jwtLogoutListener) { + this.jwtLogoutListener = jwtLogoutListener; + } + public void setKerberosService(KerberosService kerberosService) { this.kerberosService = kerberosService; } @@ -616,11 +617,6 @@ public class AccessResource extends ApplicationResource { this.knoxService = knoxService; } - private void logOutUser(HttpServletRequest httpServletRequest) { - final String jwt = new NiFiBearerTokenResolver().resolve(httpServletRequest); - jwtService.logOut(jwt); - } - public void setLogoutRequestManager(LogoutRequestManager logoutRequestManager) { this.logoutRequestManager = logoutRequestManager; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java index 3f482a0e74..f2f6cc3c57 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java @@ -55,7 +55,7 @@ import org.apache.nifi.web.api.entity.ComponentEntity; import org.apache.nifi.web.api.entity.Entity; import org.apache.nifi.web.api.entity.TransactionResultEntity; import org.apache.nifi.web.security.ProxiedEntitiesUtils; -import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver; +import org.apache.nifi.web.security.http.SecurityCookieName; import org.apache.nifi.web.security.util.CacheKey; import org.apache.nifi.web.util.WebUtils; import org.eclipse.jetty.http.HttpCookie; @@ -1285,7 +1285,7 @@ public abstract class ApplicationResource { protected Response generateTokenResponse(ResponseBuilder builder, String token) { // currently there is no way to use javax.servlet-api to set SameSite=Strict, so we do this using Jetty - HttpCookie jwtCookie = new HttpCookie(NiFiBearerTokenResolver.JWT_COOKIE_NAME, token, null, "/", VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT); + HttpCookie jwtCookie = new HttpCookie(SecurityCookieName.AUTHORIZATION_BEARER.getName(), token, null, "/", VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT); return builder.header(HttpHeader.SET_COOKIE.asString(), jwtCookie.getRFC6265SetCookie()).build(); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java index df6a1cbcef..475ee2081c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java @@ -39,8 +39,8 @@ import org.apache.http.message.BasicNameValuePair; import org.apache.nifi.authentication.exception.AuthenticationNotSupportedException; import org.apache.nifi.authorization.user.NiFiUserUtils; import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.security.jwt.JwtService; -import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver; +import org.apache.nifi.web.security.http.SecurityCookieName; +import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; import org.apache.nifi.web.security.oidc.OIDCEndpoints; import org.apache.nifi.web.security.oidc.OidcService; import org.apache.nifi.web.security.token.LoginAuthenticationToken; @@ -90,8 +90,8 @@ public class OIDCAccessResource extends AccessResource { private static final boolean LOGGING_IN = true; private OidcService oidcService; - private JwtService jwtService; - private CloseableHttpClient httpClient; + private BearerTokenProvider bearerTokenProvider; + private final CloseableHttpClient httpClient; public OIDCAccessResource() { RequestConfig config = RequestConfig.custom() @@ -161,10 +161,10 @@ public class OIDCAccessResource extends AccessResource { LoginAuthenticationToken oidcToken = oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(authorizationGrant); // exchange the oidc token for the NiFi token - String nifiJwt = jwtService.generateSignedToken(oidcToken); + final String bearerToken = bearerTokenProvider.getBearerToken(oidcToken); // store the NiFi token - oidcService.storeJwt(oidcRequestIdentifier, nifiJwt); + oidcService.storeJwt(oidcRequestIdentifier, bearerToken); } catch (final Exception e) { logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e); @@ -247,7 +247,7 @@ public class OIDCAccessResource extends AccessResource { } final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity(); - removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME); + removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName()); logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity); // Get the oidc discovery url @@ -559,8 +559,8 @@ public class OIDCAccessResource extends AccessResource { this.oidcService = oidcService; } - public void setJwtService(JwtService jwtService) { - this.jwtService = jwtService; + public void setBearerTokenProvider(final BearerTokenProvider bearerTokenProvider) { + this.bearerTokenProvider = bearerTokenProvider; } public void setProperties(final NiFiProperties properties) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml index 193423952c..856846feb2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml @@ -582,7 +582,9 @@ - + + + @@ -598,9 +600,9 @@ - + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java deleted file mode 100644 index a97a744585..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java +++ /dev/null @@ -1,422 +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.integration.accesscontrol; - -import java.util.Calendar; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.StringJoiner; -import javax.ws.rs.core.Response; -import net.minidev.json.JSONObject; -import org.apache.nifi.integration.util.SourceTestProcessor; -import org.apache.nifi.web.api.dto.AccessConfigurationDTO; -import org.apache.nifi.web.api.dto.AccessStatusDTO; -import org.apache.nifi.web.api.dto.ProcessorDTO; -import org.apache.nifi.web.api.dto.RevisionDTO; -import org.apache.nifi.web.api.entity.AccessConfigurationEntity; -import org.apache.nifi.web.api.entity.AccessStatusEntity; -import org.apache.nifi.web.api.entity.ProcessorEntity; -import org.apache.nifi.web.security.jwt.JwtServiceTest; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; - -/** - * Access token endpoint test. - */ -public class ITAccessTokenEndpoint { - - private static OneWaySslAccessControlHelper helper; - - private final String user = "nifiadmin@nifi.apache.org"; - private final String password = "password"; - private static final String CLIENT_ID = "token-endpoint-id"; - - @BeforeClass - public static void setup() throws Exception { - helper = new OneWaySslAccessControlHelper("src/test/resources/access-control/nifi-mapped-identities.properties"); - } - - // ----------- - // LOGIN CONFIG - // ----------- - /** - * Test getting access configuration. - * - * @throws Exception ex - */ - @Test - public void testGetAccessConfig() throws Exception { - String url = helper.getBaseUrl() + "/access/config"; - - Response response = helper.getUser().testGet(url); - - // ensure the request is successful - Assert.assertEquals(200, response.getStatus()); - - // extract the process group - AccessConfigurationEntity accessConfigEntity = response.readEntity(AccessConfigurationEntity.class); - - // ensure there is content - Assert.assertNotNull(accessConfigEntity); - - // extract the process group dto - AccessConfigurationDTO accessConfig = accessConfigEntity.getConfig(); - - // verify config - Assert.assertTrue(accessConfig.getSupportsLogin()); - } - - /** - * Obtains a token and creates a processor using it. - * - * @throws Exception ex - */ - @Test - public void testCreateProcessorUsingToken() throws Exception { - String url = helper.getBaseUrl() + "/access/token"; - - Response response = helper.getUser().testCreateToken(url, user, password); - - // ensure the request is successful - Assert.assertEquals(201, response.getStatus()); - - // get the token - String token = response.readEntity(String.class); - - // attempt to create a processor with it - createProcessor(token); - } - - private ProcessorDTO createProcessor(final String token) throws Exception { - String url = helper.getBaseUrl() + "/process-groups/root/processors"; - - // authorization header - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + token); - - // create the processor - ProcessorDTO processor = new ProcessorDTO(); - processor.setName("Copy"); - processor.setType(SourceTestProcessor.class.getName()); - - // create the revision - final RevisionDTO revision = new RevisionDTO(); - revision.setClientId(CLIENT_ID); - revision.setVersion(0l); - - // create the entity body - ProcessorEntity entity = new ProcessorEntity(); - entity.setRevision(revision); - entity.setComponent(processor); - - // perform the request - Response response = helper.getUser().testPostWithHeaders(url, entity, headers); - - // ensure the request is successful - Assert.assertEquals(201, response.getStatus()); - - // get the entity body - entity = response.readEntity(ProcessorEntity.class); - - // verify creation - processor = entity.getComponent(); - Assert.assertEquals("Copy", processor.getName()); - Assert.assertEquals("org.apache.nifi.integration.util.SourceTestProcessor", processor.getType()); - - return processor; - } - - /** - * Verifies the response when bad credentials are specified. - * - * @throws Exception ex - */ - @Test - public void testInvalidCredentials() throws Exception { - String url = helper.getBaseUrl() + "/access/token"; - - Response response = helper.getUser().testCreateToken(url, "user@nifi", "not a real password"); - - // ensure the request is not successful - Assert.assertEquals(400, response.getStatus()); - } - - /** - * Verifies the response when the user is known. - * - * @throws Exception ex - */ - @Test - public void testUnknownUser() throws Exception { - String url = helper.getBaseUrl() + "/access/token"; - - Response response = helper.getUser().testCreateToken(url, "not a real user", "not a real password"); - - // ensure the request is successful - Assert.assertEquals(400, response.getStatus()); - } - - /** - * Request access using access token. - * - * @throws Exception ex - */ - @Test - public void testRequestAccessUsingToken() throws Exception { - String accessStatusUrl = helper.getBaseUrl() + "/access"; - String accessTokenUrl = helper.getBaseUrl() + "/access/token"; - - Response response = helper.getUser().testGet(accessStatusUrl); - - // ensure the request is successful - Assert.assertEquals(200, response.getStatus()); - - AccessStatusEntity accessStatusEntity = response.readEntity(AccessStatusEntity.class); - AccessStatusDTO accessStatus = accessStatusEntity.getAccessStatus(); - - // verify unknown - Assert.assertEquals("UNKNOWN", accessStatus.getStatus()); - - response = helper.getUser().testCreateToken(accessTokenUrl, user, password); - - // ensure the request is successful - Assert.assertEquals(201, response.getStatus()); - - // get the token - String token = response.readEntity(String.class); - - // authorization header - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + token); - - // check the status with the token - response = helper.getUser().testGetWithHeaders(accessStatusUrl, null, headers); - - // ensure the request is successful - Assert.assertEquals(200, response.getStatus()); - - accessStatusEntity = response.readEntity(AccessStatusEntity.class); - accessStatus = accessStatusEntity.getAccessStatus(); - - // verify unregistered - Assert.assertEquals("ACTIVE", accessStatus.getStatus()); - } - - // // TODO: Revisit the HTTP status codes in this test after logout functionality change - // @Ignore("This test is failing before refactoring") - @Test - public void testLogOutSuccess() throws Exception { - String accessStatusUrl = helper.getBaseUrl() + "/access"; - String accessTokenUrl = helper.getBaseUrl() + "/access/token"; - String logoutUrl = helper.getBaseUrl() + "/access/logout"; - - Response response = helper.getUser().testGet(accessStatusUrl); - - // ensure the request is successful - Assert.assertEquals(200, response.getStatus()); - - AccessStatusEntity accessStatusEntity = response.readEntity(AccessStatusEntity.class); - AccessStatusDTO accessStatus = accessStatusEntity.getAccessStatus(); - - // verify unknown - Assert.assertEquals("UNKNOWN", accessStatus.getStatus()); - - response = helper.getUser().testCreateToken(accessTokenUrl, user, password); - - // ensure the request is successful - Assert.assertEquals(201, response.getStatus()); - - // get the token - String token = response.readEntity(String.class); - - // authorization header - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + token); - - // check the status with the token - response = helper.getUser().testGetWithHeaders(accessStatusUrl, null, headers); - - // ensure the request is successful - Assert.assertEquals(200, response.getStatus()); - - accessStatusEntity = response.readEntity(AccessStatusEntity.class); - accessStatus = accessStatusEntity.getAccessStatus(); - - // verify unregistered - Assert.assertEquals("ACTIVE", accessStatus.getStatus()); - - // log out - response = helper.getUser().testDeleteWithHeaders(logoutUrl, headers); - Assert.assertEquals(200, response.getStatus()); - - // ensure we can no longer use our token - response = helper.getUser().testGetWithHeaders(accessStatusUrl, null, headers); - Assert.assertEquals(401, response.getStatus()); - } - - @Test - public void testLogOutNoTokenHeader() throws Exception { - String accessStatusUrl = helper.getBaseUrl() + "/access"; - String accessTokenUrl = helper.getBaseUrl() + "/access/token"; - String logoutUrl = helper.getBaseUrl() + "/access/logout"; - - Response response = helper.getUser().testGet(accessStatusUrl); - - // ensure the request is successful - Assert.assertEquals(200, response.getStatus()); - - AccessStatusEntity accessStatusEntity = response.readEntity(AccessStatusEntity.class); - AccessStatusDTO accessStatus = accessStatusEntity.getAccessStatus(); - - // verify unknown - Assert.assertEquals("UNKNOWN", accessStatus.getStatus()); - - response = helper.getUser().testCreateToken(accessTokenUrl, user, password); - - // ensure the request is successful - Assert.assertEquals(201, response.getStatus()); - - // get the token - String token = response.readEntity(String.class); - - // authorization header - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + token); - - // check the status with the token - response = helper.getUser().testGetWithHeaders(accessStatusUrl, null, headers); - - // ensure the request is successful - Assert.assertEquals(200, response.getStatus()); - - accessStatusEntity = response.readEntity(AccessStatusEntity.class); - accessStatus = accessStatusEntity.getAccessStatus(); - - // verify unregistered - Assert.assertEquals("ACTIVE", accessStatus.getStatus()); - - - // log out should fail as we provided no token for logout to use - response = helper.getUser().testDeleteWithHeaders(logoutUrl, null); - Assert.assertEquals(401, response.getStatus()); - } - - @Test - public void testLogOutUnknownToken() throws Exception { - // Arrange - final String ALG_HEADER = "{\"alg\":\"HS256\"}"; - final int EXPIRATION_SECONDS = 60; - Calendar now = Calendar.getInstance(); - final long currentTime = (long) (now.getTimeInMillis() / 1000.0); - final long TOKEN_ISSUED_AT = currentTime; - final long TOKEN_EXPIRATION_SECONDS = currentTime + EXPIRATION_SECONDS; - - // Always use LinkedHashMap to enforce order of the keys because the signature depends on order - Map claims = new LinkedHashMap<>(); - claims.put("sub", "unknownuser"); - claims.put("iss", "MockIdentityProvider"); - claims.put("aud", "MockIdentityProvider"); - claims.put("preferred_username", "unknownuser"); - claims.put("kid", 1); - claims.put("exp", TOKEN_EXPIRATION_SECONDS); - claims.put("iat", TOKEN_ISSUED_AT); - final String EXPECTED_PAYLOAD = new JSONObject(claims).toString(); - - String accessStatusUrl = helper.getBaseUrl() + "/access"; - String accessTokenUrl = helper.getBaseUrl() + "/access/token"; - String logoutUrl = helper.getBaseUrl() + "/access/logout"; - - Response response = helper.getUser().testCreateToken(accessTokenUrl, user, password); - - // ensure the request is successful - Assert.assertEquals(201, response.getStatus()); - // get the token - String token = response.readEntity(String.class); - // authorization header - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + token); - // check the status with the token - response = helper.getUser().testGetWithHeaders(accessStatusUrl, null, headers); - Assert.assertEquals(200, response.getStatus()); - - // Generate a token that will not match signatures with the generated token. - final String UNKNOWN_USER_TOKEN = JwtServiceTest.generateHS256Token(ALG_HEADER, EXPECTED_PAYLOAD, true, true); - Map badHeaders = new HashMap<>(); - badHeaders.put("Authorization", "Bearer " + UNKNOWN_USER_TOKEN); - - // Log out should fail as we provide a bad token to use, signatures will mismatch - response = helper.getUser().testGetWithHeaders(logoutUrl, null, badHeaders); - Assert.assertEquals(401, response.getStatus()); - } - - @Test - public void testLogOutSplicedTokenSignature() throws Exception { - // Arrange - final String ALG_HEADER = "{\"alg\":\"HS256\"}"; - final int EXPIRATION_SECONDS = 60; - Calendar now = Calendar.getInstance(); - final long currentTime = (long) (now.getTimeInMillis() / 1000.0); - final long TOKEN_ISSUED_AT = currentTime; - final long TOKEN_EXPIRATION_SECONDS = currentTime + EXPIRATION_SECONDS; - - String accessTokenUrl = helper.getBaseUrl() + "/access/token"; - String logoutUrl = helper.getBaseUrl() + "/access/logout"; - - Response response = helper.getUser().testCreateToken(accessTokenUrl, user, password); - // ensure the request is successful - Assert.assertEquals(201, response.getStatus()); - // replace the user in the token with an unknown user - String realToken = response.readEntity(String.class); - String realSignature = realToken.split("\\.")[2]; - - // Generate a token that we will add a valid signature from a different token - // Always use LinkedHashMap to enforce order of the keys because the signature depends on order - Map claims = new LinkedHashMap<>(); - claims.put("sub", "unknownuser"); - claims.put("iss", "MockIdentityProvider"); - claims.put("aud", "MockIdentityProvider"); - claims.put("preferred_username", "unknownuser"); - claims.put("kid", 1); - claims.put("exp", TOKEN_EXPIRATION_SECONDS); - claims.put("iat", TOKEN_ISSUED_AT); - final String EXPECTED_PAYLOAD = new JSONObject(claims).toString(); - final String tempToken = JwtServiceTest.generateHS256Token(ALG_HEADER, EXPECTED_PAYLOAD, true, true); - - // Splice this token with the real token from above - String[] splitToken = tempToken.split("\\."); - StringJoiner joiner = new StringJoiner("."); - joiner.add(splitToken[0]); - joiner.add(splitToken[1]); - joiner.add(realSignature); - String splicedUserToken = joiner.toString(); - - Map badHeaders = new HashMap<>(); - badHeaders.put("Authorization", "Bearer " + splicedUserToken); - - // Log out should fail as we provide a bad token to use, signatures will mismatch - response = helper.getUser().testGetWithHeaders(logoutUrl, null, badHeaders); - Assert.assertEquals(401, response.getStatus()); - } - - @AfterClass - public static void cleanup() throws Exception { - helper.cleanup(); - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml index 667e017304..8135064da6 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml @@ -165,10 +165,6 @@ org.apache.nifi nifi-framework-authorization - - io.jsonwebtoken - jjwt - org.bouncycastle bcprov-jdk15on @@ -214,6 +210,18 @@ org.springframework.security.kerberos spring-security-kerberos-core + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-core + + + org.springframework.security + spring-security-oauth2-jose + org.slf4j jcl-over-slf4j @@ -234,6 +242,11 @@ org.glassfish.jersey.media jersey-media-json-jackson + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + org.codehaus.jettison jettison diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java index f07e43b260..391191fe8d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java @@ -16,7 +16,6 @@ */ package org.apache.nifi.web.security; -import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authorization.user.NiFiUserUtils; import org.apache.nifi.util.NiFiProperties; import org.slf4j.Logger; @@ -51,19 +50,16 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean { @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); - } + log.debug("Authenticating [{}]", authentication); - if (requiresAuthentication((HttpServletRequest) request)) { + if (requiresAuthentication()) { authenticate((HttpServletRequest) request, (HttpServletResponse) response, chain); } else { chain.doFilter(request, response); } - } - private boolean requiresAuthentication(final HttpServletRequest request) { + private boolean requiresAuthentication() { return NiFiUserUtils.getNiFiUser() == null; } @@ -71,9 +67,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean { 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())); + log.info("Authentication Started {} [{}] {} {}", request.getRemoteAddr(), authenticationRequest, request.getMethod(), request.getRequestURL()); // attempt to authenticate the user final Authentication authenticated = authenticationManager.authenticate(authenticationRequest); @@ -84,7 +78,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean { unsuccessfulAuthentication(request, response, ae); return; } catch (final Exception e) { - log.error(String.format("Unable to authenticate: %s", e.getMessage()), e); + log.error("Authentication Failed [{}]", e.getMessage(), e); // set the response status response.setContentType("text/plain"); @@ -92,7 +86,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean { // other exception - always error out PrintWriter out = response.getWriter(); - out.println(String.format("Failed to authenticate request. Please contact the system administrator.")); + out.println("Failed to authenticate request. Please contact the system administrator."); return; } @@ -117,7 +111,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean { * @param authResult The Authentication 'token'/object created by one of the various NiFiAuthenticationFilter subclasses. */ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { - log.info("Authentication success for " + authResult); + log.info("Authentication Success [{}] {} {} {}", authResult, request.getRemoteAddr(), request.getMethod(), request.getRequestURL()); SecurityContextHolder.getContext().setAuthentication(authResult); ProxiedEntitiesUtils.successfulAuthentication(request, response); @@ -149,26 +143,20 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean { response.setStatus(HttpServletResponse.SC_FORBIDDEN); out.println(ae.getMessage()); } else if (ae instanceof AuthenticationServiceException) { - log.error(String.format("Unable to authenticate: %s", ae.getMessage()), ae); + log.error("Authentication Service Failed: {}", ae.getMessage(), ae); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); out.println(String.format("Unable to authenticate: %s", ae.getMessage())); } else { - log.error(String.format("Unable to authenticate: %s", ae.getMessage()), ae); + log.error("Authentication Exception: {}", 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())); + log.warn("Authentication Failed {} {} {} [{}]", request.getRemoteAddr(), request.getMethod(), request.getRequestURL(), ae.getMessage()); // optionally log the stack trace - if (log.isDebugEnabled()) { - log.debug(StringUtils.EMPTY, ae); - } - } - - @Override - public void destroy() { + log.debug("Authentication Failed", ae); } public void setAuthenticationManager(AuthenticationManager authenticationManager) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java new file mode 100644 index 0000000000..85f025b369 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/AuthenticationSecurityConfiguration.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider; +import org.apache.nifi.web.security.logout.LogoutRequestManager; +import org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Spring Configuration for Authentication Security + */ +@Configuration +@Import({ + JwtAuthenticationSecurityConfiguration.class, + KerberosAuthenticationSecurityConfiguration.class, + KnoxAuthenticationSecurityConfiguration.class, + OidcAuthenticationSecurityConfiguration.class, + SamlAuthenticationSecurityConfiguration.class, + X509AuthenticationSecurityConfiguration.class +}) +public class AuthenticationSecurityConfiguration { + private final NiFiProperties niFiProperties; + + private final ExtensionManager extensionManager; + + private final Authorizer authorizer; + + @Autowired + public AuthenticationSecurityConfiguration( + final NiFiProperties niFiProperties, + final ExtensionManager extensionManager, + final Authorizer authorizer + ) { + this.niFiProperties = niFiProperties; + this.extensionManager = extensionManager; + this.authorizer = authorizer; + } + + @Bean + public LoginIdentityProviderFactoryBean loginIdentityProviderFactoryBean() { + final LoginIdentityProviderFactoryBean loginIdentityProviderFactoryBean = new LoginIdentityProviderFactoryBean(); + loginIdentityProviderFactoryBean.setProperties(niFiProperties); + loginIdentityProviderFactoryBean.setExtensionManager(extensionManager); + return loginIdentityProviderFactoryBean; + } + + @Bean + public Object loginIdentityProvider() throws Exception { + return loginIdentityProviderFactoryBean().getObject(); + } + + @Bean + public LogoutRequestManager logoutRequestManager() { + return new LogoutRequestManager(); + } + + @Bean + public NiFiAnonymousAuthenticationProvider anonymousAuthenticationProvider() { + return new NiFiAnonymousAuthenticationProvider(niFiProperties, authorizer); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java new file mode 100644 index 0000000000..7a88a722f2 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTClaimsSetVerifier; +import com.nimbusds.jwt.proc.JWTProcessor; +import org.apache.nifi.admin.service.IdpUserGroupService; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.components.state.StateManagerProvider; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.jwt.converter.StandardJwtAuthenticationConverter; +import org.apache.nifi.web.security.jwt.jws.StandardJWSKeySelector; +import org.apache.nifi.web.security.jwt.jws.StandardJwsSignerProvider; +import org.apache.nifi.web.security.jwt.key.command.KeyExpirationCommand; +import org.apache.nifi.web.security.jwt.key.command.KeyGenerationCommand; +import org.apache.nifi.web.security.jwt.key.StandardVerificationKeySelector; +import org.apache.nifi.web.security.jwt.key.service.StandardVerificationKeyService; +import org.apache.nifi.web.security.jwt.key.service.VerificationKeyService; +import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; +import org.apache.nifi.web.security.jwt.provider.StandardBearerTokenProvider; +import org.apache.nifi.web.security.jwt.provider.SupportedClaim; +import org.apache.nifi.web.security.jwt.revocation.JwtLogoutListener; +import org.apache.nifi.web.security.jwt.revocation.JwtRevocationService; +import org.apache.nifi.web.security.jwt.revocation.JwtRevocationValidator; +import org.apache.nifi.web.security.jwt.revocation.StandardJwtLogoutListener; +import org.apache.nifi.web.security.jwt.revocation.StandardJwtRevocationService; +import org.apache.nifi.web.security.jwt.revocation.command.RevocationExpirationCommand; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; + +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * JSON Web Token Configuration for Authentication Security + */ +@Configuration +public class JwtAuthenticationSecurityConfiguration { + private static final Set REQUIRED_CLAIMS = new HashSet<>(Arrays.asList( + SupportedClaim.ISSUER.getClaim(), + SupportedClaim.SUBJECT.getClaim(), + SupportedClaim.AUDIENCE.getClaim(), + SupportedClaim.EXPIRATION.getClaim(), + SupportedClaim.NOT_BEFORE.getClaim(), + SupportedClaim.ISSUED_AT.getClaim(), + SupportedClaim.JWT_ID.getClaim() + )); + + private final NiFiProperties niFiProperties; + + private final Authorizer authorizer; + + private final IdpUserGroupService idpUserGroupService; + + private final StateManagerProvider stateManagerProvider; + + private final Duration keyRotationPeriod; + + @Autowired + public JwtAuthenticationSecurityConfiguration( + final NiFiProperties niFiProperties, + final Authorizer authorizer, + final IdpUserGroupService idpUserGroupService, + final StateManagerProvider stateManagerProvider + ) { + this.niFiProperties = niFiProperties; + this.authorizer = authorizer; + this.idpUserGroupService = idpUserGroupService; + this.stateManagerProvider = stateManagerProvider; + this.keyRotationPeriod = niFiProperties.getSecurityUserJwsKeyRotationPeriod(); + } + + @Bean + public JwtAuthenticationProvider jwtAuthenticationProvider() { + final JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtDecoder()); + jwtAuthenticationProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter()); + return jwtAuthenticationProvider; + } + + @Bean + public JwtDecoder jwtDecoder() { + final NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(jwtProcessor()); + final OAuth2TokenValidator jwtValidator = new DelegatingOAuth2TokenValidator<>( + JwtValidators.createDefault(), + jwtRevocationValidator() + ); + jwtDecoder.setJwtValidator(jwtValidator); + return jwtDecoder; + } + + @Bean + public OAuth2TokenValidator jwtRevocationValidator() { + return new JwtRevocationValidator(jwtRevocationService()); + } + + @Bean + public JwtRevocationService jwtRevocationService() { + final StateManager stateManager = stateManagerProvider.getStateManager(StandardJwtRevocationService.class.getName()); + return new StandardJwtRevocationService(stateManager); + } + + @Bean + public JwtLogoutListener jwtLogoutListener() { + return new StandardJwtLogoutListener(jwtDecoder(), jwtRevocationService()); + } + + @Bean + public JWTProcessor jwtProcessor() { + final DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector()); + jwtProcessor.setJWTClaimsSetVerifier(claimsSetVerifier()); + return jwtProcessor; + } + + @Bean + public JWSKeySelector jwsKeySelector() { + return new StandardJWSKeySelector<>(verificationKeySelector()); + } + + @Bean + public JWTClaimsSetVerifier claimsSetVerifier() { + return new DefaultJWTClaimsVerifier<>(null, REQUIRED_CLAIMS); + } + + @Bean + public StandardJwtAuthenticationConverter jwtAuthenticationConverter() { + return new StandardJwtAuthenticationConverter(authorizer, idpUserGroupService, niFiProperties); + } + + @Bean + public BearerTokenProvider bearerTokenProvider() { + return new StandardBearerTokenProvider(jwsSignerProvider()); + } + + @Bean + public StandardJwsSignerProvider jwsSignerProvider() { + return new StandardJwsSignerProvider(verificationKeySelector()); + } + + @Bean + public StandardVerificationKeySelector verificationKeySelector() { + return new StandardVerificationKeySelector(verificationKeyService(), keyRotationPeriod); + } + + @Bean + public VerificationKeyService verificationKeyService() { + final StateManager stateManager = stateManagerProvider.getStateManager(StandardVerificationKeyService.class.getName()); + return new StandardVerificationKeyService(stateManager); + } + + @Bean + public KeyGenerationCommand keyGenerationCommand() { + final KeyGenerationCommand command = new KeyGenerationCommand(jwsSignerProvider(), verificationKeySelector()); + commandScheduler().scheduleAtFixedRate(command, keyRotationPeriod); + return command; + } + + @Bean + public KeyExpirationCommand keyExpirationCommand() { + final KeyExpirationCommand command = new KeyExpirationCommand(verificationKeyService()); + commandScheduler().scheduleAtFixedRate(command, keyRotationPeriod); + return command; + } + + @Bean + public RevocationExpirationCommand revocationExpirationCommand() { + final RevocationExpirationCommand command = new RevocationExpirationCommand(jwtRevocationService()); + commandScheduler().scheduleAtFixedRate(command, keyRotationPeriod); + return command; + } + + @Bean + public ThreadPoolTaskScheduler commandScheduler() { + final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix(getClass().getSimpleName()); + return scheduler; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KerberosAuthenticationSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KerberosAuthenticationSecurityConfiguration.java new file mode 100644 index 0000000000..5c07df4899 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KerberosAuthenticationSecurityConfiguration.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.kerberos.KerberosService; +import org.apache.nifi.web.security.spring.KerberosServiceFactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Kerberos Configuration for Authentication Security + */ +@Configuration +public class KerberosAuthenticationSecurityConfiguration { + private final NiFiProperties niFiProperties; + + @Autowired + public KerberosAuthenticationSecurityConfiguration( + final NiFiProperties niFiProperties + ) { + this.niFiProperties = niFiProperties; + } + + @Bean + public KerberosService kerberosService() throws Exception { + final KerberosServiceFactoryBean kerberosServiceFactoryBean = new KerberosServiceFactoryBean(); + kerberosServiceFactoryBean.setProperties(niFiProperties); + return kerberosServiceFactoryBean.getObject(); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KnoxAuthenticationSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KnoxAuthenticationSecurityConfiguration.java new file mode 100644 index 0000000000..c752bc70ce --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/KnoxAuthenticationSecurityConfiguration.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider; +import org.apache.nifi.web.security.knox.KnoxService; +import org.apache.nifi.web.security.knox.KnoxServiceFactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Knox Configuration for Authentication Security + */ +@Configuration +public class KnoxAuthenticationSecurityConfiguration { + private final NiFiProperties niFiProperties; + + private final Authorizer authorizer; + + @Autowired + public KnoxAuthenticationSecurityConfiguration( + final NiFiProperties niFiProperties, + final Authorizer authorizer + ) { + this.niFiProperties = niFiProperties; + this.authorizer = authorizer; + } + + @Bean + public KnoxAuthenticationProvider knoxAuthenticationProvider() { + return new KnoxAuthenticationProvider(knoxService(), niFiProperties, authorizer); + } + + @Bean + public KnoxService knoxService() { + final KnoxServiceFactoryBean knoxServiceFactoryBean = new KnoxServiceFactoryBean(); + knoxServiceFactoryBean.setProperties(niFiProperties); + return knoxServiceFactoryBean.getObject(); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcAuthenticationSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcAuthenticationSecurityConfiguration.java new file mode 100644 index 0000000000..06fbc035c4 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcAuthenticationSecurityConfiguration.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.oidc.OidcService; +import org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * OIDC Configuration for Authentication Security + */ +@Configuration +public class OidcAuthenticationSecurityConfiguration { + private final NiFiProperties niFiProperties; + + @Autowired + public OidcAuthenticationSecurityConfiguration( + final NiFiProperties niFiProperties + ) { + this.niFiProperties = niFiProperties; + } + + @Bean + public StandardOidcIdentityProvider oidcProvider() { + return new StandardOidcIdentityProvider(niFiProperties); + } + + @Bean + public OidcService oidcService() { + return new OidcService(oidcProvider()); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java new file mode 100644 index 0000000000..f421726dc6 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/SamlAuthenticationSecurityConfiguration.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.configuration; + +import org.apache.nifi.admin.service.IdpCredentialService; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; +import org.apache.nifi.web.security.saml.SAMLService; +import org.apache.nifi.web.security.saml.impl.StandardSAMLConfigurationFactory; +import org.apache.nifi.web.security.saml.impl.StandardSAMLCredentialStore; +import org.apache.nifi.web.security.saml.impl.StandardSAMLService; +import org.apache.nifi.web.security.saml.impl.StandardSAMLStateManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * SAML Configuration for Authentication Security + */ +@Configuration +public class SamlAuthenticationSecurityConfiguration { + private final NiFiProperties niFiProperties; + + private final BearerTokenProvider bearerTokenProvider; + + private final IdpCredentialService idpCredentialService; + + @Autowired + public SamlAuthenticationSecurityConfiguration( + final NiFiProperties niFiProperties, + final BearerTokenProvider bearerTokenProvider, + final IdpCredentialService idpCredentialService + ) { + this.niFiProperties = niFiProperties; + this.bearerTokenProvider = bearerTokenProvider; + this.idpCredentialService = idpCredentialService; + } + + @Bean(initMethod = "initialize", destroyMethod = "shutdown") + public SAMLService samlService() { + return new StandardSAMLService(samlConfigurationFactory(), niFiProperties); + } + + @Bean + public StandardSAMLStateManager samlStateManager() { + return new StandardSAMLStateManager(bearerTokenProvider); + } + + @Bean + public StandardSAMLCredentialStore samlCredentialStore() { + return new StandardSAMLCredentialStore(idpCredentialService); + } + + @Bean + public StandardSAMLConfigurationFactory samlConfigurationFactory() { + return new StandardSAMLConfigurationFactory(); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/X509AuthenticationSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/X509AuthenticationSecurityConfiguration.java new file mode 100644 index 0000000000..7e668c50f9 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/X509AuthenticationSecurityConfiguration.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.web.security.configuration; + +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.x509.SubjectDnX509PrincipalExtractor; +import org.apache.nifi.web.security.x509.X509AuthenticationProvider; +import org.apache.nifi.web.security.x509.X509CertificateExtractor; +import org.apache.nifi.web.security.x509.X509CertificateValidator; +import org.apache.nifi.web.security.x509.X509IdentityProvider; +import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +/** + * X.509 Configuration for Authentication Security + */ +@Configuration +public class X509AuthenticationSecurityConfiguration { + private final NiFiProperties niFiProperties; + + private final Authorizer authorizer; + + @Autowired + public X509AuthenticationSecurityConfiguration( + final NiFiProperties niFiProperties, + final Authorizer authorizer + ) { + this.niFiProperties = niFiProperties; + this.authorizer = authorizer; + } + + @Bean + public X509AuthenticationProvider x509AuthenticationProvider() { + return new X509AuthenticationProvider(certificateIdentityProvider(), authorizer, niFiProperties); + } + + @Bean + public X509CertificateExtractor certificateExtractor() { + return new X509CertificateExtractor(); + } + + @Bean + public X509PrincipalExtractor principalExtractor() { + return new SubjectDnX509PrincipalExtractor(); + } + + @Bean + public OcspCertificateValidator ocspValidator() { + return new OcspCertificateValidator(niFiProperties); + } + + @Bean + public X509CertificateValidator certificateValidator() { + final X509CertificateValidator certificateValidator = new X509CertificateValidator(); + certificateValidator.setOcspValidator(ocspValidator()); + return certificateValidator; + } + + @Bean + public X509IdentityProvider certificateIdentityProvider() { + final X509IdentityProvider identityProvider = new X509IdentityProvider(); + identityProvider.setCertificateValidator(certificateValidator()); + identityProvider.setPrincipalExtractor(principalExtractor()); + return identityProvider; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java similarity index 61% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java index 7ef2272083..96689c4501 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java @@ -14,27 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.admin.service.action; - -import org.apache.nifi.admin.dao.DAOFactory; -import org.apache.nifi.admin.dao.KeyDAO; -import org.apache.nifi.key.Key; +package org.apache.nifi.web.security.http; /** - * Gets a key for the specified key id. + * Enumeration of HTTP Cookie Names for Security */ -public class GetKeyByIdAction implements AdministrationAction { +public enum SecurityCookieName { + AUTHORIZATION_BEARER("__Host-Authorization-Bearer"); - private final int id; + private String name; - public GetKeyByIdAction(int id) { - this.id = id; + SecurityCookieName(final String name) { + this.name = name; } - @Override - public Key execute(DAOFactory daoFactory) { - final KeyDAO keyDao = daoFactory.getKeyDAO(); - return keyDao.findKeyById(id); + public String getName() { + return name; } - } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java similarity index 58% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java index 3dd37940a5..b1d36a62fb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java @@ -14,27 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.admin.service.action; - -import org.apache.nifi.admin.dao.DAOFactory; -import org.apache.nifi.admin.dao.KeyDAO; -import org.apache.nifi.key.Key; +package org.apache.nifi.web.security.http; /** - * Gets a key for the specified key id. + * Enumeration of HTTP Headers for Security */ -public class GetKeyByIdentityAction implements AdministrationAction { +public enum SecurityHeader { + AUTHORIZATION("Authorization"); - private final String identity; + private String header; - public GetKeyByIdentityAction(String identity) { - this.identity = identity; + SecurityHeader(final String header) { + this.header = header; } - @Override - public Key execute(DAOFactory daoFactory) { - final KeyDAO keyDao = daoFactory.getKeyDAO(); - return keyDao.findLatestKeyByIdentity(identity); + public String getHeader() { + return header; } - } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java deleted file mode 100644 index 6ac2401f35..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java +++ /dev/null @@ -1,50 +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.web.security.jwt; - -import org.apache.nifi.util.StringUtils; -import org.apache.nifi.web.security.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); - - // The Authorization header contains authentication credentials - private static NiFiBearerTokenResolver bearerTokenResolver = new NiFiBearerTokenResolver(); - - @Override - public Authentication attemptAuthentication(final HttpServletRequest request) { - // Only support JWT login when running securely - if (!request.isSecure()) { - return null; - } - - // Get JWT from Authorization header or cookie value - final String headerToken = bearerTokenResolver.resolve(request); - - if (StringUtils.isNotBlank(headerToken)) { - return new JwtAuthenticationRequestToken(headerToken, request.getRemoteAddr()); - } else { - return null; - } - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProvider.java deleted file mode 100644 index 135472d07b..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProvider.java +++ /dev/null @@ -1,83 +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.web.security.jwt; - -import io.jsonwebtoken.JwtException; -import org.apache.nifi.admin.service.IdpUserGroupService; -import org.apache.nifi.authorization.Authorizer; -import org.apache.nifi.authorization.user.NiFiUser; -import org.apache.nifi.authorization.user.NiFiUserDetails; -import org.apache.nifi.authorization.user.StandardNiFiUser.Builder; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.security.InvalidAuthenticationException; -import org.apache.nifi.web.security.NiFiAuthenticationProvider; -import org.apache.nifi.web.security.token.NiFiAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; - -import java.util.Set; -import java.util.stream.Collectors; - -/** - * - */ -public class JwtAuthenticationProvider extends NiFiAuthenticationProvider { - - private final JwtService jwtService; - private final IdpUserGroupService idpUserGroupService; - - public JwtAuthenticationProvider(JwtService jwtService, NiFiProperties nifiProperties, Authorizer authorizer, IdpUserGroupService idpUserGroupService) { - super(nifiProperties, authorizer); - this.jwtService = jwtService; - this.idpUserGroupService = idpUserGroupService; - } - - @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 Set userGroupProviderGroups = getUserGroups(mappedIdentity); - final Set idpUserGroups = getIdpUserGroups(mappedIdentity); - - final NiFiUser user = new Builder() - .identity(mappedIdentity) - .groups(userGroupProviderGroups) - .identityProviderGroups(idpUserGroups) - .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); - } - - private Set getIdpUserGroups(final String mappedIdentity) { - return idpUserGroupService.getUserGroups(mappedIdentity).stream() - .map(ug -> ug.getGroupName()) - .collect(Collectors.toSet()); - } - -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java deleted file mode 100644 index 5125bc5ece..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java +++ /dev/null @@ -1,59 +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.web.security.jwt; - -import org.apache.nifi.web.security.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 ""; - } - -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java deleted file mode 100644 index d235d499ae..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java +++ /dev/null @@ -1,208 +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.web.security.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.admin.service.AdministrationException; -import org.apache.nifi.admin.service.KeyService; -import org.apache.nifi.key.Key; -import org.apache.nifi.web.security.LogoutException; -import org.apache.nifi.web.security.token.LoginAuthenticationToken; -import org.slf4j.LoggerFactory; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Calendar; - -/** - * - */ -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 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"; - - // A common attack is someone trying to use a token after the user is logged out - // No need to show a stacktrace for an expected and handled scenario - String causeMessage = e.getLocalizedMessage(); - if (e.getCause() != null) { - causeMessage += "\n\tCaused by: " + e.getCause().getLocalizedMessage(); - } - if (logger.isDebugEnabled()) { - logger.error(errorMessage, e); - } else { - logger.error(errorMessage); - logger.error(causeMessage); - } - throw e; - } - } - - private Jws 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 Integer keyId = claims.get(KEY_ID_CLAIM, Integer.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(); - final String rawIssuer = authenticationToken.getIssuer(); - - 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); - - final String encodedIssuer = URLEncoder.encode(rawIssuer, "UTF-8"); - - // 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(encodedIssuer) - .setAudience(encodedIssuer) - .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); - } catch (UnsupportedEncodingException e) { - final String errorMessage = "Could not URL encode issuer: " + rawIssuer; - logger.error(errorMessage, e); - throw new JwtException(errorMessage, e); - } - } - - /** - * Log out the authenticated user using the 'kid' (Key ID) claim from the base64 encoded JWT - * - * @param token a signed, base64 encoded, JSON Web Token in form HEADER.PAYLOAD.SIGNATURE - * @throws JwtException if there is a problem with the token input - * @throws Exception if there is an issue logging the user out - */ - public void logOut(String token) throws LogoutException { - Jws claims = parseTokenFromBase64EncodedString(token); - - // Get the key ID from the claims - final Integer keyId = claims.getBody().get(KEY_ID_CLAIM, Integer.class); - - if (keyId == null) { - throw new JwtException("The key claim (kid) was not present in the request token to log out user."); - } - - try { - keyService.deleteKey(keyId); - } catch (Exception e) { - final String errorMessage = String.format("The key with key ID: %s failed to be removed from the user database.", keyId); - logger.error(errorMessage); - throw new LogoutException(errorMessage); - } - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java deleted file mode 100644 index c936c4d781..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java +++ /dev/null @@ -1,70 +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.web.security.jwt; - -import org.apache.nifi.util.StringUtils; -import org.apache.nifi.web.security.InvalidAuthenticationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.util.WebUtils; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class NiFiBearerTokenResolver implements BearerTokenResolver { - private static final Logger logger = LoggerFactory.getLogger(NiFiBearerTokenResolver.class); - private static final Pattern BEARER_HEADER_PATTERN = Pattern.compile("^Bearer (\\S*\\.\\S*\\.\\S*){1}$"); - private static final Pattern JWT_PATTERN = Pattern.compile("^(\\S*\\.\\S*\\.\\S*)$"); - public static final String AUTHORIZATION = "Authorization"; - public static final String JWT_COOKIE_NAME = "__Host-Authorization-Bearer"; - - @Override - public String resolve(HttpServletRequest request) { - final String authorizationHeader = request.getHeader(AUTHORIZATION); - final Cookie cookieHeader = WebUtils.getCookie(request, JWT_COOKIE_NAME); - - if (StringUtils.isNotBlank(authorizationHeader) && validAuthorizationHeaderFormat(authorizationHeader)) { - return getTokenFromHeader(authorizationHeader); - } else if(cookieHeader != null && validJwtFormat(cookieHeader.getValue())) { - return cookieHeader.getValue(); - } else { - logger.debug("Authorization header was not present or not in a valid format."); - return null; - } - } - - private boolean validAuthorizationHeaderFormat(String authorizationHeader) { - Matcher matcher = BEARER_HEADER_PATTERN.matcher(authorizationHeader); - return matcher.matches(); - } - - private boolean validJwtFormat(String jwt) { - Matcher matcher = JWT_PATTERN.matcher(jwt); - return matcher.matches(); - } - - private String getTokenFromHeader(String authenticationHeader) { - Matcher matcher = BEARER_HEADER_PATTERN.matcher(authenticationHeader); - if (matcher.matches()) { - return matcher.group(1); - } else { - throw new InvalidAuthenticationException("JWT did not match expected pattern."); - } - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverter.java new file mode 100644 index 0000000000..035ab2561e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverter.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.converter; + +import org.apache.nifi.admin.service.IdpUserGroupService; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserDetails; +import org.apache.nifi.authorization.user.StandardNiFiUser; +import org.apache.nifi.authorization.util.IdentityMapping; +import org.apache.nifi.authorization.util.IdentityMappingUtil; +import org.apache.nifi.authorization.util.UserGroupUtil; +import org.apache.nifi.idp.IdpUserGroup; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.token.NiFiAuthenticationToken; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Standard Converter from JSON Web Token to NiFi Authentication Token + */ +public class StandardJwtAuthenticationConverter implements Converter { + private final Authorizer authorizer; + + private final IdpUserGroupService idpUserGroupService; + + private final List identityMappings; + + public StandardJwtAuthenticationConverter(final Authorizer authorizer, final IdpUserGroupService idpUserGroupService, final NiFiProperties properties) { + this.authorizer = authorizer; + this.idpUserGroupService = idpUserGroupService; + this.identityMappings = IdentityMappingUtil.getIdentityMappings(properties); + } + + /** + * Convert JSON Web Token to NiFi Authentication Token + * + * @param jwt JSON Web Token + * @return NiFi Authentication Token + */ + @Override + public NiFiAuthenticationToken convert(final Jwt jwt) { + final NiFiUser user = getUser(jwt); + return new NiFiAuthenticationToken(new NiFiUserDetails(user)); + } + + private NiFiUser getUser(final Jwt jwt) { + final String identity = IdentityMappingUtil.mapIdentity(jwt.getSubject(), identityMappings); + + return new StandardNiFiUser.Builder() + .identity(identity) + .groups(UserGroupUtil.getUserGroups(authorizer, identity)) + .identityProviderGroups(getIdentityProviderGroups(identity)) + .build(); + } + + private Set getIdentityProviderGroups(final String identity) { + return idpUserGroupService.getUserGroups(identity).stream() + .map(IdpUserGroup::getGroupName) + .collect(Collectors.toSet()); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerContainer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerContainer.java new file mode 100644 index 0000000000..68abb91463 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerContainer.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.jws; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; + +import java.util.Objects; + +/** + * JSON Web Signature Signer Container + */ +public class JwsSignerContainer { + private final String keyIdentifier; + + private final JWSAlgorithm jwsAlgorithm; + + private final JWSSigner jwsSigner; + + public JwsSignerContainer(final String keyIdentifier, final JWSAlgorithm jwsAlgorithm, final JWSSigner jwsSigner) { + this.keyIdentifier = Objects.requireNonNull(keyIdentifier, "Key Identifier required"); + this.jwsAlgorithm = Objects.requireNonNull(jwsAlgorithm, "JWS Algorithm required"); + this.jwsSigner = Objects.requireNonNull(jwsSigner, "JWS Signer required"); + } + + public String getKeyIdentifier() { + return keyIdentifier; + } + + public JWSAlgorithm getJwsAlgorithm() { + return jwsAlgorithm; + } + + public JWSSigner getJwsSigner() { + return jwsSigner; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerProvider.java similarity index 54% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerProvider.java index c72d2c3018..c9cae4d9fb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/JwsSignerProvider.java @@ -14,31 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.admin.service.action; +package org.apache.nifi.web.security.jwt.jws; -import org.apache.nifi.admin.dao.DAOFactory; -import org.apache.nifi.admin.dao.DataAccessException; -import org.apache.nifi.admin.dao.KeyDAO; +import java.time.Instant; /** - * + * JSON Web Signature Signer Provider supports accessing signer and identifier properties */ -public class DeleteKeyAction implements AdministrationAction { - - private final Integer keyId; - +public interface JwsSignerProvider { /** - * Creates a new transactions for deleting keys for a specified user based on their keyId. + * Get JSON Web Signature Signer Container * - * @param keyId user identity + * @param expiration New JSON Web Token Expiration to be set for the returned Signer + * @return JSON Web Signature Signer Container */ - public DeleteKeyAction(Integer keyId) { - this.keyId = keyId; - } - - @Override - public Integer execute(DAOFactory daoFactory) throws DataAccessException { - final KeyDAO keyDao = daoFactory.getKeyDAO(); - return keyDao.deleteKey(keyId); - } + JwsSignerContainer getJwsSignerContainer(Instant expiration); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/SignerListener.java similarity index 65% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/SignerListener.java index caf8789d94..31fb67e6dc 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/SignerListener.java @@ -14,17 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.web.security.jwt; +package org.apache.nifi.web.security.jwt.jws; -import javax.servlet.http.HttpServletRequest; - -public interface BearerTokenResolver { +/** + * Listener handling JWS Signer events + */ +public interface SignerListener { /** - * Resolve any - * Bearer - * Token value from the request. - * @param request the request - * @return the Bearer Token value or {@code null} if none found + * On Signer Updated + * + * @param jwsSignerContainer JWS Signer Container */ - String resolve(HttpServletRequest request); + void onSignerUpdated(JwsSignerContainer jwsSignerContainer); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/SigningKeyListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/SigningKeyListener.java new file mode 100644 index 0000000000..41d924d948 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/SigningKeyListener.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.jws; + +import java.time.Instant; + +/** + * Listener handling Signing Key events + */ +public interface SigningKeyListener { + /** + * On Signing Key Used + * + * @param keyIdentifier Key Identifier + * @param expiration JSON Web Token Expiration + */ + void onSigningKeyUsed(String keyIdentifier, Instant expiration); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJWSKeySelector.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJWSKeySelector.java new file mode 100644 index 0000000000..02c901d307 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJWSKeySelector.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.jws; + +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import org.apache.nifi.web.security.jwt.key.VerificationKeySelector; + +import java.security.Key; +import java.util.List; + +/** + * Standard JSON Web Signature Key Selector for selecting keys using Key Identifier + * @param Security Context + */ +public class StandardJWSKeySelector implements JWSKeySelector { + private final VerificationKeySelector verificationKeySelector; + + /** + * Standard JSON Web Signature Key Selector constructor requires a Verification Key Selector + * @param verificationKeySelector Verification Key Selector + */ + public StandardJWSKeySelector(final VerificationKeySelector verificationKeySelector) { + this.verificationKeySelector = verificationKeySelector; + } + + /** + * Select JSON Web Signature Key using Key Identifier from JWS Header + * + * @param jwsHeader JSON Web Signature Header + * @param context Context not used + * @return List of found java.security.Key objects + */ + @Override + public List selectJWSKeys(final JWSHeader jwsHeader, final C context) { + final String keyId = jwsHeader.getKeyID(); + return verificationKeySelector.getVerificationKeys(keyId); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProvider.java new file mode 100644 index 0000000000..7912e67fa9 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProvider.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.jws; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Standard JSON Web Signature Signer Provider + */ +public class StandardJwsSignerProvider implements JwsSignerProvider, SignerListener { + private static final Logger LOGGER = LoggerFactory.getLogger(StandardJwsSignerProvider.class); + + private final AtomicReference currentSigner = new AtomicReference<>(); + + private final SigningKeyListener signingKeyListener; + + public StandardJwsSignerProvider(final SigningKeyListener signingKeyListener) { + this.signingKeyListener = signingKeyListener; + } + + /** + * Get Current JWS Signer Container and update expiration + * + * @param expiration New JSON Web Token Expiration to be set for the returned Signing Key + * @return JWS Signer Container + */ + @Override + public JwsSignerContainer getJwsSignerContainer(final Instant expiration) { + final JwsSignerContainer jwsSignerContainer = currentSigner.get(); + if (jwsSignerContainer == null) { + throw new IllegalStateException("JSON Web Signature Signer not configured"); + } + final String keyIdentifier = jwsSignerContainer.getKeyIdentifier(); + LOGGER.debug("Signer Used with Key Identifier [{}]", keyIdentifier); + signingKeyListener.onSigningKeyUsed(keyIdentifier, expiration); + return jwsSignerContainer; + } + + /** + * On Signer Updated changes the current JWS Signer + * + * @param jwsSignerContainer JWS Signer Container + */ + @Override + public void onSignerUpdated(final JwsSignerContainer jwsSignerContainer) { + LOGGER.debug("Signer Updated with Key Identifier [{}]", jwsSignerContainer.getKeyIdentifier()); + currentSigner.set(jwsSignerContainer); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/StandardVerificationKeySelector.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/StandardVerificationKeySelector.java new file mode 100644 index 0000000000..d3f7d98463 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/StandardVerificationKeySelector.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key; + +import org.apache.nifi.web.security.jwt.jws.SigningKeyListener; +import org.apache.nifi.web.security.jwt.key.service.VerificationKeyService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Key; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Standard Verification Key Selector implements listener interfaces for updating Key status + */ +public class StandardVerificationKeySelector implements SigningKeyListener, VerificationKeyListener, VerificationKeySelector { + private static final Logger LOGGER = LoggerFactory.getLogger(StandardVerificationKeySelector.class); + + private final VerificationKeyService verificationKeyService; + + private final Duration keyRotationPeriod; + + public StandardVerificationKeySelector(final VerificationKeyService verificationKeyService, final Duration keyRotationPeriod) { + this.verificationKeyService = Objects.requireNonNull(verificationKeyService, "Verification Key Service required"); + this.keyRotationPeriod = Objects.requireNonNull(keyRotationPeriod, "Key Rotation Period required"); + } + + /** + * On Verification Key Generated persist encoded Key with expiration + * + * @param keyIdentifier Key Identifier + * @param key Key + */ + @Override + public void onVerificationKeyGenerated(final String keyIdentifier, final Key key) { + final Instant expiration = Instant.now().plus(keyRotationPeriod); + verificationKeyService.save(keyIdentifier, key, expiration); + LOGGER.debug("Verification Key Saved [{}] Expiration [{}]", keyIdentifier, expiration); + } + + /** + * Get Verification Keys + * + * @param keyIdentifier Key Identifier + * @return List of Keys + */ + @Override + public List getVerificationKeys(final String keyIdentifier) { + final Optional key = verificationKeyService.findById(keyIdentifier); + final List keys = key.map(Collections::singletonList).orElse(Collections.emptyList()); + LOGGER.debug("Key Identifier [{}] Verification Keys Found [{}]", keyIdentifier, keys.size()); + return keys; + } + + /** + * On Signing Key Used set new expiration + * + * @param keyIdentifier Key Identifier + * @param expiration JSON Web Token Expiration + */ + @Override + public void onSigningKeyUsed(final String keyIdentifier, final Instant expiration) { + LOGGER.debug("Signing Key Used [{}] Expiration [{}]", keyIdentifier, expiration); + verificationKeyService.setExpiration(keyIdentifier, expiration); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeyListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeyListener.java new file mode 100644 index 0000000000..630376d121 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeyListener.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key; + +import java.security.Key; + +/** + * Listener handling Verification Key events + */ +public interface VerificationKeyListener { + /** + * On Verification Key Generated + * + * @param keyIdentifier Key Identifier + * @param key Key used for Verification + */ + void onVerificationKeyGenerated(String keyIdentifier, Key key); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeySelector.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeySelector.java new file mode 100644 index 0000000000..ebabf3a1a3 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/VerificationKeySelector.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key; + +import java.security.Key; +import java.util.List; + +/** + * Verification Key Selector returns a List of java.security.Key objects for signature verification + */ +public interface VerificationKeySelector { + /** + * Get Verification Keys for Key Identifier + * + * @param keyIdentifier Key Identifier + * @return List of found java.security.Key objects + */ + List getVerificationKeys(String keyIdentifier); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyExpirationCommand.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyExpirationCommand.java new file mode 100644 index 0000000000..bfd6eccb79 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyExpirationCommand.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.web.security.jwt.key.command; + +import org.apache.nifi.web.security.jwt.key.service.VerificationKeyService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +/** + * Key Expiration Command removes expired Verification Keys + */ +public class KeyExpirationCommand implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(KeyExpirationCommand.class); + + private final VerificationKeyService verificationKeyService; + + public KeyExpirationCommand(final VerificationKeyService verificationKeyService) { + this.verificationKeyService = Objects.requireNonNull(verificationKeyService, "Verification Key Service required"); + } + + /** + * Run deletes expired Verification Keys + */ + @Override + public void run() { + LOGGER.debug("Delete Expired Verification Keys Started"); + verificationKeyService.deleteExpired(); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java new file mode 100644 index 0000000000..d1bf809af0 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommand.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key.command; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer; +import org.apache.nifi.web.security.jwt.jws.SignerListener; +import org.apache.nifi.web.security.jwt.key.VerificationKeyListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.UUID; + +/** + * Key Generation Command produces new RSA Key Pairs and configures a JWS Signer + */ +public class KeyGenerationCommand implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(KeyGenerationCommand.class); + + private static final String KEY_ALGORITHM = "RSA"; + + private static final int KEY_SIZE = 4096; + + private static final JWSAlgorithm JWS_ALGORITHM = JWSAlgorithm.PS512; + + private final KeyPairGenerator keyPairGenerator; + + private final SignerListener signerListener; + + private final VerificationKeyListener verificationKeyListener; + + public KeyGenerationCommand(final SignerListener signerListener, final VerificationKeyListener verificationKeyListener) { + this.signerListener = Objects.requireNonNull(signerListener, "Signer Listener required"); + this.verificationKeyListener = Objects.requireNonNull(verificationKeyListener, "Verification Key Listener required"); + try { + keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); + keyPairGenerator.initialize(KEY_SIZE, new SecureRandom()); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Run generates a new Key Pair and notifies configured listeners + */ + @Override + public void run() { + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + final String keyIdentifier = UUID.randomUUID().toString(); + LOGGER.debug("Generated Key Pair [{}] Key Identifier [{}]", KEY_ALGORITHM, keyIdentifier); + + verificationKeyListener.onVerificationKeyGenerated(keyIdentifier, keyPair.getPublic()); + + final JWSSigner jwsSigner = new RSASSASigner(keyPair.getPrivate()); + signerListener.onSignerUpdated(new JwsSignerContainer(keyIdentifier, JWS_ALGORITHM, jwsSigner)); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyService.java new file mode 100644 index 0000000000..4a877a4089 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyService.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.apache.nifi.components.state.Scope; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.components.state.StateMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Standard Verification Key Service implemented using State Manager + */ +public class StandardVerificationKeyService implements VerificationKeyService { + private static final Logger LOGGER = LoggerFactory.getLogger(StandardVerificationKeyService.class); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); + + private static final Scope SCOPE = Scope.LOCAL; + + private final StateManager stateManager; + + public StandardVerificationKeyService(final StateManager stateManager) { + this.stateManager = stateManager; + } + + /** + * Find Key using specified Key Identifier + * + * @param id Key Identifier + * @return Optional Key + */ + @Override + public Optional findById(final String id) { + final Optional serializedKey = findSerializedKey(id); + return serializedKey.map(this::getVerificationKey).map(this::getKey); + } + + /** + * Delete Expired Verification Keys is synchronized to avoid losing updates from other methods + */ + @Override + public synchronized void deleteExpired() { + final Map state = getStateMap().toMap(); + + final Instant now = Instant.now(); + final Map updatedState = state + .values() + .stream() + .map(this::getVerificationKey) + .filter(verificationKey -> verificationKey.getExpiration().isAfter(now)) + .collect(Collectors.toMap(VerificationKey::getId, this::serializeVerificationKey)); + + if (updatedState.equals(state)) { + LOGGER.debug("Expired Verification Keys not found"); + } else { + try { + stateManager.setState(updatedState, SCOPE); + } catch (final IOException e) { + throw new UncheckedIOException("Delete Expired Verification Keys Failed", e); + } + LOGGER.debug("Delete Expired Verification Keys Completed: Keys Before [{}] Keys After [{}]", state.size(), updatedState.size()); + } + } + + /** + * Save Verification Key + * + * @param id Key Identifier + * @param key Key + * @param expiration Expiration + */ + @Override + public void save(final String id, final Key key, final Instant expiration) { + final VerificationKey verificationKey = new VerificationKey(); + verificationKey.setId(id); + verificationKey.setEncoded(key.getEncoded()); + verificationKey.setAlgorithm(key.getAlgorithm()); + verificationKey.setExpiration(expiration); + setVerificationKey(verificationKey); + } + + /** + * Set Expiration of Verification Key when found + * + * @param id Key Identifier + * @param expiration Expiration + */ + @Override + public void setExpiration(final String id, final Instant expiration) { + final Optional serializedKey = findSerializedKey(id); + if (serializedKey.isPresent()) { + final VerificationKey verificationKey = getVerificationKey(serializedKey.get()); + verificationKey.setExpiration(expiration); + setVerificationKey(verificationKey); + } + } + + /** + * Set Verification Key is synchronized to avoid competing updates to the State Map + * + * @param verificationKey Verification Key to be stored + */ + private synchronized void setVerificationKey(final VerificationKey verificationKey) { + try { + final String serialized = serializeVerificationKey(verificationKey); + final Map state = new HashMap<>(getStateMap().toMap()); + state.put(verificationKey.getId(), serialized); + stateManager.setState(state, SCOPE); + } catch (final IOException e) { + throw new UncheckedIOException("Set Verification Key State Failed", e); + } + LOGGER.debug("Stored Verification Key [{}] Expiration [{}]", verificationKey.getId(), verificationKey.getExpiration()); + } + + private Optional findSerializedKey(final String id) { + final StateMap stateMap = getStateMap(); + return Optional.ofNullable(stateMap.get(id)); + } + + private String serializeVerificationKey(final VerificationKey verificationKey) { + try { + return OBJECT_MAPPER.writeValueAsString(verificationKey); + } catch (final JsonProcessingException e) { + throw new UncheckedIOException("Serialize Verification Key Failed", e); + } + } + + private VerificationKey getVerificationKey(final String serialized) { + try { + return OBJECT_MAPPER.readValue(serialized, VerificationKey.class); + } catch (final JsonProcessingException e) { + throw new UncheckedIOException("Read Verification Key Failed", e); + } + } + + private Key getKey(final VerificationKey verificationKey) { + final KeySpec keySpec = new X509EncodedKeySpec(verificationKey.getEncoded()); + final String algorithm = verificationKey.getAlgorithm(); + try { + final KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + return keyFactory.generatePublic(keySpec); + } catch (final InvalidKeySpecException | NoSuchAlgorithmException e) { + final String message = String.format("Parsing Encoded Key [%s] Algorithm [%s] Failed", verificationKey.getId(), algorithm); + throw new IllegalStateException(message, e); + } + } + + private StateMap getStateMap() { + try { + return stateManager.getState(SCOPE); + } catch (final IOException e) { + throw new UncheckedIOException("Get State Failed", e); + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKey.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKey.java new file mode 100644 index 0000000000..5d2d53889d --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKey.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key.service; + +import java.time.Instant; + +/** + * Verification Key used for storing serialized instances + */ +class VerificationKey { + private String id; + + private String algorithm; + + private byte[] encoded; + + private Instant expiration; + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(final String algorithm) { + this.algorithm = algorithm; + } + + public byte[] getEncoded() { + return encoded; + } + + public void setEncoded(final byte[] encoded) { + this.encoded = encoded; + } + + public Instant getExpiration() { + return expiration; + } + + public void setExpiration(final Instant expiration) { + this.expiration = expiration; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKeyService.java similarity index 53% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKeyService.java index 3cfaf2fc8b..c880938c54 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/key/service/VerificationKeyService.java @@ -14,43 +14,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.admin.dao; +package org.apache.nifi.web.security.jwt.key.service; -import org.apache.nifi.key.Key; +import java.security.Key; +import java.time.Instant; +import java.util.Optional; /** - * Key data access. + * Verification Key Service for storing and retrieving keys */ -public interface KeyDAO { +public interface VerificationKeyService { + /** + * Find Key using specified Key Identifier + * + * @param id Key Identifier + * @return Optional Key + */ + Optional findById(String id); /** - * Gets the key for the specified user identity. Returns null if no key exists for the key id. - * - * @param id The key id - * @return The key or null + * Delete Expired Keys */ - Key findKeyById(int id); + void deleteExpired(); /** - * Gets the latest key for the specified identity. Returns null if no key exists for the user identity. + * Save Key with associated expiration * - * @param identity The identity - * @return The key or null + * @param id Key Identifier + * @param key Key + * @param expiration Expiration */ - Key findLatestKeyByIdentity(String identity); + void save(String id, Key key, Instant expiration); /** - * Creates a key for the specified user identity. + * Set Expiration for specified Key Identifier * - * @param identity The user identity - * @return The key + * @param id Key Identifier + * @param expiration Expiration */ - Key createKey(String identity); - - /** - * Deletes a key using the key ID. - * - * @param keyId The key ID - */ - Integer deleteKey(Integer keyId); + void setExpiration(String id, Instant expiration); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/BearerTokenProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/BearerTokenProvider.java new file mode 100644 index 0000000000..fd98ae2c8c --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/BearerTokenProvider.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.provider; + +import org.apache.nifi.web.security.token.LoginAuthenticationToken; + +/** + * Bearer Token Provider supports generation of Access Tokens used for Bearer Authentication + */ +public interface BearerTokenProvider { + /** + * Get Bearer Token + * + * @param loginAuthenticationToken Login Authentication Token + * @return Bearer Token + */ + String getBearerToken(LoginAuthenticationToken loginAuthenticationToken); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java new file mode 100644 index 0000000000..95185b67fc --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProvider.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.provider; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.Payload; +import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer; +import org.apache.nifi.web.security.jwt.jws.JwsSignerProvider; +import org.apache.nifi.web.security.token.LoginAuthenticationToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Objects; +import java.util.UUID; + +/** + * Standard Bearer Token Provider supports returning serialized and signed JSON Web Tokens + */ +public class StandardBearerTokenProvider implements BearerTokenProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(StandardBearerTokenProvider.class); + + private static final String URL_ENCODED_CHARACTER_SET = StandardCharsets.UTF_8.name(); + + private final JwsSignerProvider jwsSignerProvider; + + public StandardBearerTokenProvider(final JwsSignerProvider jwsSignerProvider) { + this.jwsSignerProvider = jwsSignerProvider; + } + + /** + * Get Signed JSON Web Token using Login Authentication Token + * + * @param loginAuthenticationToken Login Authentication Token + * @return Serialized Signed JSON Web Token + */ + @Override + public String getBearerToken(final LoginAuthenticationToken loginAuthenticationToken) { + Objects.requireNonNull(loginAuthenticationToken, "LoginAuthenticationToken required"); + final String subject = Objects.requireNonNull(loginAuthenticationToken.getPrincipal(), "Principal required").toString(); + final String username = loginAuthenticationToken.getName(); + + final String issuer = getUrlEncoded(loginAuthenticationToken.getIssuer()); + final Date now = new Date(); + final Date expirationTime = new Date(loginAuthenticationToken.getExpiration()); + final JWTClaimsSet claims = new JWTClaimsSet.Builder() + .jwtID(UUID.randomUUID().toString()) + .subject(subject) + .issuer(issuer) + .audience(issuer) + .notBeforeTime(now) + .issueTime(now) + .expirationTime(expirationTime) + .claim(SupportedClaim.PREFERRED_USERNAME.getClaim(), username) + .build(); + return getSignedBearerToken(claims); + } + + private String getSignedBearerToken(final JWTClaimsSet claims) { + final Date expirationTime = claims.getExpirationTime(); + final JwsSignerContainer jwsSignerContainer = jwsSignerProvider.getJwsSignerContainer(expirationTime.toInstant()); + + final String keyIdentifier = jwsSignerContainer.getKeyIdentifier(); + final JWSAlgorithm algorithm = jwsSignerContainer.getJwsAlgorithm(); + final JWSHeader header = new JWSHeader.Builder(algorithm).keyID(keyIdentifier).build(); + final Payload payload = new Payload(claims.toJSONObject()); + final JWSObject jwsObject = new JWSObject(header, payload); + + final JWSSigner signer = jwsSignerContainer.getJwsSigner(); + try { + jwsObject.sign(signer); + } catch (final JOSEException e) { + final String message = String.format("Signing Failed for Algorithm [%s] Key Identifier [%s]", algorithm, keyIdentifier); + throw new IllegalArgumentException(message, e); + } + + LOGGER.debug("Signed Bearer Token using Key [{}] for Subject [{}]", keyIdentifier, claims.getSubject()); + return jwsObject.serialize(); + } + + private String getUrlEncoded(final String string) { + try { + return URLEncoder.encode(string, URL_ENCODED_CHARACTER_SET); + } catch (final UnsupportedEncodingException e) { + throw new IllegalArgumentException(String.format("URL Encoding [%s] Failed", string), e); + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/SupportedClaim.java similarity index 51% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/SupportedClaim.java index 8c862265c9..d71e41d269 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/provider/SupportedClaim.java @@ -14,33 +14,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.admin.service.action; - -import org.apache.nifi.admin.dao.DAOFactory; -import org.apache.nifi.admin.dao.KeyDAO; -import org.apache.nifi.key.Key; +package org.apache.nifi.web.security.jwt.provider; /** - * Gets a key for the specified user identity. + * Supported Claim for JSON Web Tokens */ -public class GetOrCreateKeyAction implements AdministrationAction { +public enum SupportedClaim { + /** RFC 7519 Section 4.1.1 */ + ISSUER("iss"), - private final String identity; + /** RFC 7519 Section 4.1.2 */ + SUBJECT("sub"), - public GetOrCreateKeyAction(String identity) { - this.identity = identity; + /** RFC 7519 Section 4.1.3 */ + AUDIENCE("aud"), + + /** RFC 7519 Section 4.1.4 */ + EXPIRATION("exp"), + + /** RFC 7519 Section 4.1.5 */ + NOT_BEFORE("nbf"), + + /** RFC 7519 Section 4.1.6 */ + ISSUED_AT("iat"), + + /** RFC 7519 Section 4.1.7 */ + JWT_ID("jti"), + + /** Preferred Username defined in OpenID Connect Core 1.0 Standard Claims */ + PREFERRED_USERNAME("preferred_username"); + + private final String claim; + + SupportedClaim(final String claim) { + this.claim = claim; } - @Override - public Key execute(DAOFactory daoFactory) { - final KeyDAO keyDao = daoFactory.getKeyDAO(); - - Key key = keyDao.findLatestKeyByIdentity(identity); - if (key == null) { - key = keyDao.createKey(identity); - } - - return key; + public String getClaim() { + return claim; } - } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolver.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolver.java new file mode 100644 index 0000000000..a5707dd5b0 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolver.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.resolver; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.web.security.http.SecurityCookieName; +import org.apache.nifi.web.security.http.SecurityHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.web.util.WebUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +/** + * Bearer Token Resolver prefers the HTTP Authorization Header and then evaluates the Authorization Cookie when found + */ +public class StandardBearerTokenResolver implements BearerTokenResolver { + private static final Logger LOGGER = LoggerFactory.getLogger(StandardBearerTokenResolver.class); + private static final String BEARER_PREFIX = "Bearer "; + + /** + * Resolve Bearer Token from HTTP Request checking Authorization Header then Authorization Cookie when found + * + * @param request HTTP Servlet Request + * @return Bearer Token or null when not found + */ + @Override + public String resolve(final HttpServletRequest request) { + String bearerToken = null; + + final String header = request.getHeader(SecurityHeader.AUTHORIZATION.getHeader()); + if (StringUtils.startsWithIgnoreCase(header, BEARER_PREFIX)) { + bearerToken = StringUtils.removeStartIgnoreCase(header, BEARER_PREFIX); + } else { + final Cookie cookie = WebUtils.getCookie(request, SecurityCookieName.AUTHORIZATION_BEARER.getName()); + if (cookie == null) { + LOGGER.trace("Bearer Token not found in Header or Cookie"); + } else { + bearerToken = cookie.getValue(); + } + } + + return bearerToken; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtLogoutListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtLogoutListener.java new file mode 100644 index 0000000000..3525836255 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtLogoutListener.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.revocation; + +/** + * JSON Web Token Logout Listener + */ +public interface JwtLogoutListener { + /** + * Logout Bearer Token + * + * @param bearerToken Bearer Token + */ + void logout(String bearerToken); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationService.java similarity index 55% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationService.java index 5ac10cb730..e46b6b60d3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationService.java @@ -14,36 +14,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.admin.service; +package org.apache.nifi.web.security.jwt.revocation; -import org.apache.nifi.key.Key; +import java.time.Instant; /** - * Manages NiFi user keys. + * JSON Web Token Revocation Service */ -public interface KeyService { +public interface JwtRevocationService { + /** + * Delete Expired Revocations + */ + void deleteExpired(); /** - * Gets a key for the specified user identity. Returns null if the user has not had a key issued + * Is JSON Web Token Identifier Revoked * - * @param id The key id - * @return The key or null + * @param id JSON Web Token Identifier + * @return Revoked Status */ - Key getKey(int id); + boolean isRevoked(String id); /** - * Gets a key for the specified user identity. If a key does not exist, one will be created. + * Set Revoked Status using JSON Web Token Identifier * - * @param identity The user identity - * @return The key - * @throws AdministrationException if it failed to get/create the key + * @param id JSON Web Token Identifier + * @param expiration Expiration of Revocation Status after which the status record can be removed */ - Key getOrCreateKey(String identity); - - /** - * Deletes keys for the specified identity. - * - * @param keyId The user's key ID - */ - void deleteKey(Integer keyId); + void setRevoked(String id, Instant expiration); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidator.java new file mode 100644 index 0000000000..5acdc4b2d2 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidator.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.web.security.jwt.revocation; + +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrors; + +import static org.springframework.security.oauth2.core.OAuth2TokenValidatorResult.success; +import static org.springframework.security.oauth2.core.OAuth2TokenValidatorResult.failure; + +/** + * JSON Web Token Validator checks the JWT Identifier against a Revocation Service + */ +public class JwtRevocationValidator implements OAuth2TokenValidator { + private static final BearerTokenError REVOKED_ERROR = BearerTokenErrors.invalidToken("Access Token Revoked"); + + private static final OAuth2TokenValidatorResult FAILURE_RESULT = failure(REVOKED_ERROR); + + private final JwtRevocationService jwtRevocationService; + + public JwtRevocationValidator(final JwtRevocationService jwtRevocationService) { + this.jwtRevocationService = jwtRevocationService; + } + + /** + * Validate checks JSON Web Token Identifier against Revocation Service + * + * @param jwt JSON Web Token Identifier + * @return Validator Result based on Revoked Status + */ + @Override + public OAuth2TokenValidatorResult validate(final Jwt jwt) { + return jwtRevocationService.isRevoked(jwt.getId()) ? FAILURE_RESULT : success(); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListener.java new file mode 100644 index 0000000000..331e3cc7ad --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListener.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.revocation; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; + +/** + * Standard JSON Web Token Logout Listener handles parsing and revocation + */ +public class StandardJwtLogoutListener implements JwtLogoutListener { + /** JWT Decoder */ + private final JwtDecoder jwtDecoder; + + /** JWT Revocation Service */ + private final JwtRevocationService jwtRevocationService; + + public StandardJwtLogoutListener(final JwtDecoder jwtDecoder, final JwtRevocationService jwtRevocationService) { + this.jwtDecoder = jwtDecoder; + this.jwtRevocationService = jwtRevocationService; + } + + /** + * Logout Bearer Token sets revoked status using the JSON Web Token Identifier + * + * @param bearerToken Bearer Token + */ + @Override + public void logout(final String bearerToken) { + if (StringUtils.isNotBlank(bearerToken)) { + final Jwt jwt = jwtDecoder.decode(bearerToken); + jwtRevocationService.setRevoked(jwt.getId(), jwt.getExpiresAt()); + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationService.java new file mode 100644 index 0000000000..449731d0e9 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationService.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.revocation; + +import org.apache.nifi.components.state.Scope; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.components.state.StateMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Standard JSON Web Token Revocation Service using State Manager + */ +public class StandardJwtRevocationService implements JwtRevocationService { + private static final Logger LOGGER = LoggerFactory.getLogger(StandardJwtRevocationService.class); + + private static final Scope SCOPE = Scope.LOCAL; + + private final StateManager stateManager; + + public StandardJwtRevocationService(final StateManager stateManager) { + this.stateManager = stateManager; + } + + /** + * Delete Expired Revocations is synchronized is avoid losing updates from setRevoked() + */ + @Override + public synchronized void deleteExpired() { + final Map state = getStateMap().toMap(); + + final Instant now = Instant.now(); + final Map updatedState = state + .entrySet() + .stream() + .filter(entry -> Instant.parse(entry.getValue()).isAfter(now)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + if (updatedState.equals(state)) { + LOGGER.debug("Expired Revocations not found"); + } else { + try { + stateManager.setState(updatedState, SCOPE); + } catch (final IOException e) { + throw new UncheckedIOException("Delete Expired Revocations Failed", e); + } + LOGGER.debug("Delete Expired Revocations: Before [{}] After [{}]", state.size(), updatedState.size()); + } + } + + /** + * Is JSON Web Token Identifier Revoked based on State Map Status + * + * @param id JSON Web Token Identifier + * @return Revoked Status + */ + @Override + public boolean isRevoked(final String id) { + final StateMap stateMap = getStateMap(); + return stateMap.toMap().containsKey(id); + } + + /** + * Set Revoked Status is synchronized to avoid competing changes to the State Map + * + * @param id JSON Web Token Identifier + * @param expiration Expiration of Revocation Status after which the status record can be removed + */ + @Override + public synchronized void setRevoked(final String id, final Instant expiration) { + final StateMap stateMap = getStateMap(); + final Map state = new HashMap<>(stateMap.toMap()); + state.put(id, expiration.toString()); + try { + stateManager.setState(state, SCOPE); + LOGGER.debug("JWT Identifier [{}] Revocation Completed", id); + } catch (final IOException e) { + LOGGER.error("JWT Identifier [{}] Revocation Failed", id, e); + } + } + + private StateMap getStateMap() { + try { + return stateManager.getState(SCOPE); + } catch (final IOException e) { + throw new UncheckedIOException("Get State Failed", e); + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/command/RevocationExpirationCommand.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/command/RevocationExpirationCommand.java new file mode 100644 index 0000000000..af520d05eb --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/revocation/command/RevocationExpirationCommand.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.web.security.jwt.revocation.command; + +import org.apache.nifi.web.security.jwt.revocation.JwtRevocationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +/** + * Revocation Expiration Command removes expired Revocations + */ +public class RevocationExpirationCommand implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(RevocationExpirationCommand.class); + + private final JwtRevocationService jwtRevocationService; + + public RevocationExpirationCommand(final JwtRevocationService jwtRevocationService) { + this.jwtRevocationService = Objects.requireNonNull(jwtRevocationService, "JWT Revocation Service required"); + } + + /** + * Run deletes expired Revocations + */ + @Override + public void run() { + LOGGER.debug("Delete Expired Revocations Started"); + jwtRevocationService.deleteExpired(); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/knox/KnoxServiceFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/knox/KnoxServiceFactoryBean.java index 7a3aa07413..9c68be94c4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/knox/KnoxServiceFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/knox/KnoxServiceFactoryBean.java @@ -25,7 +25,7 @@ public class KnoxServiceFactoryBean implements FactoryBean { private NiFiProperties properties = null; @Override - public KnoxService getObject() throws Exception { + public KnoxService getObject() { if (knoxService == null) { // ensure we only allow knox if login and oidc are disabled if (properties.isKnoxSsoEnabled() && (properties.isLoginIdentityProviderEnabled() || properties.isOidcEnabled() || properties.isSamlEnabled())) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java index fc939af403..eaadd552b0 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java @@ -68,7 +68,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authentication.exception.IdentityAccessException; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.security.jwt.JwtService; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,8 +81,7 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider { private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProvider.class); private final String EMAIL_CLAIM = "email"; - private NiFiProperties properties; - private JwtService jwtService; + private final NiFiProperties properties; private OIDCProviderMetadata oidcProviderMetadata; private int oidcConnectTimeout; private int oidcReadTimeout; @@ -94,12 +92,10 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider { /** * Creates a new StandardOidcIdentityProvider. * - * @param jwtService jwt service * @param properties properties */ - public StandardOidcIdentityProvider(final JwtService jwtService, final NiFiProperties properties) { + public StandardOidcIdentityProvider(final NiFiProperties properties) { this.properties = properties; - this.jwtService = jwtService; } /** @@ -457,10 +453,8 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider { final Date expiration = claimsSet.getExpirationTime(); final long expiresIn = expiration.getTime() - now.getTimeInMillis(); - // Convert into a NiFi JWT for retrieval later - final LoginAuthenticationToken loginToken = new LoginAuthenticationToken( + return new LoginAuthenticationToken( identity, identity, expiresIn, claimsSet.getIssuer().getValue()); - return loginToken; } private OIDCTokens getOidcTokens(OIDCTokenResponse response) { @@ -510,14 +504,13 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider { private static List getAvailableClaims(JWTClaimsSet claimSet) { // Get the claims available in the ID token response - List presentClaims = claimSet.getClaims().entrySet().stream() + return claimSet.getClaims().entrySet().stream() // Check claim values are not empty - .filter(e -> StringUtils.isNotBlank(e.getValue().toString())) + .filter(e -> e.getValue() != null && StringUtils.isNotBlank(e.getValue().toString())) // If not empty, put claim name in a map .map(Map.Entry::getKey) .sorted() .collect(Collectors.toList()); - return presentClaims; } private void validateAccessToken(OIDCTokens oidcTokens) throws Exception { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java index 0df84eab9c..9995fc7c94 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java @@ -19,7 +19,7 @@ package org.apache.nifi.web.security.saml.impl; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.apache.nifi.util.StringUtils; -import org.apache.nifi.web.security.jwt.JwtService; +import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; import org.apache.nifi.web.security.saml.SAMLStateManager; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.apache.nifi.web.security.util.CacheKey; @@ -34,7 +34,7 @@ public class StandardSAMLStateManager implements SAMLStateManager { private static Logger LOGGER = LoggerFactory.getLogger(StandardSAMLStateManager.class); - private JwtService jwtService; + private BearerTokenProvider bearerTokenProvider; // identifier from cookie -> state value private final Cache stateLookupForPendingRequests; @@ -42,12 +42,12 @@ public class StandardSAMLStateManager implements SAMLStateManager { // identifier from cookie -> jwt or identity (and generate jwt on retrieval) private final Cache jwtLookupForCompletedRequests; - public StandardSAMLStateManager(final JwtService jwtService) { - this(jwtService, 60, TimeUnit.SECONDS); + public StandardSAMLStateManager(final BearerTokenProvider bearerTokenProvider) { + this(bearerTokenProvider, 60, TimeUnit.SECONDS); } - public StandardSAMLStateManager(final JwtService jwtService, final int cacheExpiration, final TimeUnit units) { - this.jwtService = jwtService; + public StandardSAMLStateManager(final BearerTokenProvider bearerTokenProvider, final int cacheExpiration, final TimeUnit units) { + this.bearerTokenProvider = bearerTokenProvider; this.stateLookupForPendingRequests = CacheBuilder.newBuilder().expireAfterWrite(cacheExpiration, units).build(); this.jwtLookupForCompletedRequests = CacheBuilder.newBuilder().expireAfterWrite(cacheExpiration, units).build(); } @@ -108,12 +108,12 @@ public class StandardSAMLStateManager implements SAMLStateManager { } final CacheKey requestIdentifierKey = new CacheKey(requestIdentifier); - final String nifiJwt = jwtService.generateSignedToken(token); + final String bearerToken = bearerTokenProvider.getBearerToken(token); try { // cache the jwt for later retrieval synchronized (jwtLookupForCompletedRequests) { - final String cachedJwt = jwtLookupForCompletedRequests.get(requestIdentifierKey, () -> nifiJwt); - if (!IdentityProviderUtils.timeConstantEqualityCheck(nifiJwt, cachedJwt)) { + final String cachedJwt = jwtLookupForCompletedRequests.get(requestIdentifierKey, () -> bearerToken); + if (!IdentityProviderUtils.timeConstantEqualityCheck(bearerToken, cachedJwt)) { throw new IllegalStateException("An existing login request is already in progress."); } } @@ -139,8 +139,4 @@ public class StandardSAMLStateManager implements SAMLStateManager { return jwt; } } - - public void setJwtService(JwtService jwtService) { - this.jwtService = jwtService; - } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java index b156804dcb..75120aeede 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java @@ -35,8 +35,6 @@ import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.properties.SensitivePropertyProviderFactoryAware; import org.apache.nifi.security.xml.XmlUtils; import org.apache.nifi.util.NiFiProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.xml.sax.SAXException; @@ -65,7 +63,6 @@ import java.util.Map; public class LoginIdentityProviderFactoryBean extends SensitivePropertyProviderFactoryAware implements FactoryBean, DisposableBean, LoginIdentityProviderLookup { - private static final Logger logger = LoggerFactory.getLogger(LoginIdentityProviderFactoryBean.class); private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/login-identity-providers.xsd"; private static final String JAXB_GENERATED_PATH = "org.apache.nifi.authentication.generated"; private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml deleted file mode 100644 index fe9d59fc70..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy index 011788f8b5..086f742fcc 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy @@ -21,13 +21,7 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod import com.nimbusds.oauth2.sdk.id.Issuer import com.nimbusds.openid.connect.sdk.SubjectType import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm -import org.apache.nifi.admin.service.KeyService -import org.apache.nifi.key.Key import org.apache.nifi.util.NiFiProperties -import org.apache.nifi.web.security.jwt.JwtService -import org.apache.nifi.web.security.token.LoginAuthenticationToken import org.junit.After import org.junit.Before import org.junit.BeforeClass @@ -43,7 +37,6 @@ import java.util.concurrent.TimeUnit class OidcServiceGroovyTest extends GroovyTestCase { private static final Logger logger = LoggerFactory.getLogger(OidcServiceGroovyTest.class) - private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value") private static final Map DEFAULT_NIFI_PROPERTIES = [ "nifi.security.user.oidc.discovery.url" : "https://localhost/oidc", "nifi.security.user.login.identity.provider" : "provider", @@ -58,7 +51,6 @@ class OidcServiceGroovyTest extends GroovyTestCase { // Mock collaborators private static NiFiProperties mockNiFiProperties - private static JwtService mockJwtService = [:] as JwtService private static StandardOidcIdentityProvider soip private static final String MOCK_REQUEST_IDENTIFIER = "mock-request-identifier" @@ -79,7 +71,7 @@ class OidcServiceGroovyTest extends GroovyTestCase { @Before void setUp() throws Exception { mockNiFiProperties = buildNiFiProperties() - soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties) + soip = new StandardOidcIdentityProvider(mockNiFiProperties) } @After @@ -91,34 +83,6 @@ class OidcServiceGroovyTest extends GroovyTestCase { new NiFiProperties(combinedProps) } - private static JwtService buildJwtService() { - def mockJS = new JwtService([:] as KeyService) { - @Override - String generateSignedToken(LoginAuthenticationToken lat) { - signNiFiToken(lat) - } - } - mockJS - } - - private static String signNiFiToken(LoginAuthenticationToken lat) { - String identity = "mockUser" - String USERNAME_CLAIM = "username" - String KEY_ID_CLAIM = "keyId" - Calendar expiration = Calendar.getInstance() - expiration.setTimeInMillis(System.currentTimeMillis() + 10_000) - String username = lat.getName() - - return Jwts.builder().setSubject(identity) - .setIssuer(lat.getIssuer()) - .setAudience(lat.getIssuer()) - .claim(USERNAME_CLAIM, username) - .claim(KEY_ID_CLAIM, SIGNING_KEY.getId()) - .setExpiration(expiration.getTime()) - .setIssuedAt(Calendar.getInstance().getTime()) - .signWith(SignatureAlgorithm.HS256, SIGNING_KEY.key.getBytes("UTF-8")).compact() - } - @Test void testShouldStoreJwt() { // Arrange @@ -189,7 +153,6 @@ class OidcServiceGroovyTest extends GroovyTestCase { } private static StandardOidcIdentityProvider buildIdentityProviderWithMockInitializedProvider(Map additionalProperties = [:]) { - JwtService mockJS = buildJwtService() NiFiProperties mockNFP = buildNiFiProperties(additionalProperties) // Mock OIDC provider metadata @@ -197,7 +160,7 @@ class OidcServiceGroovyTest extends GroovyTestCase { URI mockURI = new URI("https://localhost/oidc") OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJS, mockNFP) { + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNFP) { @Override void initializeProvider() { soip.oidcProviderMetadata = metadata diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy index 089f28a147..c84affcfa8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy @@ -43,16 +43,10 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata import com.nimbusds.openid.connect.sdk.token.OIDCTokens import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator import groovy.json.JsonOutput -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm import net.minidev.json.JSONObject import org.apache.commons.codec.binary.Base64 -import org.apache.nifi.admin.service.KeyService -import org.apache.nifi.key.Key import org.apache.nifi.util.NiFiProperties import org.apache.nifi.util.StringUtils -import org.apache.nifi.web.security.jwt.JwtService -import org.apache.nifi.web.security.token.LoginAuthenticationToken import org.junit.After import org.junit.Before import org.junit.BeforeClass @@ -66,7 +60,6 @@ import org.slf4j.LoggerFactory class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProviderGroovyTest.class) - private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value") private static final Map DEFAULT_NIFI_PROPERTIES = [ "nifi.security.user.oidc.discovery.url" : "https://localhost/oidc", "nifi.security.user.login.identity.provider" : "provider", @@ -81,7 +74,6 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { // Mock collaborators private static NiFiProperties mockNiFiProperties - private static JwtService mockJwtService = [:] as JwtService private static final String MOCK_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ" + "SI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZp" + @@ -109,34 +101,6 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { new NiFiProperties(combinedProps) } - private static JwtService buildJwtService() { - def mockJS = new JwtService([:] as KeyService) { - @Override - String generateSignedToken(LoginAuthenticationToken lat) { - signNiFiToken(lat) - } - } - mockJS - } - - private static String signNiFiToken(LoginAuthenticationToken lat) { - String identity = "mockUser" - String USERNAME_CLAIM = "username" - String KEY_ID_CLAIM = "keyId" - Calendar expiration = Calendar.getInstance() - expiration.setTimeInMillis(System.currentTimeMillis() + 10_000) - String username = lat.getName() - - return Jwts.builder().setSubject(identity) - .setIssuer(lat.getIssuer()) - .setAudience(lat.getIssuer()) - .claim(USERNAME_CLAIM, username) - .claim(KEY_ID_CLAIM, SIGNING_KEY.getId()) - .setExpiration(expiration.getTime()) - .setIssuedAt(Calendar.getInstance().getTime()) - .signWith(SignatureAlgorithm.HS256, SIGNING_KEY.key.getBytes("UTF-8")).compact() - } - @Test void testShouldGetAvailableClaims() { // Arrange @@ -168,7 +132,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { @Test void testShouldCreateClientAuthenticationFromPost() { // Arrange - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties) Issuer mockIssuer = new Issuer("https://localhost/oidc") URI mockURI = new URI("https://localhost/oidc") @@ -205,7 +169,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { void testShouldCreateClientAuthenticationFromBasic() { // Arrange // Mock collaborators - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties) Issuer mockIssuer = new Issuer("https://localhost/oidc") URI mockURI = new URI("https://localhost/oidc") @@ -241,7 +205,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { @Test void testShouldCreateTokenHTTPRequest() { // Arrange - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties) // Mock AuthorizationGrant Issuer mockIssuer = new Issuer("https://localhost/oidc") @@ -280,7 +244,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { @Test void testShouldLookupIdentityInUserInfo() { // Arrange - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties) Issuer mockIssuer = new Issuer("https://localhost/oidc") URI mockURI = new URI("https://localhost/oidc") @@ -304,7 +268,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { @Test void testLookupIdentityUserInfoShouldHandleMissingIdentity() { // Arrange - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties) Issuer mockIssuer = new Issuer("https://localhost/oidc") URI mockURI = new URI("https://localhost/oidc") @@ -329,7 +293,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { @Test void testLookupIdentityUserInfoShouldHandle500() { // Arrange - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties) Issuer mockIssuer = new Issuer("https://localhost/oidc") URI mockURI = new URI("https://localhost/oidc") @@ -698,9 +662,8 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { } private StandardOidcIdentityProvider buildIdentityProviderWithMockTokenValidator(Map additionalProperties = [:]) { - JwtService mockJS = buildJwtService() NiFiProperties mockNFP = buildNiFiProperties(additionalProperties) - StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJS, mockNFP) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNFP) // Mock OIDC provider metadata Issuer mockIssuer = new Issuer("mockIssuer") diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java deleted file mode 100644 index b4a85f38fb..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java +++ /dev/null @@ -1,282 +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.web.security.jwt; - -import org.apache.nifi.admin.service.IdpUserGroupService; -import org.apache.nifi.authorization.AccessPolicyProvider; -import org.apache.nifi.authorization.Authorizer; -import org.apache.nifi.authorization.Group; -import org.apache.nifi.authorization.ManagedAuthorizer; -import org.apache.nifi.authorization.User; -import org.apache.nifi.authorization.UserAndGroups; -import org.apache.nifi.authorization.UserGroupProvider; -import org.apache.nifi.authorization.user.NiFiUser; -import org.apache.nifi.authorization.user.NiFiUserDetails; -import org.apache.nifi.idp.IdpType; -import org.apache.nifi.idp.IdpUserGroup; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.security.InvalidAuthenticationException; -import org.apache.nifi.web.security.token.LoginAuthenticationToken; -import org.apache.nifi.web.security.token.NiFiAuthenticationToken; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Properties; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class JwtAuthenticationProviderTest { - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - private final static int EXPIRATION_MILLIS = 60000; - private final static String CLIENT_ADDRESS = "127.0.0.1"; - private final static String ADMIN_IDENTITY = "nifiadmin"; - private final static String REALMED_ADMIN_KERBEROS_IDENTITY = "nifiadmin@nifi.apache.org"; - - private final static String UNKNOWN_TOKEN = "eyJhbGciOiJIUzI1NiJ9" + - ".eyJzdWIiOiJ1bmtub3duX3Rva2VuIiwiaXNzIjoiS2VyYmVyb3NQcm9" + - "2aWRlciIsImF1ZCI6IktlcmJlcm9zUHJvdmlkZXIiLCJwcmVmZXJyZWR" + - "fdXNlcm5hbWUiOiJ1bmtub3duX3Rva2VuIiwia2lkIjoxLCJleHAiOjE" + - "2OTI0NTQ2NjcsImlhdCI6MTU5MjQxMTQ2N30.PpOGx3Ul5ydokOOuzKd" + - "aRKv1kxy6Q4jGy7rBPU8PqxY"; - - private NiFiProperties properties; - - - private JwtService jwtService; - private Authorizer authorizer; - private IdpUserGroupService idpUserGroupService; - - private JwtAuthenticationProvider jwtAuthenticationProvider; - - @Before - public void setUp() throws Exception { - TestKeyService keyService = new TestKeyService(); - jwtService = new JwtService(keyService); - idpUserGroupService = mock(IdpUserGroupService.class); - authorizer = mock(Authorizer.class); - - // Set up Kerberos identity mappings - Properties props = new Properties(); - props.put(properties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX, "^(.*?)@(.*?)$"); - props.put(properties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX, "$1"); - properties = new NiFiProperties(props); - - jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtService, properties, authorizer, idpUserGroupService); - } - - @Test - public void testAdminIdentityAndTokenIsValid() throws Exception { - // Arrange - LoginAuthenticationToken loginAuthenticationToken = - new LoginAuthenticationToken(ADMIN_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - String token = jwtService.generateSignedToken(loginAuthenticationToken); - final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS); - - when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Collections.emptyList()); - - // Act - final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); - final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal(); - - // Assert - assertEquals(ADMIN_IDENTITY, details.getUsername()); - } - - @Test - public void testKerberosRealmedIdentityAndTokenIsValid() throws Exception { - // Arrange - LoginAuthenticationToken loginAuthenticationToken = - new LoginAuthenticationToken(REALMED_ADMIN_KERBEROS_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - String token = jwtService.generateSignedToken(loginAuthenticationToken); - final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS); - - when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Collections.emptyList()); - - // Act - final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); - final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal(); - - // Assert - // Check we now have the mapped identity - assertEquals(ADMIN_IDENTITY, details.getUsername()); - } - - @Test - public void testFailToAuthenticateWithUnknownToken() throws Exception { - // Arrange - expectedException.expect(InvalidAuthenticationException.class); - expectedException.expectMessage("Unable to validate the access token."); - - // Generate a token with a known token - LoginAuthenticationToken loginAuthenticationToken = - new LoginAuthenticationToken(ADMIN_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - jwtService.generateSignedToken(loginAuthenticationToken); - - when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Collections.emptyList()); - - // Act - // Try to authenticate with an unknown token - final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(UNKNOWN_TOKEN, CLIENT_ADDRESS); - final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); - - // Assert - // Expect exception - } - - @Test - public void testIdpUserGroupsPresent() { - // Arrange - LoginAuthenticationToken loginAuthenticationToken = - new LoginAuthenticationToken(ADMIN_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - String token = jwtService.generateSignedToken(loginAuthenticationToken); - final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS); - - final String groupName1 = "group1"; - final IdpUserGroup idpUserGroup1 = createIdpUserGroup(1, ADMIN_IDENTITY, groupName1, IdpType.SAML); - - final String groupName2 = "group2"; - final IdpUserGroup idpUserGroup2 = createIdpUserGroup(2, ADMIN_IDENTITY, groupName2, IdpType.SAML); - - when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Arrays.asList(idpUserGroup1, idpUserGroup2)); - - // Act - final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); - final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal(); - - // Assert details username is correct - assertEquals(ADMIN_IDENTITY, details.getUsername()); - - final NiFiUser returnedUser = details.getNiFiUser(); - assertNotNull(returnedUser); - - // Assert user-group-provider groups is empty - assertNull(returnedUser.getGroups()); - - // Assert identity-provider groups is correct - assertEquals(2, returnedUser.getIdentityProviderGroups().size()); - assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName1)); - assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName2)); - - // Assert combined groups has only idp groups - assertEquals(2, returnedUser.getAllGroups().size()); - assertTrue(returnedUser.getAllGroups().contains(groupName1)); - assertTrue(returnedUser.getAllGroups().contains(groupName2)); - } - - @Test - public void testCombineUserGroupProviderGroupsAndIdpUserGroups() { - // setup IdpUserGroupService... - - final String groupName1 = "group1"; - final IdpUserGroup idpUserGroup1 = createIdpUserGroup(1, ADMIN_IDENTITY, groupName1, IdpType.SAML); - - final String groupName2 = "group2"; - final IdpUserGroup idpUserGroup2 = createIdpUserGroup(2, ADMIN_IDENTITY, groupName2, IdpType.SAML); - - idpUserGroupService = mock(IdpUserGroupService.class); - when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Arrays.asList(idpUserGroup1, idpUserGroup2)); - - // setup ManagedAuthorizer... - final String groupName3 = "group3"; - final Group group3 = new Group.Builder().identifierGenerateRandom().name(groupName3).build(); - - final UserGroupProvider userGroupProvider = mock(UserGroupProvider.class); - when(userGroupProvider.getUserAndGroups(ADMIN_IDENTITY)).thenReturn(new UserAndGroups() { - @Override - public User getUser() { - return new User.Builder().identifier(ADMIN_IDENTITY).identity(ADMIN_IDENTITY).build(); - } - - @Override - public Set getGroups() { - return Collections.singleton(group3); - } - }); - - final AccessPolicyProvider accessPolicyProvider = mock(AccessPolicyProvider.class); - when(accessPolicyProvider.getUserGroupProvider()).thenReturn(userGroupProvider); - - final ManagedAuthorizer managedAuthorizer = mock(ManagedAuthorizer.class); - when(managedAuthorizer.getAccessPolicyProvider()).thenReturn(accessPolicyProvider); - - jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtService, properties, managedAuthorizer, idpUserGroupService); - - // Arrange - LoginAuthenticationToken loginAuthenticationToken = - new LoginAuthenticationToken(ADMIN_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - String token = jwtService.generateSignedToken(loginAuthenticationToken); - final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS); - - // Act - final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); - final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal(); - - // Assert details username is correct - assertEquals(ADMIN_IDENTITY, details.getUsername()); - - final NiFiUser returnedUser = details.getNiFiUser(); - assertNotNull(returnedUser); - - // Assert user-group-provider groups are correct - assertEquals(1, returnedUser.getGroups().size()); - assertTrue(returnedUser.getGroups().contains(groupName3)); - - // Assert identity-provider groups are correct - assertEquals(2, returnedUser.getIdentityProviderGroups().size()); - assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName1)); - assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName2)); - - // Assert combined groups are correct - assertEquals(3, returnedUser.getAllGroups().size()); - assertTrue(returnedUser.getAllGroups().contains(groupName1)); - assertTrue(returnedUser.getAllGroups().contains(groupName2)); - assertTrue(returnedUser.getAllGroups().contains(groupName3)); - } - - private IdpUserGroup createIdpUserGroup(int id, String identity, String groupName, IdpType idpType) { - final IdpUserGroup userGroup = new IdpUserGroup(); - userGroup.setId(id); - userGroup.setIdentity(identity); - userGroup.setGroupName(groupName); - userGroup.setType(idpType); - return userGroup; - } - -} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java deleted file mode 100644 index a89082f053..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java +++ /dev/null @@ -1,682 +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.web.security.jwt; - -import io.jsonwebtoken.JwtException; -import org.apache.commons.codec.binary.Base64; -import org.apache.nifi.admin.service.AdministrationException; -import org.apache.nifi.admin.service.KeyService; -import org.apache.nifi.authorization.user.NiFiUserDetails; -import org.apache.nifi.authorization.user.StandardNiFiUser; -import org.apache.nifi.authorization.util.IdentityMapping; -import org.apache.nifi.authorization.util.IdentityMappingUtil; -import org.apache.nifi.key.Key; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.security.token.LoginAuthenticationToken; -import org.codehaus.jettison.json.JSONObject; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import static org.apache.nifi.util.NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX; -import static org.apache.nifi.util.NiFiProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class JwtServiceTest { - - private static final Logger logger = LoggerFactory.getLogger(JwtServiceTest.class); - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - /** - * These constant strings were generated using the tool at http://jwt.io - */ - private static final String VALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRl" - + "ciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZ" - + "XJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoxLCJleHAiOjI0NDc4MDg3NjEsIm" - + "lhdCI6MTQ0NzgwODcwMX0.r6aGZ6FNNYMOpcXW8BK2VYaQeX1uO0Aw1KJfjB3Q1DU"; - - // This token has an empty subject field - private static final String INVALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZG" - + "VudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvI" - + "iwia2lkIjoxLCJleHAiOjI0NDc4MDg3NjEsImlhdCI6MTQ0NzgwODcwMX0" - + ".x_1p2M6E0vwWHWMujIUnSL3GkFoDqqICllRxo2SMNaw"; - - private static final String VALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZC" - + "I6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJl" - + "c3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9"; - - // This token has an empty subject field - private static final String INVALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVu" - + "dGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoi" - + "YWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9"; - - // Algorithm field is "none" - private static final String VALID_MALSIGNED_TOKEN = "eyJhbGciOiJub25lIn0" - + ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZC" - + "I6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJl" - + "c3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9" - + ".mPO_wMNMl_zjMNevhNvUoXbSJ9Kx6jAe5OxDIAzKQbI"; - - // Algorithm field is "none" and no signature is present - private static final String VALID_MALSIGNED_NO_SIG_TOKEN = "eyJhbGciOiJub25lIn0" - + ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY" - + "2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIj" - + "oiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9."; - - // This token has an empty subject field - private static final String INVALID_MALSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVud" - + "Gl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYW" - + "xvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.WAwmUY4KHKV2oARNodkqDkbZsfRXGZfD2Ccy64GX9QF"; - - // This token is signed but expired - private static final String EXPIRED_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik" - + "1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvc" - + "HJlc3RvIiwia2lkIjoxLCJleHAiOjE0NDc4MDg3NjEsImlhdCI6MTQ0NzgwODcw" - + "MX0.ZPDIhNKuL89vTGXcuztOYaGifwcrQy_gid4j8Sspmto"; - - // Subject is "mgilman" but signed with "alopresto" key - private static final String IMPOSTER_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiJtZ2lsbWFuIiwiaXNzIjoiTW9ja0lkZW50aXR5UHJvdmlkZXIiLCJ" - + "hdWQiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsInByZWZlcnJlZF91c2VybmFtZSI" - + "6ImFsb3ByZXN0byIsImtpZCI6MSwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc" - + "4MDg3MDF9.aw5OAvLTnb_sHmSQOQzW-A7NImiZgXJ2ngbbNL2Ymkc"; - - // Issuer field is set to unknown provider - private static final String UNKNOWN_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJVbmtub3duSWRlbnRpdHlQcm92aWRlciIsIm" - + "F1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxv" - + "cHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9" - + ".SAd9tyNwSaijWet9wvAWSNmpxmPSK4XQuLx7h3ARqBo"; - - // Issuer field is absent - private static final String NO_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9" - + ".eyJzdWIiOiJhbG9wcmVzdG8iLCJhdWQiOiJNb2NrSWRlbnRpdHlQcm92a" - + "WRlciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFsb3ByZXN0byIsImtpZCI" - + "6MSwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.6kDjDanA" - + "g0NQDb3C8FmgbBAYDoIfMAEkF4WMVALsbJA"; - - private static final String KERBEROS_PROVIDER_TOKEN = "eyJhbGciOiJIUzI1NiJ9" + - ".eyJzdWIiOiJuaWZpYWRtaW5AbmlmaS5hcGFjaGUub3JnIiwiaXNzIjoiS2VyYmVyb" + - "3NQcm92aWRlciIsImF1ZCI6IktlcmJlcm9zUHJvdmlkZXIiLCJwcmVmZXJyZWRfdXN" + - "lcm5hbWUiOiJuaWZpYWRtaW5AbmlmaS5hcGFjaGUub3JnIiwia2lkIjo2LCJleHAiO" + - "jE2OTI0NTQ2NjcsImlhdCI6MTU5MjQxMTQ2N30.Mmnx6ssdjQ5_5VVRiyPWU60Oegc" + - "NdhWezaKKNK48Mew"; - - private static final String DEFAULT_HEADER = "{\"alg\":\"HS256\"}"; - private static final String DEFAULT_IDENTITY = "alopresto"; - private static final String REALMED_KERBEROS_IDENTITY = "nifiadmin@nifi.apache.org"; - private static final String KERBEROS_IDENTITY = "nifiadmin"; - - private static final String TOKEN_DELIMITER = "."; - - private static final String HMAC_SECRET = "test_hmac_shared_secret"; - - private static List identityMappings; - - private KeyService mockKeyService; - private KeyService testKeyService; - - // Class under test - private JwtService jwtService; - private JwtService jwtServiceUsingTestKeyService; - - public static String generateHS256Token(String rawHeader, String rawPayload, boolean isValid, boolean isSigned) { - return generateHS256Token(rawHeader, rawPayload, HMAC_SECRET, isValid, isSigned); - } - - private static String generateHS256Token(String rawHeader, String rawPayload, String hmacSecret, boolean isValid, - boolean isSigned) { - try { - logger.info("Generating token for " + rawHeader + " + " + rawPayload); - - String base64Header = Base64.encodeBase64URLSafeString(rawHeader.getBytes(StandardCharsets.UTF_8)); - String base64Payload = Base64.encodeBase64URLSafeString(rawPayload.getBytes(StandardCharsets.UTF_8)); - // TODO: Support valid/invalid manipulation - - final String body = base64Header + TOKEN_DELIMITER + base64Payload; - - String signature = generateHMAC(hmacSecret, body); - - return body + TOKEN_DELIMITER + signature; - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - final String errorMessage = "Could not generate the token"; - logger.error(errorMessage, e); - fail(errorMessage); - return null; - } - } - - private static String generateHMAC(String hmacSecret, String body) throws NoSuchAlgorithmException, - InvalidKeyException { - Mac hmacSHA256 = Mac.getInstance("HmacSHA256"); - SecretKeySpec secret_key = new SecretKeySpec(hmacSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); - hmacSHA256.init(secret_key); - return Base64.encodeBase64URLSafeString(hmacSHA256.doFinal(body.getBytes(StandardCharsets.UTF_8))); - } - - @Before - public void setUp() throws Exception { - final Key key = new Key(); - key.setId(1); - key.setIdentity(DEFAULT_IDENTITY); - key.setKey(HMAC_SECRET); - - Answer keyAnswer = new Answer() { - Key answerKey = key; - @Override - public Key answer(InvocationOnMock invocation) throws Throwable { - if(invocation.getMethod().equals(KeyService.class.getMethod("deleteKey", Integer.class))) { - answerKey = null; - } - return answerKey; - } - }; - - StandardNiFiUser nifiUser = mock(StandardNiFiUser.class); - when(nifiUser.getIdentity()).thenReturn(DEFAULT_IDENTITY); - NiFiUserDetails nifiUserDetails = mock(NiFiUserDetails.class); - when(nifiUserDetails.getNiFiUser()).thenReturn(nifiUser); - - Authentication authentication = mock(Authentication.class); - SecurityContext securityContext = mock(SecurityContext.class); - when(securityContext.getAuthentication()).thenReturn(authentication); - SecurityContextHolder.setContext(securityContext); - when(SecurityContextHolder.getContext().getAuthentication().getPrincipal()).thenReturn(nifiUserDetails); - - mockKeyService = mock(KeyService.class); - when(mockKeyService.getKey(anyInt())).thenAnswer(keyAnswer); - when(mockKeyService.getOrCreateKey(anyString())).thenReturn(key); - doAnswer(keyAnswer).when(mockKeyService).deleteKey(anyInt()); - - jwtService = new JwtService(mockKeyService); - jwtServiceUsingTestKeyService = new JwtService(new TestKeyService()); - - Properties props = new Properties(); - props.setProperty(SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX+"kerb", "^(.*?)@(.*?)$"); - props.setProperty(SECURITY_IDENTITY_MAPPING_VALUE_PREFIX+"kerb", "$1"); - identityMappings = IdentityMappingUtil.getIdentityMappings(new NiFiProperties(props)); - } - - @After - public void tearDown() throws Exception { - jwtService = null; - } - - @Test - public void testShouldGetAuthenticationForValidToken() throws Exception { - // Arrange - String token = VALID_SIGNED_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - assertEquals("Identity", DEFAULT_IDENTITY, identity); - } - - @Test - public void testShouldGetAuthenticationForValidKerberosToken() throws Exception { - // Arrange - String token = KERBEROS_PROVIDER_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - assertEquals("Identity", REALMED_KERBEROS_IDENTITY, identity); - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForInvalidToken() throws Exception { - // Arrange - String token = INVALID_SIGNED_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForEmptyToken() throws Exception { - // Arrange - String token = ""; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForUnsignedToken() throws Exception { - // Arrange - String token = VALID_UNSIGNED_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForMalsignedToken() throws Exception { - // Arrange - String token = VALID_MALSIGNED_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithm() throws Exception { - // Arrange - String token = VALID_MALSIGNED_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithmAndNoSignature() throws Exception { - // Arrange - String token = VALID_MALSIGNED_NO_SIG_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Ignore("Not yet implemented") - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForTokenFromUnknownIdentityProvider() throws Exception { - // Arrange - String token = UNKNOWN_ISSUER_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForTokenFromEmptyIdentityProvider() throws Exception { - // Arrange - String token = NO_ISSUER_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForExpiredToken() throws Exception { - // Arrange - String token = EXPIRED_SIGNED_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test(expected = JwtException.class) - public void testShouldNotGetAuthenticationForImposterToken() throws Exception { - // Arrange - String token = IMPOSTER_SIGNED_TOKEN; - - // Act - String identity = jwtService.getAuthenticationFromToken(token); - logger.info("Extracted identity: " + identity); - - // Assert - // Should fail - } - - @Test - public void testShouldGenerateSignedToken() throws Exception { - // Arrange - - // Token expires in 60 seconds - final int EXPIRATION_MILLIS = 60000; - LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto", - EXPIRATION_MILLIS, - "MockIdentityProvider"); - logger.info("Generating token for " + loginAuthenticationToken); - - final String EXPECTED_HEADER = DEFAULT_HEADER; - - // Convert the expiration time from ms to s - final long TOKEN_EXPIRATION_SEC = (long) (loginAuthenticationToken.getExpiration() / 1000.0); - - // Act - String token = jwtService.generateSignedToken(loginAuthenticationToken); - logger.info("Generated JWT: " + token); - - // Run after the SUT generates the token to ensure the same issued at time - // Split the token, decode the middle section, and form a new String - final String DECODED_PAYLOAD = new String(Base64.decodeBase64(token.split("\\.")[1].getBytes())); - final long ISSUED_AT_SEC = Long.valueOf(DECODED_PAYLOAD.substring(DECODED_PAYLOAD.lastIndexOf(":") + 1, - DECODED_PAYLOAD.length() - 1)); - logger.trace("Actual token was issued at " + ISSUED_AT_SEC); - - // Always use LinkedHashMap to enforce order of the signingKeys because the signature depends on order - Map claims = new LinkedHashMap<>(); - claims.put("sub", "alopresto"); - claims.put("iss", "MockIdentityProvider"); - claims.put("aud", "MockIdentityProvider"); - claims.put("preferred_username", "alopresto"); - claims.put("kid", 1); - claims.put("exp", TOKEN_EXPIRATION_SEC); - claims.put("iat", ISSUED_AT_SEC); - logger.trace("JSON Object to String: " + new JSONObject(claims).toString()); - - final String EXPECTED_PAYLOAD = new JSONObject(claims).toString(); - final String EXPECTED_TOKEN_STRING = generateHS256Token(EXPECTED_HEADER, EXPECTED_PAYLOAD, true, true); - logger.info("Expected JWT: " + EXPECTED_TOKEN_STRING); - - // Assert - assertEquals("JWT token", EXPECTED_TOKEN_STRING, token); - } - - @Test - public void testShouldGenerateSignedTokenWithURLEncodedIssuer() throws Exception { - // Arrange - - // Token expires in 60 seconds - final int EXPIRATION_MILLIS = 60000; - final String rawIssuer = "https://accounts.google.com/o/saml2?idpid=acode"; - final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto", EXPIRATION_MILLIS, rawIssuer); - logger.info("Generating token for " + loginAuthenticationToken); - - final String EXPECTED_HEADER = DEFAULT_HEADER; - - // Convert the expiration time from ms to s - final long TOKEN_EXPIRATION_SEC = (long) (loginAuthenticationToken.getExpiration() / 1000.0); - - // Act - String token = jwtService.generateSignedToken(loginAuthenticationToken); - logger.info("Generated JWT: " + token); - - // Run after the SUT generates the token to ensure the same issued at time - // Split the token, decode the middle section, and form a new String - final String DECODED_PAYLOAD = new String(Base64.decodeBase64(token.split("\\.")[1].getBytes())); - final long ISSUED_AT_SEC = Long.valueOf(DECODED_PAYLOAD.substring(DECODED_PAYLOAD.lastIndexOf(":") + 1, - DECODED_PAYLOAD.length() - 1)); - logger.trace("Actual token was issued at " + ISSUED_AT_SEC); - - // Always use LinkedHashMap to enforce order of the signingKeys because the signature depends on order - final String encodedIssuer = URLEncoder.encode(rawIssuer, "UTF-8"); - - final Map claims = new LinkedHashMap<>(); - claims.put("sub", "alopresto"); - claims.put("iss", encodedIssuer); - claims.put("aud", encodedIssuer); - claims.put("preferred_username", "alopresto"); - claims.put("kid", 1); - claims.put("exp", TOKEN_EXPIRATION_SEC); - claims.put("iat", ISSUED_AT_SEC); - logger.trace("JSON Object to String: " + new JSONObject(claims).toString()); - - final String EXPECTED_PAYLOAD = new JSONObject(claims).toString(); - final String EXPECTED_TOKEN_STRING = generateHS256Token(EXPECTED_HEADER, EXPECTED_PAYLOAD, true, true); - logger.info("Expected JWT: " + EXPECTED_TOKEN_STRING); - - // Assert - assertEquals("JWT token", EXPECTED_TOKEN_STRING, token); - } - - - @Test(expected = IllegalArgumentException.class) - public void testShouldNotGenerateTokenWithNullAuthenticationToken() throws Exception { - // Arrange - LoginAuthenticationToken nullLoginAuthenticationToken = null; - logger.info("Generating token for " + nullLoginAuthenticationToken); - - // Act - jwtService.generateSignedToken(nullLoginAuthenticationToken); - - // Assert - // Should throw exception - } - - @Test(expected = JwtException.class) - public void testShouldNotGenerateTokenWithEmptyIdentity() throws Exception { - // Arrange - final int EXPIRATION_MILLIS = 60000; - LoginAuthenticationToken emptyIdentityLoginAuthenticationToken = new LoginAuthenticationToken("", - EXPIRATION_MILLIS, "MockIdentityProvider"); - logger.info("Generating token for " + emptyIdentityLoginAuthenticationToken); - - // Act - jwtService.generateSignedToken(emptyIdentityLoginAuthenticationToken); - - // Assert - // Should throw exception - } - - @Test(expected = JwtException.class) - public void testShouldNotGenerateTokenWithNullIdentity() throws Exception { - // Arrange - final int EXPIRATION_MILLIS = 60000; - LoginAuthenticationToken nullIdentityLoginAuthenticationToken = new LoginAuthenticationToken(null, - EXPIRATION_MILLIS, "MockIdentityProvider"); - logger.info("Generating token for " + nullIdentityLoginAuthenticationToken); - - // Act - jwtService.generateSignedToken(nullIdentityLoginAuthenticationToken); - - // Assert - // Should throw exception - } - - @Test(expected = JwtException.class) - public void testShouldNotGenerateTokenWithMissingKey() throws Exception { - // Arrange - final int EXPIRATION_MILLIS = 60000; - LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - logger.info("Generating token for " + loginAuthenticationToken); - - // Set up the bad key service - KeyService missingKeyService = mock(KeyService.class); - when(missingKeyService.getOrCreateKey(anyString())).thenThrow(new AdministrationException("Could not find a " - + "key for that user")); - jwtService = new JwtService(missingKeyService); - - // Act - jwtService.generateSignedToken(loginAuthenticationToken); - - // Assert - // Should throw exception - } - - @Test - public void testShouldLogOutUser() throws Exception { - // Arrange - expectedException.expect(JwtException.class); - expectedException.expectMessage("Unable to validate the access token."); - - // Token expires in 60 seconds - final int EXPIRATION_MILLIS = 60000; - LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - logger.info("Generating token for " + loginAuthenticationToken); - - // Act - String token = jwtService.generateSignedToken(loginAuthenticationToken); - logger.info("Generated JWT: " + token); - logger.info("Validating token..."); - String authID = jwtService.getAuthenticationFromToken(token); - assertEquals(DEFAULT_IDENTITY, authID); - logger.info("Token was valid"); - logger.info("Logging out user: " + authID); - jwtService.logOut(token); - logger.info("Logged out user: " + authID); - logger.info("Checking that token is now invalid..."); - jwtService.getAuthenticationFromToken(token); - - // Assert - // Should throw exception when user is not found - } - - @Test - public void testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws Exception { - // Arrange - expectedException.expect(JwtException.class); - expectedException.expectMessage("Unable to validate the access token."); - - // Act - jwtService.logOut(null); - - // Assert - // Should throw exception when authorization header is null - } - - @Test - public void testShouldLogOutKerberosUser() throws Exception { - // Arrange - - expectedException.expect(JwtException.class); - expectedException.expectMessage("Unable to validate the access token."); - - // Token expires in 60 seconds - final int EXPIRATION_MILLIS = 60000; - LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(KERBEROS_IDENTITY, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - logger.info("Generating token for " + loginAuthenticationToken); - - // Act - String token = jwtServiceUsingTestKeyService.generateSignedToken(loginAuthenticationToken); - logger.info("Generated JWT: " + token); - logger.info("Validating token..."); - String authID = jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); - logger.info("Token was valid, unmapped user identity was: " + authID); - assertEquals(KERBEROS_IDENTITY, authID); - logger.info("Using identity mappings " + Arrays.toString(identityMappings.toArray()) + " to map identity: " + authID); - String mappedIdentity = IdentityMappingUtil.mapIdentity(authID, identityMappings); - logger.info("Logging out user with mapped identity: " + mappedIdentity); - jwtServiceUsingTestKeyService.logOut(mappedIdentity); - logger.info("Logged out user with mapped identity: " + mappedIdentity); - logger.info("Checking that token for " + mappedIdentity + " is now invalid..."); - jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); - - // Assert - // Should throw exception when user is not found - } - - @Test - public void testShouldLogOutRealmedKerberosUser() throws Exception { - // Arrange - - expectedException.expect(JwtException.class); - expectedException.expectMessage("Unable to validate the access token."); - - // Token expires in 60 seconds - final int EXPIRATION_MILLIS = 60000; - // map the kerberos identity before we create our token, just as is done in AccessResource - final String mappedIdentity = IdentityMappingUtil.mapIdentity(REALMED_KERBEROS_IDENTITY, identityMappings); - - LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, - EXPIRATION_MILLIS, - "MockIdentityProvider"); - logger.info("Generating token for " + loginAuthenticationToken); - - // Act - String token = jwtServiceUsingTestKeyService.generateSignedToken(loginAuthenticationToken); - logger.info("Generated JWT: " + token); - logger.info("Validating token..."); - String authID = jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); - logger.info("Token was valid, unmapped user identity was: " + authID); - assertEquals(KERBEROS_IDENTITY, authID); - logger.info("Using identity mappings " + Arrays.toString(identityMappings.toArray()) + " to map identity: " + authID); - logger.info("Logging out user with mapped identity: " + authID); - jwtServiceUsingTestKeyService.logOut(authID); - logger.info("Logged out user with mapped identity: " + authID); - logger.info("Checking that token for " + authID + " is now invalid..."); - jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); - - // Assert - // Should throw exception when user is not found - } - -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java deleted file mode 100644 index e14c7ed5a8..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java +++ /dev/null @@ -1,124 +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.web.security.jwt; - -import groovy.json.JsonOutput; -import org.junit.BeforeClass; -import org.junit.Test; -import org.mockito.Mock; - -import javax.servlet.http.HttpServletRequest; -import java.util.Calendar; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class NiFiBearerTokenResolverTest { - - public static String jwtString; - - @Mock - private static HttpServletRequest request; - - @BeforeClass - public static void setUpOnce() throws Exception { - final String ALG_HEADER = "{\"alg\":\"HS256\"}"; - final int EXPIRATION_SECONDS = 500; - Calendar now = Calendar.getInstance(); - final long currentTime = (long) (now.getTimeInMillis() / 1000.0); - final long TOKEN_ISSUED_AT = currentTime; - final long TOKEN_EXPIRATION_SECONDS = currentTime + EXPIRATION_SECONDS; - - Map hashMap = new HashMap() {{ - put("sub", "unknownuser"); - put("iss", "MockIdentityProvider"); - put("aud", "MockIdentityProvider"); - put("preferred_username", "unknownuser"); - put("kid", String.valueOf(1)); - put("exp", String.valueOf(TOKEN_EXPIRATION_SECONDS)); - put("iat", String.valueOf(TOKEN_ISSUED_AT)); - }}; - - // Generate a token that we will add a valid signature from a different token - // Always use LinkedHashMap to enforce order of the keys because the signature depends on order - final String EXPECTED_PAYLOAD = JsonOutput.toJson(hashMap); - - // Set up our JWT string with a test token - jwtString = JwtServiceTest.generateHS256Token(ALG_HEADER, EXPECTED_PAYLOAD, true, true); - - request = mock(HttpServletRequest.class); - } - - @Test - public void testValidAuthenticationHeaderString() { - String authenticationHeader = "Bearer " + jwtString; - when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader); - String isValidHeader = new NiFiBearerTokenResolver().resolve(request); - - assertEquals(jwtString, isValidHeader); - } - - @Test - public void testMissingBearer() { - String authenticationHeader = jwtString; - when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader); - String resolvedToken = new NiFiBearerTokenResolver().resolve(request); - - assertNull(resolvedToken); - } - - @Test - public void testExtraCharactersAtBeginningOfToken() { - String authenticationHeader = "xBearer " + jwtString; - when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader); - String resolvedToken = new NiFiBearerTokenResolver().resolve(request); - - assertNull(resolvedToken); - } - - @Test - public void testBadTokenFormat() { - String[] tokenStrings = jwtString.split("\\."); - when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.valueOf("Bearer " + tokenStrings[1] + tokenStrings[2])); - String resolvedToken = new NiFiBearerTokenResolver().resolve(request); - - assertNull(resolvedToken); - } - - @Test - public void testMultipleTokenInvalid() { - String authenticationHeader = "Bearer " + jwtString; - when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.format("%s %s", authenticationHeader, authenticationHeader)); - String resolvedToken = new NiFiBearerTokenResolver().resolve(request); - - assertNull(resolvedToken); - } - - @Test - public void testExtractToken() { - String authenticationHeader = "Bearer " + jwtString; - when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader); - String extractedToken = new NiFiBearerTokenResolver().resolve(request); - - assertEquals(jwtString, extractedToken); - } -} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java deleted file mode 100644 index 495279ede1..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java +++ /dev/null @@ -1,71 +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.web.security.jwt; - -import java.util.ArrayList; -import java.util.UUID; -import org.apache.nifi.admin.service.KeyService; -import org.apache.nifi.key.Key; - -public class TestKeyService implements KeyService { - - ArrayList signingKeys = new ArrayList(); - - public TestKeyService() { - - } - - @Override - public Key getKey(int id) { - Key key = null; - for(Key k : signingKeys) { - if(k.getId() == id) { - key = k; - } - } - return key; - } - - @Override - public Key getOrCreateKey(String identity) { - for(Key key : signingKeys) { - if(key.getIdentity().equals(identity)) { - return key; - } - } - - Key key = generateKey(identity); - signingKeys.add(key); - return key; - } - - @Override - public void deleteKey(Integer keyId) { - Key keyToRemove = null; - for(Key k : signingKeys) { - if(k.getId() == keyId) { - keyToRemove = k; - } - } - signingKeys.remove(keyToRemove); - } - - private Key generateKey(String identity) { - Integer keyId = signingKeys.size(); - return new Key(keyId, identity, UUID.randomUUID().toString()); - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverterTest.java new file mode 100644 index 0000000000..099feffd12 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/converter/StandardJwtAuthenticationConverterTest.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.converter; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; +import org.apache.nifi.admin.service.IdpUserGroupService; +import org.apache.nifi.authorization.AccessPolicyProvider; +import org.apache.nifi.authorization.Group; +import org.apache.nifi.authorization.ManagedAuthorizer; +import org.apache.nifi.authorization.UserAndGroups; +import org.apache.nifi.authorization.UserGroupProvider; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserDetails; +import org.apache.nifi.idp.IdpUserGroup; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.util.StringUtils; +import org.apache.nifi.web.security.token.NiFiAuthenticationToken; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StandardJwtAuthenticationConverterTest { + private static final String USERNAME = "NiFi"; + + private static final String AUTHORIZER_GROUP = "AuthorizerGroup"; + + private static final String PROVIDER_GROUP = "ProviderGroup"; + + private static final String TYPE_FIELD = "typ"; + + private static final String JWT_TYPE = "JWT"; + + @Mock + private ManagedAuthorizer authorizer; + + @Mock + private AccessPolicyProvider accessPolicyProvider; + + @Mock + private UserGroupProvider userGroupProvider; + + @Mock + private UserAndGroups userAndGroups; + + @Mock + private IdpUserGroupService idpUserGroupService; + + private StandardJwtAuthenticationConverter converter; + + @Before + public void setConverter() { + final Map properties = new HashMap<>(); + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(StringUtils.EMPTY, properties); + converter = new StandardJwtAuthenticationConverter(authorizer, idpUserGroupService, niFiProperties); + + when(authorizer.getAccessPolicyProvider()).thenReturn(accessPolicyProvider); + when(accessPolicyProvider.getUserGroupProvider()).thenReturn(userGroupProvider); + when(userGroupProvider.getUserAndGroups(eq(USERNAME))).thenReturn(userAndGroups); + + final Group group = new Group.Builder().name(AUTHORIZER_GROUP).identifier(AUTHORIZER_GROUP).build(); + when(userAndGroups.getGroups()).thenReturn(Collections.singleton(group)); + + final IdpUserGroup idpUserGroup = new IdpUserGroup(); + idpUserGroup.setGroupName(PROVIDER_GROUP); + when(idpUserGroupService.getUserGroups(eq(USERNAME))).thenReturn(Collections.singletonList(idpUserGroup)); + } + + @Test + public void testConvert() { + final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .subject(USERNAME) + .build(); + final String token = new PlainJWT(claimsSet).serialize(); + final Jwt jwt = Jwt.withTokenValue(token) + .header(TYPE_FIELD, JWT_TYPE) + .subject(USERNAME) + .build(); + + final NiFiAuthenticationToken authenticationToken = converter.convert(jwt); + assertNotNull(authenticationToken); + assertEquals(USERNAME, authenticationToken.toString()); + + final NiFiUserDetails details = (NiFiUserDetails) authenticationToken.getDetails(); + final NiFiUser user = details.getNiFiUser(); + + final Set expectedGroups = Collections.singleton(AUTHORIZER_GROUP); + assertEquals(expectedGroups, user.getGroups()); + + final Set expectedProviderGroups = Collections.singleton(PROVIDER_GROUP); + assertEquals(expectedProviderGroups, user.getIdentityProviderGroups()); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProviderTest.java new file mode 100644 index 0000000000..509ea1be3b --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/jws/StandardJwsSignerProviderTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.jws; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.time.Instant; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StandardJwsSignerProviderTest { + private static final String KEY_IDENTIFIER = UUID.randomUUID().toString(); + + @Mock + private SigningKeyListener signingKeyListener; + + @Mock + private JwsSignerContainer jwsSignerContainer; + + @Captor + private ArgumentCaptor keyIdentifierCaptor; + + @Captor + private ArgumentCaptor expirationCaptor; + + private StandardJwsSignerProvider provider; + + @Before + public void setProvider() { + provider = new StandardJwsSignerProvider(signingKeyListener); + when(jwsSignerContainer.getKeyIdentifier()).thenReturn(KEY_IDENTIFIER); + } + + @Test + public void testOnSignerUpdated() { + provider.onSignerUpdated(jwsSignerContainer); + final Instant expiration = Instant.now(); + final JwsSignerContainer container = provider.getJwsSignerContainer(expiration); + + assertEquals("JWS Signer Container not matched", jwsSignerContainer, container); + + verify(signingKeyListener).onSigningKeyUsed(keyIdentifierCaptor.capture(), expirationCaptor.capture()); + assertEquals(KEY_IDENTIFIER, keyIdentifierCaptor.getValue()); + assertEquals(expiration, expirationCaptor.getValue()); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java new file mode 100644 index 0000000000..96da3d3e16 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/command/KeyGenerationCommandTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key.command; + +import com.nimbusds.jose.JWSAlgorithm; +import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer; +import org.apache.nifi.web.security.jwt.jws.SignerListener; +import org.apache.nifi.web.security.jwt.key.VerificationKeyListener; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.security.Key; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class KeyGenerationCommandTest { + private static final String KEY_ALGORITHM = "RSA"; + + private static final JWSAlgorithm JWS_ALGORITHM = JWSAlgorithm.PS512; + + @Mock + private SignerListener signerListener; + + @Mock + private VerificationKeyListener verificationKeyListener; + + @Captor + private ArgumentCaptor signerCaptor; + + @Captor + private ArgumentCaptor keyIdentifierCaptor; + + @Captor + private ArgumentCaptor keyCaptor; + + private KeyGenerationCommand command; + + @Before + public void setCommand() { + command = new KeyGenerationCommand(signerListener, verificationKeyListener); + } + + @Test + public void testRun() { + command.run(); + + verify(signerListener).onSignerUpdated(signerCaptor.capture()); + final JwsSignerContainer signerContainer = signerCaptor.getValue(); + assertEquals(JWS_ALGORITHM, signerContainer.getJwsAlgorithm()); + + verify(verificationKeyListener).onVerificationKeyGenerated(keyIdentifierCaptor.capture(), keyCaptor.capture()); + final Key key = keyCaptor.getValue(); + assertEquals(KEY_ALGORITHM, key.getAlgorithm()); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyServiceTest.java new file mode 100644 index 0000000000..d9a0f212c9 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/key/service/StandardVerificationKeyServiceTest.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.key.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.apache.nifi.components.state.Scope; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.components.state.StateMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StandardVerificationKeyServiceTest { + private static final String ID = UUID.randomUUID().toString(); + + private static final String ALGORITHM = "RSA"; + + private static final byte[] ENCODED = ALGORITHM.getBytes(StandardCharsets.UTF_8); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); + + private static final Scope SCOPE = Scope.LOCAL; + + private static final Instant EXPIRED = Instant.now().minusSeconds(60); + + @Mock + private StateManager stateManager; + + @Mock + private StateMap stateMap; + + @Mock + private Key key; + + @Captor + private ArgumentCaptor> stateCaptor; + + private StandardVerificationKeyService service; + + @Before + public void setService() { + service = new StandardVerificationKeyService(stateManager); + when(key.getAlgorithm()).thenReturn(ALGORITHM); + when(key.getEncoded()).thenReturn(ENCODED); + } + + @Test + public void testDeleteExpired() throws IOException { + when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap); + + final String serialized = getSerializedVerificationKey(EXPIRED); + when(stateMap.toMap()).thenReturn(Collections.singletonMap(ID, serialized)); + + service.deleteExpired(); + + verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE)); + final Map stateSaved = stateCaptor.getValue(); + assertTrue("Expired Key not deleted", stateSaved.isEmpty()); + } + + @Test + public void testSave() throws IOException { + when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap); + when(stateMap.toMap()).thenReturn(Collections.emptyMap()); + + final Instant expiration = Instant.now(); + service.save(ID, key, expiration); + + verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE)); + final Map stateSaved = stateCaptor.getValue(); + final String serialized = stateSaved.get(ID); + assertNotNull("Serialized Key not found", serialized); + } + + @Test + public void testSetExpiration() throws IOException { + when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap); + when(stateMap.toMap()).thenReturn(Collections.emptyMap()); + + final Instant expiration = Instant.now(); + final String serialized = getSerializedVerificationKey(expiration); + when(stateMap.get(eq(ID))).thenReturn(serialized); + + service.setExpiration(ID, expiration); + + verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE)); + final Map stateSaved = stateCaptor.getValue(); + final String saved = stateSaved.get(ID); + assertNotNull("Serialized Key not found", saved); + } + + private String getSerializedVerificationKey(final Instant expiration) throws JsonProcessingException { + final VerificationKey verificationKey = new VerificationKey(); + verificationKey.setId(ID); + verificationKey.setExpiration(expiration); + return OBJECT_MAPPER.writeValueAsString(verificationKey); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java new file mode 100644 index 0000000000..668d04c8a7 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/provider/StandardBearerTokenProviderTest.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.provider; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer; +import org.apache.nifi.web.security.jwt.jws.JwsSignerProvider; +import org.apache.nifi.web.security.token.LoginAuthenticationToken; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; +import java.time.Instant; +import java.util.Collections; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StandardBearerTokenProviderTest { + private static final String USERNAME = "USERNAME"; + + private static final String IDENTITY = "IDENTITY"; + + private static final long EXPIRATION = 60; + + private static final String ISSUER = "ISSUER"; + + private static final String KEY_ALGORITHM = "RSA"; + + private static final int KEY_SIZE = 4096; + + private static final JWSAlgorithm JWS_ALGORITHM = JWSAlgorithm.PS512; + + @Mock + private JwsSignerProvider jwsSignerProvider; + + private StandardBearerTokenProvider provider; + + private JWSVerifier jwsVerifier; + + private JWSSigner jwsSigner; + + @Before + public void setProvider() throws NoSuchAlgorithmException { + provider = new StandardBearerTokenProvider(jwsSignerProvider); + + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); + keyPairGenerator.initialize(KEY_SIZE); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyPair.getPublic()); + jwsSigner = new RSASSASigner(keyPair.getPrivate()); + } + + @Test + public void testGetBearerToken() throws ParseException, JOSEException { + final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(IDENTITY, USERNAME, EXPIRATION, ISSUER); + final String keyIdentifier = UUID.randomUUID().toString(); + final JwsSignerContainer jwsSignerContainer = new JwsSignerContainer(keyIdentifier, JWS_ALGORITHM, jwsSigner); + when(jwsSignerProvider.getJwsSignerContainer(isA(Instant.class))).thenReturn(jwsSignerContainer); + + final String bearerToken = provider.getBearerToken(loginAuthenticationToken); + + final SignedJWT signedJwt = SignedJWT.parse(bearerToken); + assertTrue("Verification Failed", signedJwt.verify(jwsVerifier)); + + final JWTClaimsSet claims = signedJwt.getJWTClaimsSet(); + assertNotNull("Issue Time not found", claims.getIssueTime()); + assertNotNull("Not Before Time Time not found", claims.getNotBeforeTime()); + assertNotNull("Expiration Time Time not found", claims.getExpirationTime()); + assertEquals(ISSUER, claims.getIssuer()); + assertEquals(Collections.singletonList(ISSUER), claims.getAudience()); + assertEquals(IDENTITY, claims.getSubject()); + assertEquals(USERNAME, claims.getClaim(SupportedClaim.PREFERRED_USERNAME.getClaim())); + assertNotNull("JSON Web Token Identifier not found", claims.getJWTID()); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolverTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolverTest.java new file mode 100644 index 0000000000..e9e407346a --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/resolver/StandardBearerTokenResolverTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.resolver; + +import org.apache.nifi.web.security.http.SecurityCookieName; +import org.apache.nifi.web.security.http.SecurityHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StandardBearerTokenResolverTest { + private static final String BEARER_TOKEN = "TOKEN"; + + private StandardBearerTokenResolver resolver; + + @Mock + private HttpServletRequest request; + + @Before + public void setResolver() { + resolver = new StandardBearerTokenResolver(); + } + + @Test + public void testResolveAuthorizationHeaderFound() { + setHeader(String.format("Bearer %s", BEARER_TOKEN)); + assertEquals(BEARER_TOKEN, resolver.resolve(request)); + } + + @Test + public void testResolveAuthorizationHeaderMissingPrefix() { + setHeader(BEARER_TOKEN); + assertNull(resolver.resolve(request)); + } + + @Test + public void testResolveAuthorizationHeaderIncorrectPrefix() { + setHeader(String.format("Basic %s", BEARER_TOKEN)); + assertNull(resolver.resolve(request)); + } + + @Test + public void testResolveCookieFound() { + final Cookie cookie = new Cookie(SecurityCookieName.AUTHORIZATION_BEARER.getName(), BEARER_TOKEN); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + assertEquals(BEARER_TOKEN, resolver.resolve(request)); + } + + private void setHeader(final String header) { + when(request.getHeader(eq(SecurityHeader.AUTHORIZATION.getHeader()))).thenReturn(header); + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidatorTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidatorTest.java new file mode 100644 index 0000000000..2528d45aa1 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/JwtRevocationValidatorTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.revocation; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.UUID; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class JwtRevocationValidatorTest { + private static final String ID = UUID.randomUUID().toString(); + + private static final String TOKEN = "TOKEN"; + + private static final String TYPE_FIELD = "typ"; + + private static final String JWT_TYPE = "JWT"; + + @Mock + private JwtRevocationService jwtRevocationService; + + private Jwt jwt; + + private JwtRevocationValidator validator; + + @Before + public void setValidator() { + validator = new JwtRevocationValidator(jwtRevocationService); + jwt = Jwt.withTokenValue(TOKEN).header(TYPE_FIELD, JWT_TYPE).jti(ID).build(); + } + + @Test + public void testValidateSuccess() { + when(jwtRevocationService.isRevoked(eq(ID))).thenReturn(false); + final OAuth2TokenValidatorResult result = validator.validate(jwt); + assertFalse(result.hasErrors()); + } + + @Test + public void testValidateFailure() { + when(jwtRevocationService.isRevoked(eq(ID))).thenReturn(true); + final OAuth2TokenValidatorResult result = validator.validate(jwt); + assertTrue(result.hasErrors()); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListenerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListenerTest.java new file mode 100644 index 0000000000..f3cf555dfe --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtLogoutListenerTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.revocation; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; + +import java.time.Instant; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StandardJwtLogoutListenerTest { + private static final String ID = UUID.randomUUID().toString(); + + private static final Instant EXPIRES = Instant.now(); + + private static final String TOKEN = "TOKEN"; + + private static final String TYPE_FIELD = "typ"; + + private static final String JWT_TYPE = "JWT"; + + @Mock + private JwtRevocationService jwtRevocationService; + + @Mock + private JwtDecoder jwtDecoder; + + private Jwt jwt; + + private StandardJwtLogoutListener listener; + + @Before + public void setListener() { + listener = new StandardJwtLogoutListener(jwtDecoder, jwtRevocationService); + jwt = Jwt.withTokenValue(TOKEN).header(TYPE_FIELD, JWT_TYPE).jti(ID).expiresAt(EXPIRES).build(); + } + + @Test + public void testLogoutBearerTokenNullZeroInteractions() { + listener.logout(null); + verifyZeroInteractions(jwtDecoder); + verifyZeroInteractions(jwtRevocationService); + } + + @Test + public void testLogoutBearerToken() { + when(jwtDecoder.decode(eq(TOKEN))).thenReturn(jwt); + + listener.logout(TOKEN); + + verify(jwtRevocationService).setRevoked(eq(ID), eq(EXPIRES)); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationServiceTest.java new file mode 100644 index 0000000000..b2c991f841 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/revocation/StandardJwtRevocationServiceTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt.revocation; + +import org.apache.nifi.components.state.Scope; +import org.apache.nifi.components.state.StateManager; +import org.apache.nifi.components.state.StateMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StandardJwtRevocationServiceTest { + private static final String ID = UUID.randomUUID().toString(); + + private static final Scope SCOPE = Scope.LOCAL; + + private static final Instant EXPIRED = Instant.now().minusSeconds(60); + + @Mock + private StateManager stateManager; + + @Mock + private StateMap stateMap; + + @Captor + private ArgumentCaptor> stateCaptor; + + private StandardJwtRevocationService service; + + @Before + public void setService() { + service = new StandardJwtRevocationService(stateManager); + } + + @Test + public void testDeleteExpired() throws IOException { + when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap); + when(stateMap.toMap()).thenReturn(Collections.singletonMap(ID, EXPIRED.toString())); + + service.deleteExpired(); + + verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE)); + final Map stateSaved = stateCaptor.getValue(); + assertTrue("Expired Key not deleted", stateSaved.isEmpty()); + } + + @Test + public void testIsRevokedFound() throws IOException { + when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap); + when(stateMap.toMap()).thenReturn(Collections.singletonMap(ID, EXPIRED.toString())); + + assertTrue(service.isRevoked(ID)); + } + + @Test + public void testIsRevokedNotFound() throws IOException { + when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap); + when(stateMap.toMap()).thenReturn(Collections.emptyMap()); + + assertFalse(service.isRevoked(ID)); + } + + @Test + public void testSetRevoked() throws IOException { + when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap); + when(stateMap.toMap()).thenReturn(Collections.emptyMap()); + + final Instant expiration = Instant.now(); + service.setRevoked(ID, expiration); + + verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE)); + final Map stateSaved = stateCaptor.getValue(); + final String saved = stateSaved.get(ID); + assertEquals("Expiration not matched", expiration.toString(), saved); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLStateManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLStateManager.java index 68fc14645e..ce1fdfe1ba 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLStateManager.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLStateManager.java @@ -16,7 +16,7 @@ */ package org.apache.nifi.web.security.saml.impl; -import org.apache.nifi.web.security.jwt.JwtService; +import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; import org.apache.nifi.web.security.saml.SAMLStateManager; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.junit.Before; @@ -32,13 +32,13 @@ import static org.mockito.Mockito.when; public class TestStandardSAMLStateManager { - private JwtService jwtService; + private BearerTokenProvider bearerTokenProvider; private SAMLStateManager stateManager; @Before public void setup() { - jwtService = mock(JwtService.class); - stateManager = new StandardSAMLStateManager(jwtService); + bearerTokenProvider = mock(BearerTokenProvider.class); + stateManager = new StandardSAMLStateManager(bearerTokenProvider); } @Test @@ -76,7 +76,7 @@ public class TestStandardSAMLStateManager { // create the jwt and cache it final String fakeJwt = "fake-jwt"; - when(jwtService.generateSignedToken(token)).thenReturn(fakeJwt); + when(bearerTokenProvider.getBearerToken(token)).thenReturn(fakeJwt); stateManager.createJwt(requestId, token); // should return the jwt above diff --git a/nifi-nar-bundles/nifi-framework-bundle/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/pom.xml index e5feefaf8d..de89aab0a4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/pom.xml @@ -339,34 +339,22 @@ com.nimbusds oauth2-oidc-sdk - 6.16.2 + 9.10.2 net.minidev json-smart - 2.3.1 + 2.4.7 com.nimbusds lang-tag - 1.4.4 + 1.5 com.nimbusds nimbus-jose-jwt - 7.9 - - - - io.jsonwebtoken - jjwt - 0.6.0 - - - com.fasterxml.jackson.core - jackson-databind - - + 9.11.2 org.codehaus.jettison @@ -583,6 +571,21 @@ spring-context ${spring.version} + + org.springframework.security + spring-security-oauth2-resource-server + ${spring.security.version} + + + org.springframework.security + spring-security-oauth2-core + ${spring.security.version} + + + org.springframework.security + spring-security-oauth2-jose + ${spring.security.version} + org.apache.commons commons-collections4