-
Notifications
You must be signed in to change notification settings - Fork 3.8k
druid extension for OpenID Connect auth using pac4j lib #8992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
45e01b3
7090b75
7a7eb69
1368bf4
9e4e084
faf402e
95594fa
111d474
dcfe56a
315d480
cbf3500
10679e8
ddbdf72
5000ab2
230c85c
1e515ef
44eb2b5
4076497
212e477
2f2fd60
78512c6
145cfc3
4ccbdcc
eb3969a
16609f7
ba21d4c
64f9954
929ab5d
3fb747c
9ab87fa
ae9e823
35c4244
dbe8330
a3fd86b
cd8a9f8
455dcba
5bcc6da
0d93789
d1d128d
b6ead6f
c357169
db99f2a
ab6cdbe
1f6712e
8c13af1
4ade873
07a8257
1a4ab02
318b2d6
101a53b
0bc2e05
335b439
53010c7
027accf
1a1f2a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: it would be nice to have more unit tests such as failures in the constructor,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added another UT for constructor failure due to invalid params, however there can be many different invalid combination of algo names, modes etc .. generic check in constructor caches them all. |
||
| { | ||
| @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 | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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| | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is added in the
druid-coremodule even though it's used in only thedruid-pac4jextension. I guess you want to use this class in other places in the future. In that case, some detailed Javadoc would help.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, added