Java tutorial
// Copyright 2017 JanusGraph Authors // // 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 org.janusgraph.graphdb.tinkerpop.gremlin.server.auth; import static org.apache.tinkerpop.gremlin.groovy.jsr223.dsl.credential.CredentialGraphTokens.PROPERTY_PASSWORD; import static org.apache.tinkerpop.gremlin.groovy.jsr223.dsl.credential.CredentialGraphTokens.PROPERTY_USERNAME; import static org.janusgraph.graphdb.tinkerpop.gremlin.server.handler.HttpHMACAuthenticationHandler.PROPERTY_GENERATE_TOKEN; import static org.janusgraph.graphdb.tinkerpop.gremlin.server.handler.HttpHMACAuthenticationHandler.PROPERTY_TOKEN; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Base64; import java.util.Date; import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.lang3.StringUtils; import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser; import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.mindrot.jbcrypt.BCrypt; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; /** * A class for doing Basic Auth and Token auth using an HMAC intended to be used with * the HMACAuthenticationHandler * * @author Keith Lohnes lohnesk@gmail.com */ public class HMACAuthenticator extends JanusGraphAbstractAuthenticator { /** * Hmac algorithm defaults to hmacsha256 */ public static final String CONFIG_HMAC_ALGO = "hmacAlgo"; /** * How long an auth token should stay valid */ public static final String CONFIG_TOKEN_TIMEOUT = "tokenTimeout"; /** * Hmac secret config */ public static final String CONFIG_HMAC_SECRET = "hmacSecret"; private static final String AUTH_ERROR = "Username and/or password are incorrect"; private static final String DEFAULT_HMAC_ALGO = "HmacSHA256"; private static final char[] DEFAULT_HMAC_SECRET = "secret".toCharArray(); private static final Long DEFAULT_HMAC_TOKEN_TIMEOUT = 3600000L; private char[] secret; private String hmacAlgo; private Long timeout; @Override public boolean requireAuthentication() { return true; } @Override public SaslNegotiator newSaslNegotiator(final InetAddress remoteAddress) { throw new RuntimeException("HMACAuthenticator does not use SASL!"); } public SaslNegotiator newSaslNegotiator() { throw new RuntimeException("HMACAuthenticator does not use SASL!"); } public void setup(final Map<String, Object> config) { Preconditions.checkArgument(config != null, "Credential configuration cannot be null"); Preconditions.checkState(config.containsKey(CONFIG_HMAC_SECRET), String.format("Credential configuration missing the %s key", CONFIG_HMAC_SECRET)); if (null != config && config.containsKey(CONFIG_HMAC_ALGO)) { hmacAlgo = config.get(CONFIG_HMAC_ALGO).toString(); } else { hmacAlgo = DEFAULT_HMAC_ALGO; } if (null != config && config.containsKey(CONFIG_TOKEN_TIMEOUT)) { timeout = ((Number) config.get(CONFIG_TOKEN_TIMEOUT)).longValue(); } else { timeout = DEFAULT_HMAC_TOKEN_TIMEOUT; } super.setup(config); if (null != config & config.containsKey(CONFIG_HMAC_SECRET)) { secret = config.get(CONFIG_HMAC_SECRET).toString().toCharArray(); } else { secret = DEFAULT_HMAC_SECRET; } } @Override public AuthenticatedUser authenticate(final Map<String, String> credentials) throws AuthenticationException { if (credentials.get(PROPERTY_GENERATE_TOKEN) != null) { final AuthenticatedUser user = authenticateUser(credentials); if (user == null) { throw new AuthenticationException(AUTH_ERROR); } credentials.put(PROPERTY_TOKEN, getToken(credentials)); return user; } else if (credentials.get(PROPERTY_TOKEN) != null) { if (validateToken(credentials)) { return new AuthenticatedUser(credentials.get(PROPERTY_USERNAME)); } else { throw new AuthenticationException("Invalid token"); } } else { return authenticateUser(credentials); } } private AuthenticatedUser authenticateUser(final Map<String, String> credentials) throws AuthenticationException { final Vertex v = credentialStore.findUser(credentials.get(PROPERTY_USERNAME)); if (null == v || !BCrypt.checkpw(credentials.get(PROPERTY_PASSWORD), v.value(PROPERTY_PASSWORD))) { throw new AuthenticationException(AUTH_ERROR); } return new AuthenticatedUser(credentials.get(PROPERTY_USERNAME)); } private boolean validateToken(Map<String, String> credentials) { final String token = credentials.get(PROPERTY_TOKEN); final Map<String, String> tokenMap = parseToken(token); final String username = tokenMap.get(PROPERTY_USERNAME); final String time = tokenMap.get("time"); final String password = credentialStore.findUser(username).value(PROPERTY_PASSWORD); final String salt = getBcryptSaltFromStoredPassword(password); final String expected = generateToken(username, salt, time); final Long timeLong = Long.parseLong(time); final Long currentTime = new Date().getTime(); final String base64Token = new String(Base64.getUrlEncoder().encode(token.getBytes())); //Short circuit if the lengths aren't the same or time has expired if (timeLong + timeout < currentTime || expected.length() != base64Token.length()) { return false; } else { //Don't short circuit comparison to prevent timing attacks boolean isValid = true; for (int i = 0; i < expected.length(); i++) { if (base64Token.charAt(i) != expected.charAt(i)) { isValid = false; } } return isValid; } } private Map<String, String> parseToken(final String token) { final String[] parts = token.split(":"); return ImmutableMap.of(PROPERTY_USERNAME, parts[0], "time", parts[1], "hmac", parts[2]); } private String generateToken(final String username, final String salt, final String time) { try { final CharBuffer secretAndSalt = CharBuffer.allocate(secret.length + salt.length() + 1); secretAndSalt.put(secret); secretAndSalt.put(":"); secretAndSalt.put(salt); final String tokenPrefix = username + ":" + time.toString() + ":"; final SecretKeySpec keySpec = new SecretKeySpec(toBytes(secretAndSalt.array()), hmacAlgo); final Mac hmac = Mac.getInstance(hmacAlgo); hmac.init(keySpec); hmac.update(username.getBytes()); hmac.update(time.toString().getBytes()); final Base64.Encoder encoder = Base64.getUrlEncoder(); final byte[] hmacbytes = encoder.encode(hmac.doFinal()); final byte[] tokenbytes = tokenPrefix.getBytes(); final byte[] token = ByteBuffer.wrap(new byte[tokenbytes.length + hmacbytes.length]).put(tokenbytes) .put(hmacbytes).array(); return new String(encoder.encode(token)); } catch (Exception ex) { throw new RuntimeException(ex); } } private String getToken(final Map<String, String> credentials) { final String username = credentials.get(PROPERTY_USERNAME); final Vertex user = credentialStore.findUser(username); final String password = user.value(PROPERTY_PASSWORD); final String salt = getBcryptSaltFromStoredPassword(password); final String time = Long.toString(new Date().getTime()); return generateToken(username, salt, time); } //In BCrypt, the salt is the 22 chars after the 3rd $ private String getBcryptSaltFromStoredPassword(String password) { Integer saltStart = StringUtils.ordinalIndexOf(password, "$", 3); return password.substring(saltStart + 1, saltStart + 23); } private byte[] toBytes(char[] chars) { CharBuffer charBuffer = CharBuffer.wrap(chars); ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer); byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); Arrays.fill(charBuffer.array(), '\u0000'); //Clear sensitive data from memory Arrays.fill(byteBuffer.array(), (byte) 0); //Clear sensitive data from memory return bytes; } }