/* Copyright (c) 2011 Danish Maritime Authority.
*
* 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 net.maritimecloud.mms.server.security.impl;
import com.typesafe.config.Config;
import net.maritimecloud.mms.server.security.AuthenticationException;
import net.maritimecloud.mms.server.security.AuthenticationHandler;
import net.maritimecloud.mms.server.security.AuthenticationToken;
import org.apache.commons.codec.digest.Crypt;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.Md5Crypt;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The class implements support for an Apache webserver-style configuration with
* authentication against a htpasswd file.
*
* The class implements the {@code AuthenticationHandler} interface and attempts
* to authenticate the client from an Apache htpasswd-style credentials file.
* <p/>
* The authentication token must be of type {@code UsernamePasswordToken}.
*
* <p>The security configuration file must specify the "htpasswd-file" attribute, pointing
* to an Apache htpasswd-style credentials file</p>
*/
@SuppressWarnings("unused")
public class ApacheConfSecurityHandler implements AuthenticationHandler {
private final static Pattern HTPASSWD_ENTRY = Pattern.compile("^([^:]+):(.+)");
private Config conf;
/** User + encrypted passwords loaded from Apache htpasswd-style file */
private Map<String, String> userPasswords = new HashMap<>();
/** Last modified timestamp of the htpasswd file */
private long htpasswdFileLastModified = -1L;
/** {@inheritDoc} */
@Override
public void init(Config conf) {
this.conf = conf;
}
/** {@inheritDoc} */
@Override
public Config getConf() {
return conf;
}
/*************************************************/
/** Authentication Support **/
/*************************************************/
/** {@inheritDoc} */
@Override
public void authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null || !(token instanceof UsernamePasswordToken)) {
throw new AuthenticationException("Invalid authentication token: " + token);
}
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
try {
String htpasswdFile = getConf().getString("htpasswd-file");
if (!authenticate(usernamePasswordToken, new File(htpasswdFile))) {
throw new AuthenticationException("Invalid username-password");
}
} catch (IOException e) {
throw new AuthenticationException("Error executing htpasswd authentication", e);
}
}
/**
* Check if the credentials are valid according to the Apache htpasswd-style credentials file
*
* @param token the credentials to check
* @param htpasswdFile the htpasswd file
* @return if the credentials are valid
*/
protected boolean authenticate(UsernamePasswordToken token, File htpasswdFile) throws IOException {
Objects.requireNonNull(token);
Objects.requireNonNull(htpasswdFile);
// Read in the htpasswd file
checkReadHtpasswdFile(htpasswdFile);
String storedPwd = userPasswords.get(token.getUsername());
if (storedPwd != null) {
final String passwd = new String(token.getPassword());
// test Apache MD5 variant encrypted password
if (storedPwd.startsWith("$apr1$")) {
return storedPwd.equals(Md5Crypt.apr1Crypt(passwd, storedPwd));
}
// test unsalted SHA password
else if (storedPwd.startsWith("{SHA}")) {
String passwd64 = org.apache.commons.codec.binary.Base64.encodeBase64String(DigestUtils.sha1(passwd));
return storedPwd.substring("{SHA}".length()).equals(passwd64);
}
// test libc crypt() encoded password
else if (storedPwd.equals(Crypt.crypt(passwd, storedPwd))) {
return true;
}
// test clear text
else if (storedPwd.equals(passwd)) {
return true;
}
}
// Not authenticated
return false;
}
/**
* Check if the htpasswd file has been updated since last time it was read.
* Reads in all users + encrypted password.
*
* @param htpasswdFile the password file
*/
protected synchronized void checkReadHtpasswdFile(File htpasswdFile) throws IOException {
if (!htpasswdFile.exists()) {
throw new IOException("File does not exist: " + htpasswdFile);
}
// Only read it, if the file has been updated ... and the first time called.
if (htpasswdFile.lastModified() != htpasswdFileLastModified) {
userPasswords.clear();
try (Scanner scanner = new Scanner(new FileInputStream(htpasswdFile))) {
while( scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
if ( !line.isEmpty() && !line.startsWith("#") ) {
Matcher m = HTPASSWD_ENTRY.matcher(line);
if ( m.matches() ) {
userPasswords.put(m.group(1), m.group(2));
}
}
}
}
// Record time stamp
htpasswdFileLastModified = htpasswdFile.lastModified();
}
}
}