-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add file based password authenticator plugin
Cherry-pick of trinodb/trino#1912 (trinodb/trino#1912) Co-authored-by: David Phillips <[email protected]> Co-authored-by: Rupam Kundu <[email protected]>
- Loading branch information
Showing
17 changed files
with
788 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
presto-password-authenticators/src/main/java/com/facebook/presto/password/Credential.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* Licensed 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 com.facebook.presto.password; | ||
|
||
import java.util.Objects; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
public final class Credential | ||
{ | ||
private final String user; | ||
private final String password; | ||
|
||
public Credential(String username, String password) | ||
{ | ||
this.user = requireNonNull(username, "username is null"); | ||
this.password = requireNonNull(password, "password is null"); | ||
} | ||
|
||
public String getUser() | ||
{ | ||
return user; | ||
} | ||
|
||
public String getPassword() | ||
{ | ||
return password; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object obj) | ||
{ | ||
if (this == obj) { | ||
return true; | ||
} | ||
if ((obj == null) || (getClass() != obj.getClass())) { | ||
return false; | ||
} | ||
Credential o = (Credential) obj; | ||
return Objects.equals(user, o.getUser()) && | ||
Objects.equals(password, o.getPassword()); | ||
} | ||
|
||
@Override | ||
public int hashCode() | ||
{ | ||
return Objects.hash(user, password); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
...ssword-authenticators/src/main/java/com/facebook/presto/password/file/EncryptionUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
/* | ||
* Licensed 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 com.facebook.presto.password.file; | ||
|
||
import at.favre.lib.crypto.bcrypt.BCrypt; | ||
import at.favre.lib.crypto.bcrypt.IllegalBCryptFormatException; | ||
import com.google.common.base.Splitter; | ||
|
||
import javax.crypto.SecretKeyFactory; | ||
import javax.crypto.spec.PBEKeySpec; | ||
|
||
import java.security.MessageDigest; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.security.spec.KeySpec; | ||
import java.util.List; | ||
|
||
import static com.google.common.base.Preconditions.checkArgument; | ||
import static com.google.common.io.BaseEncoding.base16; | ||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
import static java.util.Objects.requireNonNull; | ||
|
||
public final class EncryptionUtil | ||
{ | ||
private static final int BCRYPT_MIN_COST = 8; | ||
private static final int PBKDF2_MIN_ITERATIONS = 1000; | ||
|
||
private EncryptionUtil() {} | ||
|
||
public static int getBCryptCost(String password) | ||
{ | ||
try { | ||
return BCrypt.Version.VERSION_2A.parser.parse(password.getBytes(UTF_8)).cost; | ||
} | ||
catch (IllegalBCryptFormatException e) { | ||
throw new HashedPasswordException("Invalid BCrypt password", e); | ||
} | ||
} | ||
|
||
public static int getPBKDF2Iterations(String password) | ||
{ | ||
return PBKDF2Password.fromString(password).iterations(); | ||
} | ||
|
||
public static boolean doesBCryptPasswordMatch(String inputPassword, String hashedPassword) | ||
{ | ||
return BCrypt.verifyer().verify(inputPassword.toCharArray(), hashedPassword).verified; | ||
} | ||
|
||
public static boolean doesPBKDF2PasswordMatch(String inputPassword, String hashedPassword) | ||
{ | ||
PBKDF2Password password = PBKDF2Password.fromString(hashedPassword); | ||
|
||
try { | ||
KeySpec spec = new PBEKeySpec(inputPassword.toCharArray(), password.salt(), password.iterations(), password.hash().length * 8); | ||
SecretKeyFactory key = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); | ||
byte[] inputHash = key.generateSecret(spec).getEncoded(); | ||
|
||
if (password.hash().length != inputHash.length) { | ||
throw new HashedPasswordException("PBKDF2 password input is malformed"); | ||
} | ||
return MessageDigest.isEqual(password.hash(), inputHash); | ||
} | ||
catch (NoSuchAlgorithmException | InvalidKeySpecException e) { | ||
throw new HashedPasswordException("Invalid PBKDF2 password", e); | ||
} | ||
} | ||
|
||
public static HashingAlgorithm getHashingAlgorithm(String password) | ||
{ | ||
if (password.startsWith("$2y")) { | ||
if (getBCryptCost(password) < BCRYPT_MIN_COST) { | ||
throw new HashedPasswordException("Minimum cost of BCrypt password must be " + BCRYPT_MIN_COST); | ||
} | ||
return HashingAlgorithm.BCRYPT; | ||
} | ||
|
||
if (password.contains(":")) { | ||
if (getPBKDF2Iterations(password) < PBKDF2_MIN_ITERATIONS) { | ||
throw new HashedPasswordException("Minimum iterations of PBKDF2 password must be " + PBKDF2_MIN_ITERATIONS); | ||
} | ||
return HashingAlgorithm.PBKDF2; | ||
} | ||
|
||
throw new HashedPasswordException("Password hashing algorithm cannot be determined"); | ||
} | ||
|
||
private static class PBKDF2Password | ||
{ | ||
private final int iterations; | ||
private final byte[] salt; | ||
private final byte[] hash; | ||
|
||
private PBKDF2Password(int iterations, byte[] salt, byte[] hash) | ||
{ | ||
this.iterations = iterations; | ||
this.salt = requireNonNull(salt, "salt is null"); | ||
this.hash = requireNonNull(hash, "hash is null"); | ||
} | ||
|
||
public int iterations() | ||
{ | ||
return iterations; | ||
} | ||
|
||
public byte[] salt() | ||
{ | ||
return salt; | ||
} | ||
|
||
public byte[] hash() | ||
{ | ||
return hash; | ||
} | ||
|
||
public static PBKDF2Password fromString(String password) | ||
{ | ||
try { | ||
List<String> parts = Splitter.on(":").splitToList(password); | ||
checkArgument(parts.size() == 3, "wrong part count"); | ||
|
||
int iterations = Integer.parseInt(parts.get(0)); | ||
byte[] salt = base16().lowerCase().decode(parts.get(1)); | ||
byte[] hash = base16().lowerCase().decode(parts.get(2)); | ||
|
||
return new PBKDF2Password(iterations, salt, hash); | ||
} | ||
catch (IllegalArgumentException e) { | ||
throw new HashedPasswordException("Invalid PBKDF2 password"); | ||
} | ||
} | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
...ord-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/* | ||
* Licensed 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 com.facebook.presto.password.file; | ||
|
||
import com.facebook.airlift.http.server.BasicPrincipal; | ||
import com.facebook.airlift.log.Logger; | ||
import com.facebook.presto.spi.security.AccessDeniedException; | ||
import com.facebook.presto.spi.security.PasswordAuthenticator; | ||
|
||
import javax.inject.Inject; | ||
|
||
import java.io.File; | ||
import java.io.FileNotFoundException; | ||
import java.security.Principal; | ||
import java.util.function.Supplier; | ||
|
||
import static com.google.common.base.Suppliers.memoizeWithExpiration; | ||
import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||
|
||
public class FileAuthenticator | ||
implements PasswordAuthenticator | ||
{ | ||
private static final Logger log = Logger.get(FileAuthenticator.class); | ||
private final Supplier<PasswordStore> passwordStoreSupplier; | ||
|
||
@Inject | ||
public FileAuthenticator(FileConfig config) throws FileNotFoundException | ||
{ | ||
File file = config.getPasswordFile(); | ||
if (!file.exists()) { | ||
log.error("File %s does not exist", file.getAbsolutePath()); | ||
throw new FileNotFoundException("File " + file.getAbsolutePath() + " does not exist"); | ||
} | ||
int cacheMaxSize = config.getAuthTokenCacheMaxSize(); | ||
|
||
passwordStoreSupplier = memoizeWithExpiration( | ||
() -> new PasswordStore(file, cacheMaxSize), | ||
config.getRefreshPeriod().toMillis(), | ||
MILLISECONDS); | ||
} | ||
|
||
@Override | ||
public Principal createAuthenticatedPrincipal(String user, String password) | ||
{ | ||
if (!passwordStoreSupplier.get().authenticate(user, password)) { | ||
throw new AccessDeniedException("Invalid credentials"); | ||
} | ||
|
||
return new BasicPrincipal(user); | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
...henticators/src/main/java/com/facebook/presto/password/file/FileAuthenticatorFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/* | ||
* Licensed 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 com.facebook.presto.password.file; | ||
|
||
import com.facebook.airlift.bootstrap.Bootstrap; | ||
import com.facebook.presto.spi.security.PasswordAuthenticator; | ||
import com.facebook.presto.spi.security.PasswordAuthenticatorFactory; | ||
import com.google.inject.Injector; | ||
import com.google.inject.Scopes; | ||
|
||
import java.util.Map; | ||
|
||
import static com.facebook.airlift.configuration.ConfigBinder.configBinder; | ||
import static com.google.common.base.Throwables.throwIfUnchecked; | ||
|
||
public class FileAuthenticatorFactory | ||
implements PasswordAuthenticatorFactory | ||
{ | ||
@Override | ||
public String getName() | ||
{ | ||
return "file"; | ||
} | ||
|
||
@Override | ||
public PasswordAuthenticator create(Map<String, String> config) | ||
{ | ||
try { | ||
Bootstrap app = new Bootstrap( | ||
binder -> { | ||
configBinder(binder).bindConfig(FileConfig.class); | ||
binder.bind(FileAuthenticator.class).in(Scopes.SINGLETON); | ||
}); | ||
|
||
Injector injector = app | ||
.doNotInitializeLogging() | ||
.setRequiredConfigurationProperties(config) | ||
.initialize(); | ||
|
||
return injector.getInstance(FileAuthenticator.class); | ||
} | ||
catch (Exception e) { | ||
throwIfUnchecked(e); | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
Oops, something went wrong.