druid extension for OpenID Connect auth using pac4j lib (#8992)

* druid pac4j security extension for OpenID Connect OAuth 2.0 authentication

* update version in druid-pac4j pom

* introducing unauthorized resource filter

* authenticated but authorized /unified-webconsole.html

* use httpReq.getRequestURI() for matching callback path

* add documentation

* minor doc addition

* licesne file updates

* make dependency analyze succeed

* fix doc build

* hopefully fixes doc build

* hopefully fixes license check build

* yet another try on fixing license build

* revert unintentional changes to website folder

* update version to 0.18.0-SNAPSHOT

* check session and its expiry on each request

* add crypto service

* code for encrypting the cookie

* update doc with cookiePassphrase

* update license yaml

* make sessionstore in Pac4jFilter private non static

* make Pac4jFilter fields final

* okta: use sha256 for hmac

* remove incubating

* add UTs for crypto util and session store impl

* use standard charsets

* add license header

* remove unused file

* add org.objenesis.objenesis to license.yaml

* a bit of nit changes  in CryptoService  and embedding EncryptionResult for clarity

* rename alg  to cipherAlgName

* take cipher alg name, mode and padding as input

* add java doc  for CryptoService  and make it more understandable

* another  UT for CryptoService

* cache pac4j Config

* use generics clearly in Pac4jSessionStore

* update cookiePassphrase doc to mention PasswordProvider

* mark stuff Nullable where appropriate in Pac4jSessionStore

* update doc to mention jdbc

* add error log on reaching callback resource

* javadoc  for Pac4jCallbackResource

* introduce NOOP_HTTP_ACTION_ADAPTER

* add correct module name in license file

* correct extensions folder name in licenses.yaml

* replace druid-kubernetes-extensions to druid-pac4j

* cache SecureRandom instance

* rename UnauthorizedResourceFilter to AuthenticationOnlyResourceFilter
This commit is contained in:
Himanshu 2020-03-23 18:15:45 -07:00 committed by GitHub
parent cdf4a26904
commit 5604ac7963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1368 additions and 1 deletions

View File

@ -0,0 +1,217 @@
/*
* 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.druid.crypto;
import com.google.common.base.Preconditions;
import org.apache.druid.java.util.common.StringUtils;
import javax.annotation.Nullable;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
/**
* Utility class for symmetric key encryption (i.e. same secret is used for encryption and decryption) of byte[]
* using javax.crypto package.
*
* To learn about possible algorithms supported and their names,
* See https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
*/
public class CryptoService
{
// Based on Javadocs on SecureRandom, It is threadsafe as well.
private static final SecureRandom SECURE_RANDOM_INSTANCE = new SecureRandom();
// User provided secret phrase used for encrypting data
private final char[] passPhrase;
// Variables for algorithm used to generate a SecretKey based on user provided passPhrase
private final String secretKeyFactoryAlg;
private final int saltSize;
private final int iterationCount;
private final int keyLength;
// Cipher algorithm information
private final String cipherAlgName;
private final String cipherAlgMode;
private final String cipherAlgPadding;
// transformation = "cipherAlgName/cipherAlgMode/cipherAlgPadding" used in Cipher.getInstance(transformation)
private final String transformation;
public CryptoService(
String passPhrase,
@Nullable String cipherAlgName,
@Nullable String cipherAlgMode,
@Nullable String cipherAlgPadding,
@Nullable String secretKeyFactoryAlg,
@Nullable Integer saltSize,
@Nullable Integer iterationCount,
@Nullable Integer keyLength
)
{
Preconditions.checkArgument(
passPhrase != null && !passPhrase.isEmpty(),
"null/empty passPhrase"
);
this.passPhrase = passPhrase.toCharArray();
this.cipherAlgName = cipherAlgName == null ? "AES" : cipherAlgName;
this.cipherAlgMode = cipherAlgMode == null ? "CBC" : cipherAlgMode;
this.cipherAlgPadding = cipherAlgPadding == null ? "PKCS5Padding" : cipherAlgPadding;
this.transformation = StringUtils.format("%s/%s/%s", this.cipherAlgName, this.cipherAlgMode, this.cipherAlgPadding);
this.secretKeyFactoryAlg = secretKeyFactoryAlg == null ? "PBKDF2WithHmacSHA256" : secretKeyFactoryAlg;
this.saltSize = saltSize == null ? 8 : saltSize;
this.iterationCount = iterationCount == null ? 65536 : iterationCount;
this.keyLength = keyLength == null ? 128 : keyLength;
// encrypt/decrypt a test string to ensure all params are valid
String testString = "duh! !! !!!";
Preconditions.checkState(
testString.equals(StringUtils.fromUtf8(decrypt(encrypt(StringUtils.toUtf8(testString))))),
"decrypt(encrypt(testString)) failed"
);
}
public byte[] encrypt(byte[] plain)
{
try {
byte[] salt = new byte[saltSize];
SECURE_RANDOM_INSTANCE.nextBytes(salt);
SecretKey tmp = getKeyFromPassword(passPhrase, salt);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), cipherAlgName);
Cipher ecipher = Cipher.getInstance(transformation);
ecipher.init(Cipher.ENCRYPT_MODE, secret);
return new EncryptedData(
salt,
ecipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV(),
ecipher.doFinal(plain)
).toByteAray();
}
catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidParameterSpecException | IllegalBlockSizeException | BadPaddingException ex) {
throw new RuntimeException(ex);
}
}
public byte[] decrypt(byte[] data)
{
try {
EncryptedData encryptedData = EncryptedData.fromByteArray(data);
SecretKey tmp = getKeyFromPassword(passPhrase, encryptedData.getSalt());
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), cipherAlgName);
Cipher dcipher = Cipher.getInstance(transformation);
dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(encryptedData.getIv()));
return dcipher.doFinal(encryptedData.getCipher());
}
catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) {
throw new RuntimeException(ex);
}
}
private SecretKey getKeyFromPassword(char[] passPhrase, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException
{
SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKeyFactoryAlg);
KeySpec spec = new PBEKeySpec(passPhrase, salt, iterationCount, keyLength);
return factory.generateSecret(spec);
}
private static class EncryptedData
{
private final byte[] salt;
private final byte[] iv;
private final byte[] cipher;
public EncryptedData(byte[] salt, byte[] iv, byte[] cipher)
{
this.salt = salt;
this.iv = iv;
this.cipher = cipher;
}
public byte[] getSalt()
{
return salt;
}
public byte[] getIv()
{
return iv;
}
public byte[] getCipher()
{
return cipher;
}
public byte[] toByteAray()
{
int headerLength = 12;
ByteBuffer bb = ByteBuffer.allocate(salt.length + iv.length + cipher.length + headerLength);
bb.putInt(salt.length)
.putInt(iv.length)
.putInt(cipher.length)
.put(salt)
.put(iv)
.put(cipher);
bb.flip();
return bb.array();
}
public static EncryptedData fromByteArray(byte[] array)
{
ByteBuffer bb = ByteBuffer.wrap(array);
int saltSize = bb.getInt();
int ivSize = bb.getInt();
int cipherSize = bb.getInt();
byte[] salt = new byte[saltSize];
bb.get(salt);
byte[] iv = new byte[ivSize];
bb.get(iv);
byte[] cipher = new byte[cipherSize];
bb.get(cipher);
return new EncryptedData(salt, iv, cipher);
}
}
}

