/*
* 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.Map;
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 org.apache.jackrabbit.api.JackrabbitSession;
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.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.NodeIdFactory;
import org.apache.jackrabbit.core.security.SecurityConstants;
import org.apache.jackrabbit.core.security.user.UserImpl;
import org.apache.jackrabbit.spi.Name;
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.
*/
class CompatTokenProvider {
private static final Logger log = LoggerFactory.getLogger(CompatTokenProvider.class);
private static final String TOKEN_ATTRIBUTE = ".token";
private static final String TOKEN_ATTRIBUTE_EXPIRY = TOKEN_ATTRIBUTE + ".exp";
private static final String TOKEN_ATTRIBUTE_KEY = TOKEN_ATTRIBUTE + ".key";
private static final String TOKENS_NODE_NAME = ".tokens";
private static final String TOKENS_NT_NAME = "nt:unstructured"; // TODO: configurable
private static final char DELIM = '_';
private final SessionImpl session;
private final UserManager userManager;
private final long tokenExpiration;
CompatTokenProvider(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 {
String userPath = null;
Principal pr = user.getPrincipal();
if (pr instanceof ItemBasedPrincipal) {
userPath = ((ItemBasedPrincipal) pr).getPath();
}
TokenCredentials tokenCredentials;
if (userPath != null && session.nodeExists(userPath)) {
Node userNode = session.getNode(userPath);
Node tokenParent;
if (!userNode.hasNode(TOKENS_NODE_NAME)) {
userNode.addNode(TOKENS_NODE_NAME, TOKENS_NT_NAME);
try {
session.save();
} catch (RepositoryException e) {
// may happen when .tokens node is created concurrently
session.refresh(false);
}
}
tokenParent = userNode.getNode(TOKENS_NODE_NAME);
long creationTime = new Date().getTime();
long expirationTime = creationTime + tokenExpiration;
Calendar cal = GregorianCalendar.getInstance();
cal.setTimeInMillis(creationTime);
// generate key part of the login token
String key = generateKey(8);
// create the token node
String tokenName = Text.replace(ISO8601.format(cal), ":", ".");
Node tokenNode;
// avoid usage of sequential nodeIDs
if (System.getProperty(NodeIdFactory.SEQUENTIAL_NODE_ID) == null) {
tokenNode = tokenParent.addNode(tokenName);
} else {
tokenNode = ((NodeImpl) tokenParent).addNodeWithUuid(tokenName, NodeId.randomId().toString());
}
StringBuilder sb = new StringBuilder(tokenNode.getIdentifier());
sb.append(DELIM).append(key);
String token = sb.toString();
tokenCredentials = new TokenCredentials(token);
sc.setAttribute(TOKEN_ATTRIBUTE, token);
// add key property
tokenNode.setProperty(TOKEN_ATTRIBUTE_KEY, getDigestedKey(key));
// add expiration time property
cal.setTimeInMillis(expirationTime);
tokenNode.setProperty(TOKEN_ATTRIBUTE_EXPIRY, session.getValueFactory().createValue(cal));
// add additional attributes passed in by the credentials.
for (String name : sc.getAttributeNames()) {
if (!TOKEN_ATTRIBUTE.equals(name)) {
String value = sc.getAttribute(name).toString();
tokenNode.setProperty(name, value);
tokenCredentials.setAttribute(name, value);
}
}
session.save();
return new CompatModeInfo(token, tokenNode);
} else {
throw new RepositoryException("Cannot create login token: No corresponding node for User " + user.getID() +" in workspace '" + session.getWorkspace().getName() + "'.");
}
}
/**
* 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 CompatModeInfo(token);
}
}
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);
}
public static String getUserId(TokenCredentials tokenCredentials, Session session) throws RepositoryException {
if (!(session instanceof JackrabbitSession)) {
throw new RepositoryException("JackrabbitSession expected");
}
NodeImpl n = (NodeImpl) getTokenNode(tokenCredentials.getToken(), session);
return getUserId(n, ((JackrabbitSession) session).getUserManager());
}
private 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</code> if the specified attribute name doesn't have
* a 'jcr' or 'rep' namespace prefix; <code>true</code> otherwise. This is
* a lazy evaluation in order to avoid testing the defining node type of
* the associated jcr property.
*
* @param propertyName
* @return <code>true</code> if the specified property name doesn't seem
* to represent repository internal information.
*/
private static boolean isInfoAttribute(String propertyName) {
String prefix = Text.getNamespacePrefix(propertyName);
return !Name.NS_JCR_PREFIX.equals(prefix) && !Name.NS_REP_PREFIX.equals(prefix);
}
private static boolean isValidTokenTree(NodeImpl tokenNode) throws RepositoryException {
if (tokenNode == null) {
return false;
} else {
return TOKENS_NODE_NAME.equals(tokenNode.getParent().getName());
}
}
private static String generateKey(int size) {
SecureRandom random = new SecureRandom();
byte key[] = new byte[size];
random.nextBytes(key);
StringBuffer res = new StringBuffer(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 getDigestedKey(TokenCredentials tc) throws RepositoryException {
String tk = tc.getToken();
int pos = tk.indexOf(DELIM);
if (pos > -1) {
return getDigestedKey(tk.substring(pos+1));
}
return null;
}
private static String getDigestedKey(String key) throws RepositoryException {
try {
StringBuilder sb = new StringBuilder();
sb.append("{").append(SecurityConstants.DEFAULT_DIGEST).append("}");
sb.append(Text.digest(SecurityConstants.DEFAULT_DIGEST, key, "UTF-8"));
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RepositoryException("Failed to generate login token.");
} catch (UnsupportedEncodingException e) {
throw new RepositoryException("Failed to generate login token.");
}
}
private final class CompatModeInfo implements TokenInfo {
private final String token;
private final Map<String, String> attributes;
private final Map<String, String> info;
private final long expiry;
private final String key;
private CompatModeInfo(String token) throws RepositoryException {
this(token, getTokenNode(token, session));
}
private CompatModeInfo(String token, Node n) throws RepositoryException {
this.token = token;
long expTime = Long.MAX_VALUE;
String keyV = null;
if (token != null) {
attributes = new HashMap<String, String>();
info = new HashMap<String, String>();
PropertyIterator it = n.getProperties();
while (it.hasNext()) {
Property p = it.nextProperty();
String name = p.getName();
if (TOKEN_ATTRIBUTE_EXPIRY.equals(name)) {
expTime = p.getLong();
} else if (TOKEN_ATTRIBUTE_KEY.equals(name)) {
keyV = p.getString();
} else if (isMandatoryAttribute(name)) {
attributes.put(name, p.getString());
} else if (isInfoAttribute(name)) {
info.put(name, p.getString());
} // else: jcr property -> ignore
}
} else {
attributes = Collections.emptyMap();
info = Collections.emptyMap();
}
expiry = expTime;
key = keyV;
}
public String getToken() {
return token;
}
public boolean isExpired(long loginTime) {
return expiry < loginTime;
}
public boolean remove() {
Session s = null;
try {
s = ((SessionImpl) session).createSession(session.getWorkspace().getName());
Node tokenNode = getTokenNode(token, s);
tokenNode.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 boolean matches(TokenCredentials tokenCredentials) throws RepositoryException {
// test for matching key
if (key != null && !key.equals(getDigestedKey(tokenCredentials))) {
return false;
}
// check if all other required attributes match
for (String name : attributes.keySet()) {
if (!attributes.get(name).equals(tokenCredentials.getAttribute(name))) {
// no match -> login fails.
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 key : info.keySet()) {
if (!attrNames.contains(key)) {
tokenCredentials.setAttribute(key, info.get(key));
}
}
return true;
}
public boolean resetExpiration(long loginTime) throws RepositoryException {
Node tokenNode;
Session s = null;
try {
// expiry...
if (expiry - loginTime <= tokenExpiration/2) {
long expirationTime = loginTime + tokenExpiration;
Calendar cal = GregorianCalendar.getInstance();
cal.setTimeInMillis(expirationTime);
s = ((SessionImpl) session).createSession(session.getWorkspace().getName());
tokenNode = getTokenNode(token, s);
tokenNode.setProperty(TOKEN_ATTRIBUTE_EXPIRY, s.getValueFactory().createValue(cal));
s.save();
return true;
}
} catch (RepositoryException e) {
log.warn("Failed to update expiry or informative attributes of token node.", e);
} finally {
if (s != null) {
s.logout();
}
}
return false;
}
public TokenCredentials getCredentials() {
TokenCredentials tc = new TokenCredentials(token);
for (String name : attributes.keySet()) {
tc.setAttribute(name, attributes.get(name));
}
for (String name : info.keySet()) {
tc.setAttribute(name, info.get(name));
}
return tc;
}
}
}