/* * 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.jackrabbit.core.security.authentication.token; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.SecureRandom; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.jcr.AccessDeniedException; import javax.jcr.NamespaceRegistry; import javax.jcr.Node; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.SimpleCredentials; import javax.jcr.Value; import javax.jcr.ValueFactory; import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; import org.apache.jackrabbit.api.security.user.Authorizable; import org.apache.jackrabbit.api.security.user.User; import org.apache.jackrabbit.api.security.user.UserManager; import org.apache.jackrabbit.core.NodeImpl; import org.apache.jackrabbit.core.ProtectedItemModifier; import org.apache.jackrabbit.core.SessionImpl; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.security.user.PasswordUtility; import org.apache.jackrabbit.core.security.user.UserImpl; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.commons.name.NameConstants; import org.apache.jackrabbit.util.ISO8601; import org.apache.jackrabbit.util.Text; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Backport of the TokenProvider implementation present with OAK adjusted to * match some subtle differences in jackrabbit token login. */ public class TokenProvider extends ProtectedItemModifier { private static final Logger log = LoggerFactory.getLogger(TokenProvider.class); private static final String TOKEN_ATTRIBUTE = ".token"; private static final String TOKEN_ATTRIBUTE_EXPIRY = "rep:token.exp"; private static final String TOKEN_ATTRIBUTE_KEY = "rep:token.key"; private static final String TOKENS_NODE_NAME = ".tokens"; private static final String TOKEN_NT_NAME = "rep:Token"; private static final Name TOKENS_NT_NAME = NameConstants.NT_UNSTRUCTURED; private static final char DELIM = '_'; private static final Set<String> RESERVED_ATTRIBUTES = new HashSet(3); static { RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE); RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE_EXPIRY); RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE_KEY); } private static final Collection<String> RESERVED_PREFIXES = Collections.unmodifiableList(Arrays.asList( NamespaceRegistry.PREFIX_XML, NamespaceRegistry.PREFIX_JCR, NamespaceRegistry.PREFIX_NT, NamespaceRegistry.PREFIX_MIX, Name.NS_XMLNS_PREFIX, Name.NS_REP_PREFIX, Name.NS_SV_PREFIX )); private final SessionImpl session; private final UserManager userManager; private final long tokenExpiration; TokenProvider(SessionImpl session, long tokenExpiration) throws RepositoryException { this.session = session; this.userManager = session.getUserManager(); this.tokenExpiration = tokenExpiration; } /** * Create a separate token node underneath a dedicated token store within * the user home node. That token node contains the hashed token, the * expiration time and additional mandatory attributes that will be verified * during login. * * @param user * @param sc The current simple credentials. * @return A new {@code TokenInfo} or {@code null} if the token could not * be created. */ public TokenInfo createToken(User user, SimpleCredentials sc) throws RepositoryException { TokenInfo tokenInfo = null; if (sc != null && user != null && user.getID().equalsIgnoreCase(sc.getUserID())) { String[] attrNames = sc.getAttributeNames(); Map<String, String> attributes = new HashMap<String, String>(attrNames.length); for (String attrName : sc.getAttributeNames()) { attributes.put(attrName, sc.getAttribute(attrName).toString()); } tokenInfo = createToken(user, attributes); if (tokenInfo != null) { // also set the new token to the simple credentials. sc.setAttribute(TOKEN_ATTRIBUTE, tokenInfo.getToken()); } } return tokenInfo; } /** * Create a separate token node underneath a dedicated token store within * the user home node. That token node contains the hashed token, the * expiration time and additional mandatory attributes that will be verified * during login. * * @param userId The identifier of the user for which a new token should * be created. * @param attributes The attributes associated with the new token. * @return A new {@code TokenInfo} or {@code null} if the token could not * be created. */ private TokenInfo createToken(User user, Map<String, ?> attributes) throws RepositoryException { String error = "Failed to create login token. "; NodeImpl tokenParent = getTokenParent(user); if (tokenParent != null) { try { ValueFactory vf = session.getValueFactory(); long creationTime = new Date().getTime(); Calendar creation = GregorianCalendar.getInstance(); creation.setTimeInMillis(creationTime); Name tokenName = session.getQName(Text.replace(ISO8601.format(creation), ":", ".")); NodeImpl tokenNode = super.addNode(tokenParent, tokenName, session.getQName(TOKEN_NT_NAME), NodeId.randomId()); String key = generateKey(8); String token = new StringBuilder(tokenNode.getId().toString()).append(DELIM).append(key).toString(); String keyHash = PasswordUtility.buildPasswordHash(getKeyValue(key, user.getID())); setProperty(tokenNode, session.getQName(TOKEN_ATTRIBUTE_KEY), vf.createValue(keyHash)); setProperty(tokenNode, session.getQName(TOKEN_ATTRIBUTE_EXPIRY), createExpirationValue(creationTime, session)); for (String name : attributes.keySet()) { if (!RESERVED_ATTRIBUTES.contains(name)) { String attr = attributes.get(name).toString(); setProperty(tokenNode, session.getQName(name), vf.createValue(attr)); } } session.save(); return new TokenInfoImpl(tokenNode, token, user.getID()); } catch (NoSuchAlgorithmException e) { // error while generating login token log.error(error, e); } catch (UnsupportedEncodingException e) { // error while generating login token log.error(error, e); } catch (AccessDeniedException e) { log.warn(error, e); } } else { log.warn("Unable to get/create token store for user {}", user.getID()); } return null; } private Value createExpirationValue(long creationTime, Session session) throws RepositoryException { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(createExpirationTime(creationTime, tokenExpiration)); return session.getValueFactory().createValue(cal); } /** * Retrieves the token information associated with the specified login * token. If no accessible {@code Tree} exists for the given token or if * the token is not associated with a valid user this method returns {@code null}. * * @param token A valid login token. * @return The {@code TokenInfo} associated with the specified token or * {@code null} of the corresponding information does not exist or is not * associated with a valid user. */ public TokenInfo getTokenInfo(String token) throws RepositoryException { if (token == null) { return null; } NodeImpl tokenNode = (NodeImpl) getTokenNode(token, session); String userId = getUserId(tokenNode, userManager); if (userId == null || !isValidTokenTree(tokenNode)) { return null; } else { return new TokenInfoImpl(tokenNode, token, userId); } } static Node getTokenNode(String token, Session session) throws RepositoryException { int pos = token.indexOf(DELIM); String id = (pos == -1) ? token : token.substring(0, pos); return session.getNodeByIdentifier(id); } static String getUserId(NodeImpl tokenNode, UserManager userManager) throws RepositoryException { if (tokenNode != null) { final NodeImpl userNode = (NodeImpl) tokenNode.getParent().getParent(); final String principalName = userNode.getProperty(UserImpl.P_PRINCIPAL_NAME).getString(); if (userNode.isNodeType(UserImpl.NT_REP_USER)) { Authorizable a = userManager.getAuthorizable(new ItemBasedPrincipal() { public String getPath() throws RepositoryException { return userNode.getPath(); } public String getName() { return principalName; } }); if (a != null && !a.isGroup() && !((User)a).isDisabled()) { return a.getID(); } } else { throw new RepositoryException("Failed to calculate userId from token credentials"); } } return null; } /** * Returns {@code true} if the specified {@code attributeName} * starts with or equals {@link #TOKEN_ATTRIBUTE}. * * @param attributeName The attribute name. * @return {@code true} if the specified {@code attributeName} * starts with or equals {@link #TOKEN_ATTRIBUTE}. */ static boolean isMandatoryAttribute(String attributeName) { return attributeName != null && attributeName.startsWith(TOKEN_ATTRIBUTE); } /** * Returns {@code false} if the specified attribute name doesn't have * a 'jcr' or 'rep' namespace prefix; {@code true} otherwise. This is * a lazy evaluation in order to avoid testing the defining node type of * the associated jcr property. * * @param attributeName The attribute name. * @return {@code true} if the specified property name doesn't seem * to represent repository internal information. */ static boolean isInfoAttribute(String attributeName) { String prefix = Text.getNamespacePrefix(attributeName); return !RESERVED_PREFIXES.contains(prefix); } private static long createExpirationTime(long creationTime, long tokenExpiration) { return creationTime + tokenExpiration; } private static long getExpirationTime(NodeImpl tokenNode, long defaultValue) throws RepositoryException { if (tokenNode.hasProperty(TOKEN_ATTRIBUTE_EXPIRY)) { return tokenNode.getProperty(TOKEN_ATTRIBUTE_EXPIRY).getLong(); } else { return defaultValue; } } private static String generateKey(int size) { SecureRandom random = new SecureRandom(); byte key[] = new byte[size]; random.nextBytes(key); StringBuilder res = new StringBuilder(key.length * 2); for (byte b : key) { res.append(Text.hexTable[(b >> 4) & 15]); res.append(Text.hexTable[b & 15]); } return res.toString(); } private static String getKeyValue(String key, String userId) { return key + userId; } private static boolean isValidTokenTree(NodeImpl tokenNode) throws RepositoryException { if (tokenNode == null) { return false; } else { return TOKENS_NODE_NAME.equals(tokenNode.getParent().getName()) && TOKEN_NT_NAME.equals(tokenNode.getPrimaryNodeType().getName()); } } private NodeImpl getTokenParent(User user) throws RepositoryException { NodeImpl tokenParent = null; String parentPath = null; try { if (user != null) { Principal pr = user.getPrincipal(); if (pr instanceof ItemBasedPrincipal) { String userPath = ((ItemBasedPrincipal) pr).getPath(); NodeImpl userNode = (NodeImpl) session.getNode(userPath); if (userNode.hasNode(TOKENS_NODE_NAME)) { tokenParent = (NodeImpl) userNode.getNode(TOKENS_NODE_NAME); } else { tokenParent = userNode.addNode(session.getQName(TOKENS_NODE_NAME), TOKENS_NT_NAME, NodeId.randomId()); parentPath = userPath + '/' + TOKENS_NODE_NAME; session.save(); } } } else { log.debug("Cannot create login token: No user specified. (null)"); } } catch (RepositoryException e) { // conflict while creating token store for this user -> refresh and // try to get the tree from the updated root. log.debug("Conflict while creating token store -> retrying", e); session.refresh(false); if (parentPath != null && session.nodeExists(parentPath)) { tokenParent = (NodeImpl) session.getNode(parentPath); } } return tokenParent; } private class TokenInfoImpl implements TokenInfo { private final String token; private final String tokenPath; private final String userId; private final long expirationTime; private final String key; private final Map<String, String> mandatoryAttributes; private final Map<String, String> publicAttributes; private TokenInfoImpl(NodeImpl tokenNode, String token, String userId) throws RepositoryException { this.token = token; this.tokenPath = tokenNode.getPath(); this.userId = userId; expirationTime = getExpirationTime(tokenNode, Long.MIN_VALUE); key = tokenNode.getProperty(TOKEN_ATTRIBUTE_KEY).getString(); mandatoryAttributes = new HashMap<String, String>(); publicAttributes = new HashMap<String, String>(); PropertyIterator pit = tokenNode.getProperties(); while (pit.hasNext()) { Property property = pit.nextProperty(); String name = property.getName(); String value = property.getString(); if (RESERVED_ATTRIBUTES.contains(name)) { continue; } if (isMandatoryAttribute(name)) { mandatoryAttributes.put(name, value); } else if (isInfoAttribute(name)) { // info attribute publicAttributes.put(name, value); } // else: jcr specific property } } public String getToken() { return token; } public boolean isExpired(long loginTime) { return expirationTime < loginTime; } public boolean resetExpiration(long loginTime) throws RepositoryException { if (isExpired(loginTime)) { log.debug("Attempt to reset an expired token."); return false; } Session s = null; try { if (expirationTime - loginTime <= tokenExpiration / 2) { s = session.createSession(session.getWorkspace().getName()); setProperty((NodeImpl) s.getNode(tokenPath), session.getQName(TOKEN_ATTRIBUTE_EXPIRY), createExpirationValue(loginTime, session)); s.save(); log.debug("Successfully reset token expiration time."); return true; } } catch (RepositoryException e) { log.warn("Error while resetting token expiration", e); } finally { if (s != null) { s.logout(); } } return false; } public boolean matches(TokenCredentials tokenCredentials) { String tk = tokenCredentials.getToken(); int pos = tk.lastIndexOf(DELIM); if (pos > -1) { tk = tk.substring(pos + 1); } if (key == null || !PasswordUtility.isSame(key, getKeyValue(tk, userId))) { return false; } for (String name : mandatoryAttributes.keySet()) { String expectedValue = mandatoryAttributes.get(name); if (!expectedValue.equals(tokenCredentials.getAttribute(name))) { return false; } } // update set of informative attributes on the credentials // based on the properties present on the token node. Collection<String> attrNames = Arrays.asList(tokenCredentials.getAttributeNames()); for (String name : publicAttributes.keySet()) { if (!attrNames.contains(name)) { tokenCredentials.setAttribute(name, publicAttributes.get(name).toString()); } } return true; } public boolean remove() { Session s = null; try { s = session.createSession(session.getWorkspace().getName()); Node node = s.getNode(tokenPath); node.remove(); s.save(); return true; } catch (RepositoryException e) { log.warn("Internal error while removing token node.", e); } finally { if (s != null) { s.logout(); } } return false; } public TokenCredentials getCredentials() { TokenCredentials tc = new TokenCredentials(token); for (String name : mandatoryAttributes.keySet()) { tc.setAttribute(name, mandatoryAttributes.get(name)); } for (String name : publicAttributes.keySet()) { tc.setAttribute(name, publicAttributes.get(name)); } return tc; } } }