View File

@ -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.druid.crypto;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class CryptoServiceTest
{
@Test
public void testEncryptDecrypt()
{
CryptoService cryptoService = new CryptoService(
"random-passphrase",
"AES",
"CBC",
"PKCS5Padding",
"PBKDF2WithHmacSHA256",
8,
65536,
128
);
byte[] original = "i am a test string".getBytes(StandardCharsets.UTF_8);
byte[] decrypted = cryptoService.decrypt(cryptoService.encrypt(original));
Assert.assertArrayEquals(original, decrypted);
}
@Test
public void testInvalidParamsConstructorFailure()
{
try {
new CryptoService(
"random-passphrase",
"ABCD",
"EFGH",
"PAXXDDING",
"QWERTY",
8,
65536,
128
);
Assert.fail("Must Fail!!!");
}
catch (RuntimeException ex) {
// expected
}
}
}

View File

@ -250,6 +250,7 @@ def build_compatible_license_names():
compatible_licenses['CDDL/GPLv2+CE'] = 'CDDL 1.1'
compatible_licenses['CDDL + GPLv2 with classpath exception'] = 'CDDL 1.1'
compatible_licenses['CDDL License'] = 'CDDL 1.1'
compatible_licenses['COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0'] = 'CDDL 1.0'
compatible_licenses['Eclipse Public License 1.0'] = 'Eclipse Public License 1.0'
compatible_licenses['The Eclipse Public License, Version 1.0'] = 'Eclipse Public License 1.0'

