You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by an...@apache.org on 2012/07/24 15:49:22 UTC

svn commit: r1365057 - /jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/

Author: angela
Date: Tue Jul 24 13:49:22 2012
New Revision: 1365057

URL: http://svn.apache.org/viewvc?rev=1365057&view=rev
Log:
OAK-91 : Implement Authentication Support  (work in progress)

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfo.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProvider.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderCallback.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java?rev=1365057&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenAuthentication.java Tue Jul 24 13:49:22 2012
@@ -0,0 +1,95 @@
+/*
+ * 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.oak.security.authentication.token;
+
+import java.security.Principal;
+import java.util.Date;
+import java.util.Set;
+import javax.jcr.Credentials;
+
+import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials;
+import org.apache.jackrabbit.oak.security.authentication.Authentication;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TokenAuthentication... TODO
+ */
+class TokenAuthentication implements Authentication {
+
+    private static final Logger log = LoggerFactory.getLogger(TokenAuthentication.class);
+
+    private final TokenProvider tokenProvider;
+    private TokenInfo tokenInfo;
+
+    TokenAuthentication(TokenProvider tokenProvider) {
+        this.tokenProvider = tokenProvider;
+    }
+
+    @Override
+    public boolean authenticate(Credentials credentials) {
+        boolean success = false;
+        if (credentials instanceof TokenCredentials) {
+            TokenCredentials tc = (TokenCredentials) credentials;
+            success = validateCredentials(tc);
+        }
+        return success;
+    }
+
+    /**
+     * Always returns {@code false}
+     */
+    @Override
+    public boolean impersonate(Set<Principal> principals) {
+        return false;
+    }
+
+    TokenInfo getTokenInfo() {
+        return tokenInfo;
+    }
+
+    //--------------------------------------------------------------------------
+    private boolean validateCredentials(TokenCredentials tokenCredentials) {
+        // credentials without userID -> check if attributes provide
+        // sufficient information for successful authentication.
+        String token = tokenCredentials.getToken();
+
+        tokenInfo = tokenProvider.getTokenInfo(token);
+        if (tokenInfo == null) {
+            log.debug("Invalid token credentials");
+            return false;
+        }
+
+        long loginTime = new Date().getTime();
+        if (tokenInfo.isExpired(loginTime)) {
+            // token is expired
+            log.debug("Token is expired");
+            tokenProvider.removeToken(tokenInfo);
+            return false;
+        }
+
+        if (tokenInfo.matches(tokenCredentials)) {
+            if (!tokenProvider.resetTokenExpiration(tokenInfo, loginTime)) {
+                log.debug("Unable to reset token expiration... trying next time");
+            }
+            return true;
+        }
+
+
+        return false;
+    }
+}

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfo.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfo.java?rev=1365057&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfo.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenInfo.java Tue Jul 24 13:49:22 2012
@@ -0,0 +1,37 @@
+/*
+ * 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.oak.security.authentication.token;
+
+import java.util.Map;
+
+import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials;
+
+/**
+ * TokenInfo... TODO document, move to spi/api
+ */
+public interface TokenInfo {
+
+    String getToken();
+
+    boolean isExpired(long loginTime);
+
+    boolean matches(TokenCredentials tokenCredentials);
+
+    Map<String, String> getPrivateAttributes();
+
+    Map<String, String> getPublicAttributes();
+}

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java?rev=1365057&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenLoginModule.java Tue Jul 24 13:49:22 2012
@@ -0,0 +1,154 @@
+/*
+ * 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.oak.security.authentication.token;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.jcr.Credentials;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+
+import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials;
+import org.apache.jackrabbit.oak.api.AuthInfo;
+import org.apache.jackrabbit.oak.security.authentication.AuthInfoImpl;
+import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TokenLoginModule... TODO
+ */
+public class TokenLoginModule extends AbstractLoginModule {
+
+    /**
+     * logger instance
+     */
+    private static final Logger log = LoggerFactory.getLogger(TokenLoginModule.class);
+
+    private TokenProvider tokenProvider;
+
+    private TokenCredentials tokenCredentials;
+    private TokenInfo tokenInfo;
+    private String userID;
+    private Set<Principal> principals;
+
+    //--------------------------------------------------------< LoginModule >---
+
+    @Override
+    public boolean login() throws LoginException {
+        tokenProvider = getTokenProvider();
+        if (tokenProvider == null) {
+            return false;
+        }
+
+        Credentials credentials = getCredentials();
+        if (credentials instanceof TokenCredentials) {
+            TokenCredentials tc = (TokenCredentials) credentials;
+            TokenAuthentication authentication = new TokenAuthentication(tokenProvider);
+            if (authentication.authenticate(tc)) {
+                tokenCredentials = tc;
+                tokenInfo = authentication.getTokenInfo();
+                userID = null; // TODO: getUserID(tc);
+                principals = null; // TODO getPrincipals(userID);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean commit() throws LoginException {
+        if (tokenCredentials != null || !principals.isEmpty()) {
+            if (!subject.isReadOnly()) {
+                subject.getPublicCredentials().add(tokenCredentials);
+                subject.getPrincipals().addAll(principals);
+                subject.getPublicCredentials().add(getAuthInfo(tokenInfo));
+            }
+            return true;
+        }
+
+        if (tokenProvider != null && sharedState.containsKey(SHARED_KEY_CREDENTIALS)) {
+            Credentials shared = getSharedCredentials();
+            if (shared != null) {
+                if (tokenProvider.doCreateToken(shared)) {
+                    TokenInfo ti = tokenProvider.createToken(shared);
+                    if (ti != null) {
+                        TokenCredentials tc = new TokenCredentials(ti.getToken());
+                        Map<String, String> attributes = ti.getPrivateAttributes();
+                        for (String name : attributes.keySet()) {
+                            tc.setAttribute(name, attributes.get(name));
+                        }
+                        attributes = ti.getPublicAttributes();
+                        for (String name : attributes.keySet()) {
+                            tc.setAttribute(name, attributes.get(name));
+                        }
+                        subject.getPublicCredentials().add(tc);
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean abort() throws LoginException {
+        tokenCredentials = null;
+        principals = null;
+        return true;
+    }
+
+    //------------------------------------------------< AbstractLoginModule >---
+    @Override
+    protected Set<Class> getSupportedCredentials() {
+        return Collections.<Class>singleton(TokenCredentials.class);
+    }
+
+    //--------------------------------------------------------------------------
+    private TokenProvider getTokenProvider() {
+        TokenProvider provider = null;
+        if (callbackHandler != null) {
+            try {
+                TokenProviderCallback tcCallback = new TokenProviderCallback();
+                callbackHandler.handle(new Callback[] {tcCallback});
+                provider = tcCallback.getTokenProvider();
+            } catch (IOException e) {
+                log.warn(e.getMessage());
+            } catch (UnsupportedCallbackException e) {
+                log.warn(e.getMessage());
+            }
+        }
+        return provider;
+    }
+
+    private AuthInfo getAuthInfo(TokenInfo tokenInfo) {
+        Map<String, Object> attributes = new HashMap<String, Object>();
+        if (tokenProvider != null && tokenInfo != null) {
+            Map<String, String> publicAttributes = tokenInfo.getPublicAttributes();
+            for (String attrName : publicAttributes.keySet()) {
+                attributes.put(attrName, publicAttributes.get(attrName));
+            }
+        }
+        return new AuthInfoImpl(userID, attributes, principals);
+    }
+}

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProvider.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProvider.java?rev=1365057&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProvider.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProvider.java Tue Jul 24 13:49:22 2012
@@ -0,0 +1,40 @@
+/*
+ * 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.oak.security.authentication.token;
+
+import javax.jcr.Credentials;
+
+/**
+ * TokenProvider... TODO document, move to spi/api
+ */
+public interface TokenProvider {
+
+    /**
+     * Default expiration time in ms for login tokens is 2 hours.
+     */
+    long TOKEN_EXPIRATION = 2 * 3600 * 1000;
+
+    boolean doCreateToken(Credentials credentials);
+
+    TokenInfo createToken(Credentials credentials);
+
+    TokenInfo getTokenInfo(String token);
+
+    boolean removeToken(TokenInfo tokenInfo);
+
+    boolean resetTokenExpiration(TokenInfo tokenInfo, long loginTime);
+}

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderCallback.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderCallback.java?rev=1365057&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderCallback.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderCallback.java Tue Jul 24 13:49:22 2012
@@ -0,0 +1,50 @@
+/*
+ * 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.oak.security.authentication.token;
+
+import javax.security.auth.callback.Callback;
+
+/**
+ * TokenContextCallback... TODO
+ */
+public class TokenProviderCallback implements Callback {
+
+    private TokenProvider tokenProvider;
+
+    /**
+     * Returns the principal provider as set using
+     * {@link #setTokenProvider(TokenProvider)}
+     * or {@code null}.
+     *
+     * @return an instance of {@code PrincipalProvider} or {@code null} if no
+     * provider has been set before.
+     */
+    public TokenProvider getTokenProvider() {
+        return tokenProvider;
+    }
+
+    /**
+     * Sets the {@code TokenProvider} that is being used during the
+     * authentication process.
+     *
+     * @param tokenProvider The {@code TokenProvider} to use during the
+     * authentication process.
+     */
+    public void setTokenProvider(TokenProvider tokenProvider) {
+        this.tokenProvider = tokenProvider;
+    }
+}

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java?rev=1365057&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java Tue Jul 24 13:49:22 2012
@@ -0,0 +1,369 @@
+/*
+ * 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.oak.security.authentication.token;
+
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+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.Iterator;
+import java.util.Map;
+import java.util.UUID;
+import javax.jcr.Credentials;
+import javax.jcr.PropertyType;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.query.Query;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.CoreValue;
+import org.apache.jackrabbit.oak.api.CoreValueFactory;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.QueryEngine;
+import org.apache.jackrabbit.oak.api.Result;
+import org.apache.jackrabbit.oak.api.ResultRow;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.core.DefaultConflictHandler;
+import org.apache.jackrabbit.oak.spi.security.user.PasswordUtility;
+import org.apache.jackrabbit.util.ISO8601;
+import org.apache.jackrabbit.util.Text;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TokenProvider... TODO
+ */
+public class TokenProviderImpl implements TokenProvider {
+
+    /**
+     * logger instance
+     */
+    private static final Logger log = LoggerFactory.getLogger(TokenProviderImpl.class);
+
+    /**
+     * Constant for the token attribute passed with simple credentials to
+     * trigger the generation of a new token.
+     */
+    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 = JcrConstants.NT_UNSTRUCTURED;
+
+    private static final int STATUS_VALID = 0;
+    private static final int STATUS_EXPIRED = 1;
+    private static final int STATUS_MISMATCH = 2;
+
+    private static final char DELIM = '_';
+
+    private final ContentSession contentSession;
+    private final long tokenExpiration;
+
+    public TokenProviderImpl(ContentSession contentSession, long tokenExpiration) {
+        this.contentSession = contentSession;
+        this.tokenExpiration = tokenExpiration;
+    }
+
+    //------------------------------------------------------< TokenProvider >---
+    @Override
+    public boolean doCreateToken(Credentials credentials) {
+        if (credentials instanceof SimpleCredentials) {
+            SimpleCredentials sc = (SimpleCredentials) credentials;
+            Object attr = sc.getAttribute(TOKEN_ATTRIBUTE);
+            return (attr != null && "".equals(attr.toString()));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public TokenInfo createToken(Credentials credentials) {
+        if (credentials instanceof SimpleCredentials) {
+            final SimpleCredentials sc = (SimpleCredentials) credentials;
+            String userID = sc.getUserID();
+            String userPath = getUserPath(contentSession, userID);
+
+            Root root = contentSession.getCurrentRoot();
+            Tree userTree = (userPath == null) ? null : root.getTree(userPath);
+            if (userTree != null) {
+                try {
+                    Tree tokenParent = userTree.getChild(TOKENS_NODE_NAME);
+                    if (tokenParent == null) {
+                        tokenParent = userTree.addChild(TOKENS_NODE_NAME);
+                        CoreValue primaryType = contentSession.getCoreValueFactory().createValue(TOKENS_NT_NAME);
+                        tokenParent.setProperty(JcrConstants.JCR_PRIMARYTYPE, primaryType);
+                    }
+
+                    long creationTime = new Date().getTime();
+                    Calendar creation = GregorianCalendar.getInstance();
+                    creation.setTimeInMillis(creationTime);
+                    String tokenName = Text.replace(ISO8601.format(creation), ":", ".");
+                    Tree tokenTree = tokenParent.addChild(tokenName);
+
+                    String key = generateKey(8);
+                    String token = new StringBuilder(tokenTree.getPath()).append(DELIM).append(key).toString();
+
+                    CoreValueFactory vf = contentSession.getCoreValueFactory();
+                    tokenTree.setProperty(TOKEN_ATTRIBUTE_KEY, vf.createValue(PasswordUtility.buildPasswordHash(key)));
+                    final long expirationTime = creationTime + tokenExpiration;
+                    tokenTree.setProperty(TOKEN_ATTRIBUTE_EXPIRY, getExpirationValue(expirationTime));
+
+                    Map<String, String> attributes;
+                    for (String name : sc.getAttributeNames()) {
+                        if (!TOKEN_ATTRIBUTE.equals(name)) {
+                            String attr = sc.getAttribute(name).toString();
+                            tokenTree.setProperty(name, vf.createValue(attr));
+                        }
+                    }
+                    root.commit(DefaultConflictHandler.OURS);
+
+                    // also set the new token to the simple credentials.
+                    sc.setAttribute(TOKEN_ATTRIBUTE, token);
+                    return new TokenInfoImpl(tokenTree, token);
+
+                } catch (NoSuchAlgorithmException e) {
+                    log.debug("Failed to create login token ", e.getMessage());
+                } catch (UnsupportedEncodingException e) {
+                    log.debug("Failed to create login token ", e.getMessage());
+                } catch (CommitFailedException e) {
+                    log.debug("Failed to create login token ", e.getMessage());
+                }
+            } else {
+                log.debug("Cannot create login token: No corresponding node for User " + userID + '.');
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public TokenInfo getTokenInfo(String token) {
+        Root root = contentSession.getCurrentRoot();
+        int pos = token.indexOf(DELIM);
+        String tokenPath = (pos == -1) ? token : token.substring(0, pos);
+        Tree tokenTree = root.getTree(tokenPath);
+        return (tokenTree == null) ? null : new TokenInfoImpl(tokenTree, token);
+    }
+
+    @Override
+    public boolean removeToken(TokenInfo tokenInfo) {
+        Tree tokenTree = getTokenTree(tokenInfo);
+        if (tokenTree != null) {
+            try {
+                if (tokenTree.remove()) {
+                    contentSession.getCurrentRoot().commit(DefaultConflictHandler.OURS);
+                    return true;
+                }
+            } catch (CommitFailedException e) {
+                log.debug("Error while removing expired token", e.getMessage());
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean resetTokenExpiration(TokenInfo tokenInfo, long loginTime) {
+        Tree tokenTree = getTokenTree(tokenInfo);
+        if (tokenTree != null) {
+            long expTime = tokenTree.getProperty(TOKEN_ATTRIBUTE_EXPIRY).getValue().getLong();
+            if (expTime - loginTime <= tokenExpiration/2) {
+                long expirationTime = loginTime + tokenExpiration;
+                try {
+                    tokenTree.setProperty(TOKEN_ATTRIBUTE_EXPIRY, getExpirationValue(expirationTime));
+                    contentSession.getCurrentRoot().commit(DefaultConflictHandler.OURS);
+                    return true;
+                } catch (CommitFailedException e) {
+                    log.warn("Error while resetting token expiration", e.getMessage());
+                }
+            }
+        }
+        return false;
+    }
+
+
+    //--------------------------------------------------------------------------
+
+    private CoreValue getExpirationValue(long expirationTime) {
+        Calendar cal = GregorianCalendar.getInstance();
+        cal.setTimeInMillis(expirationTime);
+        return contentSession.getCoreValueFactory().createValue(ISO8601.format(cal), PropertyType.DATE);
+    }
+
+    /**
+     * Returns {@code true} if the specified {@code attributeName}
+     * starts with or equals {@link #TOKEN_ATTRIBUTE}.
+     *
+     * @param attributeName
+     * @return {@code true} if the specified {@code attributeName}
+     * starts with or equals {@link #TOKEN_ATTRIBUTE}.
+     */
+    private static boolean isMandatoryAttribute(String attributeName) {
+        return attributeName != null && attributeName.startsWith(TOKEN_ATTRIBUTE);
+    }
+
+    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 Tree getTokenTree(TokenInfo tokenInfo) {
+        if (tokenInfo instanceof TokenInfoImpl) {
+            return contentSession.getCurrentRoot().getTree(((TokenInfoImpl) tokenInfo).tokenPath);
+        } else {
+            return null;
+        }
+    }
+
+    // TODO: move to user related oak-spi that is used both by JCR usermanagement
+    // and oak-level functionality.
+    private static String getUserPath(ContentSession contentSession, String userID) {
+        QueryEngine qe = contentSession.getQueryEngine();
+        try {
+            String uuid = UUID.nameUUIDFromBytes(userID.toLowerCase().getBytes("UTF-8")).toString();
+            Map<String, CoreValue> bindings = Collections.singletonMap("id", contentSession.getCoreValueFactory().createValue(uuid));
+            String statement = "SELECT * FROM [rep:User] WHERE [jcr:uuid] = $id";
+            Result result = contentSession.getQueryEngine().executeQuery(statement, Query.JCR_SQL2, contentSession, Long.MAX_VALUE, 0, bindings, null);
+            Iterator<? extends ResultRow> it = result.getRows().iterator();
+            if (it.hasNext()) {
+                return it.next().getPath();
+            }
+        } catch (Exception e) {
+            // no such user.
+        }
+        return null;
+    }
+
+    //--------------------------------------------------------------------------
+
+    private static class TokenInfoImpl implements TokenInfo {
+
+        private final String token;
+        private final String tokenPath;
+
+        private final long expirationTime;
+        private final String key;
+        private Map<String, String> mandatoryAttributes;
+        private Map<String, String> publicAttributes;
+
+
+        private TokenInfoImpl(Tree tokenTree, String token) {
+            this.token = token;
+            this.tokenPath = tokenTree.getPath();
+
+            PropertyState expTime = tokenTree.getProperty(TOKEN_ATTRIBUTE_EXPIRY);
+            if (expTime == null) {
+                expirationTime = Long.MIN_VALUE;
+            } else {
+                expirationTime = expTime.getValue().getLong();
+            }
+
+            PropertyState keyProp = tokenTree.getProperty(TOKEN_ATTRIBUTE_KEY);
+            key = (keyProp == null) ? null : keyProp.getValue().getString();
+
+            mandatoryAttributes = new HashMap<String, String>();
+            publicAttributes = new HashMap<String, String>();
+            for (PropertyState propertyState : tokenTree.getProperties()) {
+                String name = propertyState.getName();
+                String value = propertyState.getValue().getString();
+                if (isMandatoryAttribute(name)) {
+                    mandatoryAttributes.put(name, value);
+                } else if (isInfoAttribute(name)) {
+                    // info attribute
+                    publicAttributes.put(name, value);
+                } // else: jcr specific property
+            }
+        }
+
+        @Override
+        public String getToken() {
+            return token;
+        }
+
+        @Override
+        public boolean isExpired(long loginTime) {
+            return expirationTime < loginTime;
+        }
+
+        @Override
+        public boolean matches(TokenCredentials tokenCredentials) {
+            if (key == null || !PasswordUtility.isSame(key, tokenCredentials.getToken())) {
+                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;
+        }
+
+        @Override
+        public Map<String, String> getPrivateAttributes() {
+            return Collections.unmodifiableMap(mandatoryAttributes);
+        }
+
+        @Override
+        public Map<String, String> getPublicAttributes() {
+            return Collections.unmodifiableMap(publicAttributes);
+        }
+
+        /**
+         * 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 propertyName
+         * @return {@code true} 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 !"jcr".equals(prefix) && !"rep".equals(prefix);
+        }
+    }
+}