View File

@ -240,6 +240,8 @@
<argument>org.apache.druid.extensions:simple-client-sslcontext</argument>
<argument>-c</argument>
<argument>org.apache.druid.extensions:druid-basic-security</argument>
<argument>-c</argument>
<argument>org.apache.druid.extensions:druid-pac4j</argument>
<argument>${druid.distribution.pulldeps.opts}</argument>
</arguments>
</configuration>

View File

@ -0,0 +1,45 @@
---
id: druid-pac4j
title: "Druid pac4j based Security extension"
---
<!--
~ 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.
-->
Apache Druid Extension to enable [OpenID Connect](https://openid.net/connect/) based Authentication for Druid Processes using [pac4j](https://github.com/pac4j/pac4j) as the underlying client library.
This can be used with any authentication server that supports same e.g. [Okta](https://developer.okta.com/).
This extension should only be used at the router node to enable a group of users in existing authentication server to interact with Druid cluster, using the [Web Console](../../operations/druid-console.html). This extension does not support JDBC client authentication.
## Configuration
### Creating an Authenticator
```
druid.auth.authenticatorChain=["pac4j"]
druid.auth.authenticator.pac4j.type=pac4j
```
### Properties
|Property|Description|Default|required|
|--------|---------------|-----------|-------|--------|
|`druid.auth.pac4j.oidc.clientID`|OAuth Client Application id.|none|Yes|
|`druid.auth.pac4j.oidc.clientSecret`|OAuth Client Application secret. It can be provided as plaintext string or The [Password Provider](../../operations/password-provider.md).|none|Yes|
|`druid.auth.pac4j.oidc.discoveryURI`|discovery URI for fetching OP metadata [see this](http://openid.net/specs/openid-connect-discovery-1_0.html).|none|Yes|
|`druid.auth.pac4j.oidc.cookiePassphrase`|passphrase for encrypting the cookies used to manage authentication session with browser. It can be provided as plaintext string or The [Password Provider](../../operations/password-provider.md).|none|Yes|

View File

@ -60,6 +60,7 @@ Core extensions are maintained by Druid committers.
|mysql-metadata-storage|MySQL metadata store.|[link](../development/extensions-core/mysql.md)|
|postgresql-metadata-storage|PostgreSQL metadata store.|[link](../development/extensions-core/postgresql.md)|
|simple-client-sslcontext|Simple SSLContext provider module to be used by Druid's internal HttpClient when talking to other Druid processes over HTTPS.|[link](../development/extensions-core/simple-client-sslcontext.md)|
|druid-pac4j|OpenID Connect authentication for druid processes.|[link](../development/extensions-core/druid-pac4j.md)|
## Community extensions

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.druid.extensions</groupId>
<artifactId>druid-pac4j</artifactId>
<name>druid-pac4j</name>
<description>druid-pac4j</description>
<parent>
<groupId>org.apache.druid</groupId>
<artifactId>druid</artifactId>
<version>0.18.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
<pac4j.version>3.8.3</pac4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.druid</groupId>
<artifactId>druid-server</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>${pac4j.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.druid</groupId>
<artifactId>druid-core</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-core</artifactId>
<version>${pac4j.version}</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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.druid.security.pac4j;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import org.apache.druid.metadata.PasswordProvider;
public class OIDCConfig
{
@JsonProperty
private final String clientID;
@JsonProperty
private final PasswordProvider clientSecret;
@JsonProperty
private final String discoveryURI;
@JsonProperty
private final PasswordProvider cookiePassphrase;
@JsonCreator
public OIDCConfig(
@JsonProperty("clientID") String clientID,
@JsonProperty("clientSecret") PasswordProvider clientSecret,
@JsonProperty("discoveryURI") String discoveryURI,
@JsonProperty("cookiePassphrase") PasswordProvider cookiePassphrase
)
{
this.clientID = Preconditions.checkNotNull(clientID, "null clientID");
this.clientSecret = Preconditions.checkNotNull(clientSecret, "null clientSecret");
this.discoveryURI = Preconditions.checkNotNull(discoveryURI, "null discoveryURI");
this.cookiePassphrase = Preconditions.checkNotNull(cookiePassphrase, "null cookiePassphrase");
}
@JsonProperty
public String getClientID()
{
return clientID;
}
@JsonProperty
public PasswordProvider getClientSecret()
{
return clientSecret;
}
@JsonProperty
public String getDiscoveryURI()
{
return discoveryURI;
}
@JsonProperty
public PasswordProvider getCookiePassphrase()
{
return cookiePassphrase;
}
}

View File

@ -0,0 +1,127 @@
/*
* 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.druid.security.pac4j;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import org.apache.druid.server.security.AuthenticationResult;
import org.apache.druid.server.security.Authenticator;
import org.pac4j.core.config.Config;
import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
import org.pac4j.core.http.url.DefaultUrlResolver;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration;
import javax.annotation.Nullable;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.EnumSet;
import java.util.Map;
@JsonTypeName("pac4j")
public class Pac4jAuthenticator implements Authenticator
{
private final String name;
private final String authorizerName;
private final Supplier<Config> pac4jConfigSupplier;
private final OIDCConfig oidcConfig;
@JsonCreator
public Pac4jAuthenticator(
@JsonProperty("name") String name,
@JsonProperty("authorizerName") String authorizerName,
@JacksonInject OIDCConfig oidcConfig
)
{
this.name = name;
this.authorizerName = authorizerName;
this.oidcConfig = oidcConfig;
this.pac4jConfigSupplier = Suppliers.memoize(() -> createPac4jConfig(oidcConfig));
}
@Override
public Filter getFilter()
{
return new Pac4jFilter(
name,
authorizerName,
pac4jConfigSupplier.get(),
oidcConfig.getCookiePassphrase().getPassword()
);
}
@Override
public String getAuthChallengeHeader()
{
return null;
}
@Override
@Nullable
public AuthenticationResult authenticateJDBCContext(Map<String, Object> context)
{
return null;
}
@Override
public Class<? extends Filter> getFilterClass()
{
return null;
}
@Override
public Map<String, String> getInitParameters()
{
return null;
}
@Override
public String getPath()
{
return "/*";
}
@Override
public EnumSet<DispatcherType> getDispatcherType()
{
return null;
}
private Config createPac4jConfig(OIDCConfig oidcConfig)
{
OidcConfiguration oidcConf = new OidcConfiguration();
oidcConf.setClientId(oidcConfig.getClientID());
oidcConf.setSecret(oidcConfig.getClientSecret().getPassword());
oidcConf.setDiscoveryURI(oidcConfig.getDiscoveryURI());
oidcConf.setExpireSessionWithToken(true);
oidcConf.setUseNonce(true);
OidcClient oidcClient = new OidcClient(oidcConf);
oidcClient.setUrlResolver(new DefaultUrlResolver(true));
oidcClient.setCallbackUrlResolver(new NoParameterCallbackUrlResolver());
return new Config(Pac4jCallbackResource.SELF_URL, oidcClient);
}
}

View File

@ -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.druid.security.pac4j;
import com.google.inject.Inject;
import org.apache.druid.guice.LazySingleton;
import org.apache.druid.java.util.common.logger.Logger;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
/**
* Fixed Callback endpoint used after successful login with Identity Provider e.g. OAuth server.
* See https://www.pac4j.org/blog/understanding-the-callback-endpoint.html
*/
@Path(Pac4jCallbackResource.SELF_URL)
@LazySingleton
public class Pac4jCallbackResource
{
public static final String SELF_URL = "/druid-ext/druid-pac4j/callback";
private static final Logger LOGGER = new Logger(Pac4jCallbackResource.class);
@Inject
public Pac4jCallbackResource()
{
}
@GET
public Response callback()
{
LOGGER.error(
new RuntimeException(),
"This endpoint is to be handled by the pac4j filter to redirect users, request should never reach here."
);
return Response.serverError().build();
}
}

View File

@ -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.druid.security.pac4j;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.ImmutableList;
import com.google.inject.Binder;
import org.apache.druid.guice.Jerseys;
import org.apache.druid.guice.JsonConfigProvider;
import org.apache.druid.initialization.DruidModule;
import java.util.List;
public class Pac4jDruidModule implements DruidModule
{
@Override
public List<? extends Module> getJacksonModules()
{
return ImmutableList.of(
new SimpleModule("Pac4jDruidSecurity").registerSubtypes(
Pac4jAuthenticator.class
)
);
}
@Override
public void configure(Binder binder)
{
JsonConfigProvider.bind(binder, "druid.auth.pac4j.oidc", OIDCConfig.class);
Jerseys.addResource(binder, Pac4jCallbackResource.class);
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.druid.security.pac4j;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.server.security.AuthConfig;
import org.apache.druid.server.security.AuthenticationResult;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.J2EContext;
import org.pac4j.core.context.session.SessionStore;
import org.pac4j.core.engine.CallbackLogic;
import org.pac4j.core.engine.DefaultCallbackLogic;
import org.pac4j.core.engine.DefaultSecurityLogic;
import org.pac4j.core.engine.SecurityLogic;
import org.pac4j.core.http.adapter.HttpActionAdapter;
import org.pac4j.core.profile.CommonProfile;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
public class Pac4jFilter implements Filter
{
private static final Logger LOGGER = new Logger(Pac4jFilter.class);
private static final HttpActionAdapter<String, J2EContext> NOOP_HTTP_ACTION_ADAPTER = (int code, J2EContext ctx) -> null;
private final Config pac4jConfig;
private final SecurityLogic<String, J2EContext> securityLogic;
private final CallbackLogic<String, J2EContext> callbackLogic;
private final SessionStore<J2EContext> sessionStore;
private final String name;
private final String authorizerName;
public Pac4jFilter(String name, String authorizerName, Config pac4jConfig, String cookiePassphrase)
{
this.pac4jConfig = pac4jConfig;
this.securityLogic = new DefaultSecurityLogic<>();
this.callbackLogic = new DefaultCallbackLogic<>();
this.name = name;
this.authorizerName = authorizerName;
this.sessionStore = new Pac4jSessionStore<>(cookiePassphrase);
}
@Override
public void init(FilterConfig filterConfig)
{
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException
{
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
J2EContext context = new J2EContext(httpServletRequest, httpServletResponse, sessionStore);
if (Pac4jCallbackResource.SELF_URL.equals(httpServletRequest.getRequestURI())) {
callbackLogic.perform(
context,
pac4jConfig,
NOOP_HTTP_ACTION_ADAPTER,
"/",
true, false, false, null);
} else {
String uid = securityLogic.perform(
context,
pac4jConfig,
(J2EContext ctx, Collection<CommonProfile> profiles, Object... parameters) -> {
if (profiles.isEmpty()) {
LOGGER.warn("No profiles found after OIDC auth.");
return null;
} else {
return profiles.iterator().next().getId();
}
},
NOOP_HTTP_ACTION_ADAPTER,
null, null, null, null);
if (uid != null) {
AuthenticationResult authenticationResult = new AuthenticationResult(uid, authorizerName, name, null);
servletRequest.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT, authenticationResult);
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
@Override
public void destroy()
{
}
}

View File

@ -0,0 +1,211 @@
/*
* 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.druid.security.pac4j;
import org.apache.commons.io.IOUtils;
import org.apache.druid.crypto.CryptoService;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.logger.Logger;
import org.pac4j.core.context.ContextHelper;
import org.pac4j.core.context.Cookie;
import org.pac4j.core.context.Pac4jConstants;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.context.session.SessionStore;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.util.JavaSerializationHelper;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Code here is slight adaptation from <a href="https://github.com/apache/knox/blob/master/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/session/KnoxSessionStore.java">KnoxSessionStore</a>
* for storing oauth session information in cookies.
*/
public class Pac4jSessionStore<T extends WebContext> implements SessionStore<T>
{
private static final Logger LOGGER = new Logger(Pac4jSessionStore.class);
public static final String PAC4J_SESSION_PREFIX = "pac4j.session.";
private final JavaSerializationHelper javaSerializationHelper;
private final CryptoService cryptoService;
public Pac4jSessionStore(String cookiePassphrase)
{
javaSerializationHelper = new JavaSerializationHelper();
cryptoService = new CryptoService(
cookiePassphrase,
"AES",
"CBC",
"PKCS5Padding",
"PBKDF2WithHmacSHA256",
8,
65536,
128
);
}
@Override
public String getOrCreateSessionId(WebContext context)
{
return null;
}
@Nullable
@Override
public Object get(WebContext context, String key)
{
final Cookie cookie = ContextHelper.getCookie(context, PAC4J_SESSION_PREFIX + key);
Object value = null;
if (cookie != null) {
value = uncompressDecryptBase64(cookie.getValue());
}
LOGGER.debug("Get from session: [%s] = [%s]", key, value);
return value;
}
@Override
public void set(WebContext context, String key, @Nullable Object value)
{
Object profile = value;
Cookie cookie;
if (value == null) {
cookie = new Cookie(PAC4J_SESSION_PREFIX + key, null);
} else {
if (key.contentEquals(Pac4jConstants.USER_PROFILES)) {
/* trim the profile object */
profile = clearUserProfile(value);
}
LOGGER.debug("Save in session: [%s] = [%s]", key, profile);
cookie = new Cookie(
PAC4J_SESSION_PREFIX + key,
compressEncryptBase64(profile)
);
}
cookie.setDomain("");
cookie.setHttpOnly(true);
cookie.setSecure(ContextHelper.isHttpsOrSecure(context));
cookie.setPath("/");
cookie.setMaxAge(900);
context.addResponseCookie(cookie);
}
@Nullable
private String compressEncryptBase64(final Object o)
{
if (o == null || "".equals(o)
|| (o instanceof Map<?, ?> && ((Map<?, ?>) o).isEmpty())) {
return null;
} else {
byte[] bytes = javaSerializationHelper.serializeToBytes((Serializable) o);
bytes = compress(bytes);
if (bytes.length > 3000) {
LOGGER.warn("Cookie too big, it might not be properly set");
}
return StringUtils.encodeBase64String(cryptoService.encrypt(bytes));
}
}
@Nullable
private Serializable uncompressDecryptBase64(final String v)
{
if (v != null && !v.isEmpty()) {
byte[] bytes = StringUtils.decodeBase64String(v);
if (bytes != null) {
return javaSerializationHelper.unserializeFromBytes(unCompress(cryptoService.decrypt(bytes)));
}
}
return null;
}
private byte[] compress(final byte[] data)
{
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(data.length)) {
try (GZIPOutputStream gzip = new GZIPOutputStream(byteStream)) {
gzip.write(data);
}
return byteStream.toByteArray();
}
catch (IOException ex) {
throw new TechnicalException(ex);
}
}
private byte[] unCompress(final byte[] data)
{
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
GZIPInputStream gzip = new GZIPInputStream(inputStream)) {
return IOUtils.toByteArray(gzip);
}
catch (IOException ex) {
throw new TechnicalException(ex);
}
}
private Object clearUserProfile(final Object value)
{
if (value instanceof Map<?, ?>) {
final Map<String, CommonProfile> profiles = (Map<String, CommonProfile>) value;
profiles.forEach((name, profile) -> profile.clearSensitiveData());
return profiles;
} else {
final CommonProfile profile = (CommonProfile) value;
profile.clearSensitiveData();
return profile;
}
}
@Override
public SessionStore buildFromTrackableSession(WebContext arg0, Object arg1)
{
return null;
}
@Override
public boolean destroySession(WebContext arg0)
{
return false;
}
@Override
public Object getTrackableSession(WebContext arg0)
{
return null;
}
@Override
public boolean renewSession(final WebContext context)
{
return false;
}
}

View File

@ -0,0 +1,16 @@
# 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.
org.apache.druid.security.pac4j.Pac4jDruidModule

View File

@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.druid.security.pac4j;
import org.easymock.Capture;
import org.easymock.EasyMock;
import org.junit.Assert;
import org.junit.Test;
import org.pac4j.core.context.Cookie;
import org.pac4j.core.context.WebContext;
import java.util.Collections;
public class Pac4jSessionStoreTest
{
@Test
public void testSetAndGet()
{
Pac4jSessionStore<WebContext> sessionStore = new Pac4jSessionStore("test-cookie-passphrase");
WebContext webContext1 = EasyMock.mock(WebContext.class);
EasyMock.expect(webContext1.getScheme()).andReturn("https");
Capture<Cookie> cookieCapture = EasyMock.newCapture();
webContext1.addResponseCookie(EasyMock.capture(cookieCapture));
EasyMock.replay(webContext1);
sessionStore.set(webContext1, "key", "value");
Cookie cookie = cookieCapture.getValue();
Assert.assertTrue(cookie.isSecure());
Assert.assertTrue(cookie.isHttpOnly());
Assert.assertTrue(cookie.isSecure());
Assert.assertEquals(900, cookie.getMaxAge());
WebContext webContext2 = EasyMock.mock(WebContext.class);
EasyMock.expect(webContext2.getRequestCookies()).andReturn(Collections.singletonList(cookie));
EasyMock.replay(webContext2);
Assert.assertEquals("value", sessionStore.get(webContext2, "key"));
}
}

View File

@ -136,6 +136,16 @@ source_paths:
---
name: code adapted from Apache Knox KnoxSessionStore and ConfigurableEncryptor
license_category: source
module: extensions/druid-pac4j
license_name: Apache License version 2.0
source_paths:
- extensions-core/druid-pac4j/src/main/java/org/apache/druid/security/pac4j/Pac4jSessionStore.java
- core/src/main/java/org/apache/druid/crypto/CryptoService.java
---
name: AWS SDK for Java
license_category: binary
module: java-core
@ -654,6 +664,116 @@ libraries:
---
name: pac4j-oidc java security library
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 3.8.3
libraries:
- org.pac4j: pac4j-oidc
---
name: pac4j-core java security library
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 3.8.3
libraries:
- org.pac4j: pac4j-core
---
name: org.objenesis objenesis
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 3.0.1
libraries:
- org.objenesis: objenesis
---
name: com.nimbusds lang-tag
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 1.4.4
libraries:
- com.nimbusds: lang-tag
---
name: com.nimbusds nimbus-jose-jwt
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 7.9
libraries:
- com.nimbusds: nimbus-jose-jwt
---
name: com.nimbusds oauth2-oidc-sdk
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 6.5
libraries:
- com.nimbusds: oauth2-oidc-sdk
---
name: net.bytebuddy byte-buddy
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 1.9.10
libraries:
- net.bytebuddy: byte-buddy
---
name: net.bytebuddy byte-buddy-agent
license_category: binary
module: extensions/druid-pac4j
license_name: Apache License version 2.0
version: 1.9.10
libraries:
- net.bytebuddy: byte-buddy-agent
---
name: org.mockito mockito-core
license_category: binary
module: extensions/druid-pac4j
license_name: MIT License
version: 2.28.2
libraries:
- org.mockito: mockito-core
---
name: javax.activation activation
license_category: binary
module: extensions/druid-pac4j
license_name: COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
version: 1.1.1
libraries:
- javax.activation: activation
---
name: com.sun.mail javax.mail
license_category: binary
module: extensions/druid-pac4j
license_name: CDDL 1.1
version: 1.6.1
libraries:
- com.sun.mail: javax.mail
---
name: Netty
license_category: binary
module: java-core

View File

@ -143,6 +143,7 @@
<module>extensions-core/datasketches</module>
<module>extensions-core/druid-bloom-filter</module>
<module>extensions-core/druid-kerberos</module>
<module>extensions-core/druid-pac4j</module>
<module>extensions-core/hdfs-storage</module>
<module>extensions-core/histogram</module>
<module>extensions-core/stats</module>

View File

@ -0,0 +1,56 @@
/*
* 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.druid.server.security;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* Sets necessary request attributes for requests sent to endpoints that need authentication but no authorization.
*/
public class AuthenticationOnlyResourceFilter implements Filter
{
@Override
public void init(FilterConfig filterConfig)
{
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException
{
// This request will not go to an Authorizer, so we need to set this for PreResponseAuthorizationCheckFilter
servletRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy()
{
}
}

View File

@ -64,6 +64,13 @@ public class AuthenticationUtils
}
}
public static void addNoopAuthorizationFilters(ServletContextHandler root, List<String> unsecuredPaths)
{
for (String unsecuredPath : unsecuredPaths) {
root.addFilter(new FilterHolder(new AuthenticationOnlyResourceFilter()), unsecuredPath, null);
}
}
public static void addSecuritySanityCheckFilter(
ServletContextHandler root,
ObjectMapper jsonMapper

View File

@ -39,10 +39,13 @@ class WebConsoleJettyServerInitializer
"/favicon.png",
"/assets/*",
"/public/*",
WEB_CONSOLE_ROOT,
"/console-config.js"
);
private static final List<String> UNAUTHORIZED_PATHS_FOR_UI = ImmutableList.of(
WEB_CONSOLE_ROOT
);
static void intializeServerForWebConsoleRoot(ServletContextHandler root)
{
root.setInitParameter("org.eclipse.jetty.servlet.Default.redirectWelcome", "true");
@ -51,6 +54,7 @@ class WebConsoleJettyServerInitializer
root.setBaseResource(Resource.newClassPathResource("org/apache/druid/console"));
AuthenticationUtils.addNoopAuthenticationAndAuthorizationFilters(root, UNSECURED_PATHS_FOR_UI);
AuthenticationUtils.addNoopAuthorizationFilters(root, UNAUTHORIZED_PATHS_FOR_UI);
}
static Handler createWebConsoleRewriteHandler()

View File

@ -225,6 +225,7 @@ deserialization
deserialize
deserialized
downtimes
druid
e.g.
encodings
endian
@ -645,6 +646,11 @@ maximumSize
onHeapPolling
pollPeriod
reverseLoadingCacheSpec
- ../docs/development/extensions-core/druid-pac4j.md
OAuth
Okta
OpenID
pac4j
- ../docs/development/extensions-core/google.md
GCS
StaticGoogleBlobStoreFirehose