diff --git a/scm-core/src/main/java/sonia/scm/security/BearerAuthenticationToken.java b/scm-core/src/main/java/sonia/scm/security/BearerAuthenticationToken.java
new file mode 100644
index 0000000000..0a995bf304
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/BearerAuthenticationToken.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer. 2. Redistributions in
+ * binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other
+ * materials provided with the distribution. 3. Neither the name of SCM-Manager;
+ * nor the names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import org.apache.shiro.authc.AuthenticationToken;
+
+/**
+ * Token used for authentication with bearer tokens.
+ *
+ * @author Sebastian Sdorra
+ * @since 2.0.0
+ */
+public class BearerAuthenticationToken implements AuthenticationToken
+{
+
+ /** Field description */
+ private static final long serialVersionUID = -5005335710978534182L;
+
+ //~--- constructors ---------------------------------------------------------
+
+ /**
+ * Constructs a new BearerAuthenticationToken
+ *
+ *
+ * @param token bearer token
+ */
+ public BearerAuthenticationToken(String token)
+ {
+ this.token = token;
+ }
+
+ //~--- get methods ----------------------------------------------------------
+
+ /**
+ * Returns the token.
+ *
+ *
+ * @return token
+ */
+ @Override
+ public String getCredentials()
+ {
+ return token;
+ }
+
+ /**
+ * Returns the username or null.
+ *
+ *
+ * @return username or null
+ */
+ @Override
+ public String getPrincipal()
+ {
+ return null;
+ }
+
+ //~--- set methods ----------------------------------------------------------
+
+ /**
+ * Sets the username.
+ *
+ *
+ * @param username username
+ */
+ public void setUsername(String username)
+ {
+ this.username = username;
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** bearer token */
+ private final String token;
+
+ /** username */
+ private String username;
+}
diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java
index 1d457d303e..4b08856157 100644
--- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java
+++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java
@@ -111,6 +111,21 @@ public final class DAORealmHelper
UsernamePasswordToken upt = (UsernamePasswordToken) token;
String principal = upt.getUsername();
+ return getAuthenticationInfo(principal, null);
+ }
+
+ /**
+ * Method description
+ *
+ *
+ * @param principal
+ * @param credentials
+ *
+ * @return
+ */
+ public AuthenticationInfo getAuthenticationInfo(String principal,
+ String credentials)
+ {
checkArgument(!Strings.isNullOrEmpty(principal), "username is required");
logger.debug("try to authenticate {}", principal);
@@ -141,7 +156,14 @@ public final class DAORealmHelper
collection.add(user, realm);
collection.add(collectGroups(principal), realm);
- return new SimpleAuthenticationInfo(collection, user.getPassword());
+ String creds = credentials;
+
+ if (credentials == null)
+ {
+ creds = user.getPassword();
+ }
+
+ return new SimpleAuthenticationInfo(collection, creds);
}
//~--- methods --------------------------------------------------------------
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index 1f3c6205cc..08d92af60f 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -69,6 +69,12 @@
shiro-guice
${shiro.version}
+
+
+ io.jsonwebtoken
+ jjwt
+ 0.4
+
diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java
new file mode 100644
index 0000000000..7dc6c6b03b
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer. 2. Redistributions in
+ * binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other
+ * materials provided with the distribution. 3. Neither the name of SCM-Manager;
+ * nor the names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
+import org.apache.shiro.realm.AuthenticatingRealm;
+
+import sonia.scm.group.GroupDAO;
+import sonia.scm.plugin.Extension;
+import sonia.scm.user.UserDAO;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Realm for authentication with {@link BearerAuthenticationToken}.
+ *
+ * @author Sebastian Sdorra
+ * @since 2.0.0
+ */
+@Singleton
+@Extension
+public class BearerRealm extends AuthenticatingRealm
+{
+
+ /** realm name */
+ @VisibleForTesting
+ static final String REALM = "BearerRealm";
+
+ //~--- constructors ---------------------------------------------------------
+
+ /**
+ * Constructs ...
+ *
+ *
+ * @param resolver key resolver
+ * @param userDAO user dao
+ * @param groupDAO group dao
+ */
+ @Inject
+ public BearerRealm(SecureKeyResolver resolver, UserDAO userDAO,
+ GroupDAO groupDAO)
+ {
+ this.resolver = resolver;
+ this.helper = new DAORealmHelper(REALM, userDAO, groupDAO);
+ setCredentialsMatcher(new AllowAllCredentialsMatcher());
+ setAuthenticationTokenClass(BearerAuthenticationToken.class);
+ }
+
+ //~--- methods --------------------------------------------------------------
+
+ /**
+ * Validates the given jwt token and retrieves authentication data from
+ * {@link UserDAO} and {@link GroupDAO}.
+ *
+ *
+ * @param token jwt token
+ *
+ * @return authentication data from user and group dao
+ */
+ @Override
+ protected AuthenticationInfo doGetAuthenticationInfo(
+ AuthenticationToken token)
+ {
+ checkArgument(token instanceof BearerAuthenticationToken, "%s is required",
+ BearerAuthenticationToken.class);
+
+ BearerAuthenticationToken bt = (BearerAuthenticationToken) token;
+ Claims c = checkToken(bt);
+
+ return helper.getAuthenticationInfo(c.getSubject(), bt.getCredentials());
+ }
+
+ /**
+ * Validates the jwt token.
+ *
+ *
+ * @param token jwt token
+ *
+ * @return claim
+ */
+ private Claims checkToken(BearerAuthenticationToken token)
+ {
+ Claims claims;
+
+ try
+ {
+ //J-
+ claims = Jwts.parser()
+ .setSigningKeyResolver(resolver)
+ .parseClaimsJws(token.getCredentials())
+ .getBody();
+ //J+
+ }
+ catch (JwtException ex)
+ {
+ throw new AuthenticationException("signature is invalid", ex);
+ }
+
+ return claims;
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** dao realm helper */
+ private final DAORealmHelper helper;
+
+ /** secure key resolver */
+ private final SecureKeyResolver resolver;
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java
new file mode 100644
index 0000000000..71b694f7ed
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer. 2. Redistributions in
+ * binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other
+ * materials provided with the distribution. 3. Neither the name of SCM-Manager;
+ * nor the names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import sonia.scm.user.User;
+
+import static com.google.common.base.Preconditions.*;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import javax.inject.Inject;
+
+/**
+ * Creates bearer token for a given user.
+ *
+ * @author Sebastian Sdorra
+ * @since 2.0.0
+ */
+public final class BearerTokenGenerator
+{
+
+ /**
+ * Constructs a new token generator.
+ *
+ *
+ * @param keyGenerator key generator
+ * @param keyResolver secure key resolver
+ */
+ @Inject
+ public BearerTokenGenerator(KeyGenerator keyGenerator,
+ SecureKeyResolver keyResolver)
+ {
+ this.keyGenerator = keyGenerator;
+ this.keyResolver = keyResolver;
+ }
+
+ //~--- methods --------------------------------------------------------------
+
+ /**
+ * Creates a new bearer token for the given user.
+ *
+ *
+ * @param user user
+ *
+ * @return bearer token
+ */
+ public String createBearerToken(User user)
+ {
+ checkNotNull(user, "user is required");
+
+ SecureKey key = keyResolver.getSecureKey(user.getName());
+
+ // TODO add expiration date
+
+ //J-
+ return Jwts.builder()
+ .setSubject(user.getName())
+ .setId(keyGenerator.createKey())
+ .signWith(SignatureAlgorithm.HS256, key.getBytes())
+ .compact();
+ //J+
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** key generator */
+ private final KeyGenerator keyGenerator;
+
+ /** secure key resolver */
+ private final SecureKeyResolver keyResolver;
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecureKey.java b/scm-webapp/src/main/java/sonia/scm/security/SecureKey.java
new file mode 100644
index 0000000000..f8e12de398
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/SecureKey.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer. 2. Redistributions in
+ * binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other
+ * materials provided with the distribution. 3. Neither the name of SCM-Manager;
+ * nor the names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import com.google.common.base.Objects;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * Secure key can be used for singing messages and tokens.
+ *
+ * @author Sebastian Sdorra
+ * @since 2.0.0
+ */
+@XmlRootElement(name = "secure-key")
+@XmlAccessorType(XmlAccessType.FIELD)
+public final class SecureKey
+{
+
+ /**
+ * Constructs a new secure key.
+ * This constructor should only be used by jaxb.
+ *
+ */
+ SecureKey() {}
+
+ /**
+ * Constructs a new secure key.
+ *
+ *
+ * @param bytes bytes of key
+ * @param creationDate creation date
+ */
+ public SecureKey(byte[] bytes, long creationDate)
+ {
+ this.bytes = bytes;
+ this.creationDate = creationDate;
+ }
+
+ //~--- methods --------------------------------------------------------------
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj == null)
+ {
+ return false;
+ }
+
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+
+ final SecureKey other = (SecureKey) obj;
+
+ return Objects.equal(bytes, other.bytes)
+ && Objects.equal(creationDate, other.creationDate);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(bytes, creationDate);
+ }
+
+ //~--- get methods ----------------------------------------------------------
+
+ /**
+ * Returns the bytes of the key.
+ *
+ *
+ * @return bytes of key
+ */
+ public byte[] getBytes()
+ {
+ return bytes;
+ }
+
+ /**
+ * Returns the creation date of the key.
+ *
+ *
+ * @return key creation date
+ */
+ public long getCreationDate()
+ {
+ return creationDate;
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** bytes of key */
+ private byte[] bytes;
+
+ /** creation date */
+ private long creationDate;
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java
new file mode 100644
index 0000000000..c7c594d5e3
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer. 2. Redistributions in
+ * binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other
+ * materials provided with the distribution. 3. Neither the name of SCM-Manager;
+ * nor the names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwsHeader;
+import io.jsonwebtoken.SigningKeyResolverAdapter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import sonia.scm.store.ConfigurationEntryStore;
+import sonia.scm.store.ConfigurationEntryStoreFactory;
+
+import static com.google.common.base.Preconditions.*;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import java.security.SecureRandom;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Resolve secure keys which can be used for signing token and messages.
+ *
+ * @author Sebastian Sdorra
+ * @since 2.0.0
+ */
+@Singleton
+public class SecureKeyResolver extends SigningKeyResolverAdapter
+{
+
+ /** key length */
+ private static final int KEY_LENGTH = 64;
+
+ /** name of the configuration store */
+ @VisibleForTesting
+ static final String STORE_NAME = "keys";
+
+ /**
+ * the logger for SecureKeyResolver
+ */
+ private static final Logger logger =
+ LoggerFactory.getLogger(SecureKeyResolver.class);
+
+ //~--- constructors ---------------------------------------------------------
+
+ /**
+ * Constructs a new SecureKeyResolver
+ *
+ *
+ * @param storeFactory store factory
+ */
+ @Inject
+ public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory)
+ {
+ this.store = storeFactory.getStore(SecureKey.class, STORE_NAME);
+ }
+
+ //~--- methods --------------------------------------------------------------
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims)
+ {
+ checkNotNull(claims, "claims is required");
+
+ String subject = claims.getSubject();
+
+ checkArgument(!Strings.isNullOrEmpty(subject), "subject is required");
+
+ SecureKey key = store.get(subject);
+
+ checkState(key != null, "could not resolve key for subject %s", subject);
+
+ return key.getBytes();
+ }
+
+ //~--- get methods ----------------------------------------------------------
+
+ /**
+ * Returns the secure key for the given subject, if there is no key for the
+ * subject a new key is generated.
+ *
+ * @param subject subject
+ *
+ * @return secure key
+ */
+ public SecureKey getSecureKey(String subject)
+ {
+ SecureKey key = store.get(subject);
+
+ if (key == null)
+ {
+ logger.trace("create new key for subject");
+ key = createNewKey();
+ store.put(subject, key);
+ }
+
+ return key;
+ }
+
+ //~--- methods --------------------------------------------------------------
+
+ /**
+ * Creates a new secure key.
+ *
+ *
+ * @return new secure key
+ */
+ private SecureKey createNewKey()
+ {
+ byte[] bytes = new byte[KEY_LENGTH];
+
+ random.nextBytes(bytes);
+
+ return new SecureKey(bytes, System.currentTimeMillis());
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** secure randon */
+ private final SecureRandom random = new SecureRandom();
+
+ /** configuration entry store */
+ private final ConfigurationEntryStore store;
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java
new file mode 100644
index 0000000000..243e0da8f2
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java
@@ -0,0 +1,278 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. Neither the name of SCM-Manager; nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwsHeader;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.subject.PrincipalCollection;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import sonia.scm.group.GroupDAO;
+import sonia.scm.user.User;
+import sonia.scm.user.UserDAO;
+import sonia.scm.user.UserTestData;
+
+import static org.junit.Assert.*;
+
+import static org.mockito.Mockito.*;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import java.security.SecureRandom;
+
+import java.util.Date;
+
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ *
+ * @author Sebastian Sdorra
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class BearerRealmTest
+{
+
+ /**
+ * Method description
+ *
+ */
+ @Test
+ public void testDoGetAuthenticationInfo()
+ {
+ SecureKey key = createSecureKey();
+
+ User marvin = UserTestData.createMarvin();
+
+ when(userDAO.get(marvin.getName())).thenReturn(marvin);
+
+ resolveKey(key);
+
+ String compact = createCompactToken(marvin.getName(), key);
+
+ BearerAuthenticationToken token = new BearerAuthenticationToken(compact);
+ AuthenticationInfo info = realm.doGetAuthenticationInfo(token);
+
+ assertNotNull(info);
+
+ PrincipalCollection principals = info.getPrincipals();
+
+ assertEquals(marvin.getName(), principals.getPrimaryPrincipal());
+ assertEquals(marvin, principals.oneByType(User.class));
+ }
+
+ /**
+ * Method description
+ *
+ */
+ @Test(expected = AuthenticationException.class)
+ public void testDoGetAuthenticationInfoWithExpiredToken()
+ {
+ User trillian = UserTestData.createTrillian();
+
+ when(userDAO.get(trillian.getName())).thenReturn(trillian);
+
+ SecureKey key = createSecureKey();
+
+ resolveKey(key);
+
+ Date exp = new Date(System.currentTimeMillis() - 600l);
+ String compact = createCompactToken(trillian.getName(), key, exp);
+
+ realm.doGetAuthenticationInfo(new BearerAuthenticationToken(compact));
+ }
+
+ /**
+ * Method description
+ *
+ */
+ @Test(expected = AuthenticationException.class)
+ public void testDoGetAuthenticationInfoWithInvalidSignature()
+ {
+ resolveKey(createSecureKey());
+
+ User trillian = UserTestData.createTrillian();
+ String compact = createCompactToken(trillian.getName(), createSecureKey());
+
+ realm.doGetAuthenticationInfo(new BearerAuthenticationToken(compact));
+ }
+
+ /**
+ * Method description
+ *
+ */
+ @Test(expected = AuthenticationException.class)
+ public void testDoGetAuthenticationInfoWithoutSignature()
+ {
+ User trillian = UserTestData.createTrillian();
+
+ when(userDAO.get(trillian.getName())).thenReturn(trillian);
+
+ String compact = Jwts.builder().setSubject("test").compact();
+
+ realm.doGetAuthenticationInfo(new BearerAuthenticationToken(compact));
+ }
+
+ /**
+ * Method description
+ *
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testDoGetAuthenticationInfoWrongToken()
+ {
+ realm.doGetAuthenticationInfo(new UsernamePasswordToken("test", "test"));
+ }
+
+ //~--- set methods ----------------------------------------------------------
+
+ /**
+ * Method description
+ *
+ */
+ @Before
+ public void setUp()
+ {
+ realm = new BearerRealm(keyResolver, userDAO, groupDAO);
+ }
+
+ //~--- methods --------------------------------------------------------------
+
+ /**
+ * Method description
+ *
+ *
+ * @param subject
+ * @param key
+ *
+ * @return
+ */
+ private String createCompactToken(String subject, SecureKey key)
+ {
+ return createCompactToken(subject, key,
+ new Date(System.currentTimeMillis() + 60000));
+ }
+
+ /**
+ * Method description
+ *
+ *
+ * @param subject
+ * @param key
+ * @param exp
+ *
+ * @return
+ */
+ private String createCompactToken(String subject, SecureKey key, Date exp)
+ {
+ //J-
+ return Jwts.builder()
+ .setSubject(subject)
+ .setExpiration(exp)
+ .signWith(SignatureAlgorithm.HS256, key.getBytes())
+ .compact();
+ //J+
+ }
+
+ /**
+ * Method description
+ *
+ *
+ * @return
+ */
+ private SecureKey createSecureKey()
+ {
+ byte[] bytes = new byte[32];
+
+ random.nextBytes(bytes);
+
+ return new SecureKey(bytes, System.currentTimeMillis());
+ }
+
+ /**
+ * Method description
+ *
+ *
+ * @param key
+ */
+ private void resolveKey(SecureKey key)
+ {
+ //J-
+ when(
+ keyResolver.resolveSigningKey(
+ any(JwsHeader.class),
+ any(Claims.class)
+ )
+ )
+ .thenReturn(
+ new SecretKeySpec(
+ key.getBytes(),
+ SignatureAlgorithm.HS256.getValue()
+ )
+ );
+ //J+
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** Field description */
+ private final SecureRandom random = new SecureRandom();
+
+ /** Field description */
+ @Mock
+ private GroupDAO groupDAO;
+
+ /** Field description */
+ @Mock
+ private SecureKeyResolver keyResolver;
+
+ /** Field description */
+ private BearerRealm realm;
+
+ /** Field description */
+ @Mock
+ private UserDAO userDAO;
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java
new file mode 100644
index 0000000000..7f1b0a6e37
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. Neither the name of SCM-Manager; nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import sonia.scm.user.User;
+import sonia.scm.user.UserTestData;
+
+import static org.hamcrest.Matchers.*;
+
+import static org.junit.Assert.*;
+
+import static org.mockito.Mockito.*;
+
+//~--- JDK imports ------------------------------------------------------------
+
+import java.security.SecureRandom;
+
+/**
+ *
+ * @author Sebastian Sdorra
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class BearerTokenGeneratorTest
+{
+
+ /**
+ * Method description
+ *
+ */
+ @Test
+ public void testCreateBearerToken()
+ {
+ User trillian = UserTestData.createTrillian();
+ SecureKey key = createSecureKey();
+
+ when(keyGenerator.createKey()).thenReturn("sid");
+ when(keyResolver.getSecureKey(trillian.getName())).thenReturn(key);
+
+ String token = tokenGenerator.createBearerToken(trillian);
+
+ assertThat(token, not(isEmptyOrNullString()));
+ assertTrue(Jwts.parser().isSigned(token));
+
+ Claims claims = Jwts.parser().setSigningKey(key.getBytes()).parseClaimsJws(
+ token).getBody();
+
+ assertEquals(trillian.getName(), claims.getSubject());
+ assertEquals("sid", claims.getId());
+ }
+
+ //~--- set methods ----------------------------------------------------------
+
+ /**
+ * Method description
+ *
+ */
+ @Before
+ public void setUp()
+ {
+ tokenGenerator = new BearerTokenGenerator(keyGenerator, keyResolver);
+ }
+
+ //~--- methods --------------------------------------------------------------
+
+ /**
+ * Method description
+ *
+ *
+ * @return
+ */
+ private SecureKey createSecureKey()
+ {
+ byte[] bytes = new byte[32];
+
+ random.nextBytes(bytes);
+
+ return new SecureKey(bytes, System.currentTimeMillis());
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** Field description */
+ private final SecureRandom random = new SecureRandom();
+
+ /** Field description */
+ @Mock
+ private KeyGenerator keyGenerator;
+
+ /** Field description */
+ @Mock
+ private SecureKeyResolver keyResolver;
+
+ /** Field description */
+ private BearerTokenGenerator tokenGenerator;
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java
new file mode 100644
index 0000000000..e3bacb4abc
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2014, Sebastian Sdorra
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. Neither the name of SCM-Manager; nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * http://bitbucket.org/sdorra/scm-manager
+ *
+ */
+
+
+
+package sonia.scm.security;
+
+//~--- non-JDK imports --------------------------------------------------------
+
+import io.jsonwebtoken.Jwts;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import sonia.scm.store.ConfigurationEntryStore;
+import sonia.scm.store.ConfigurationEntryStoreFactory;
+
+import static org.junit.Assert.*;
+
+import static org.mockito.Mockito.*;
+
+/**
+ *
+ * @author Sebastian Sdorra
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class SecureKeyResolverTest
+{
+
+ /**
+ * Method description
+ *
+ */
+ @Test
+ public void testGetSecureKey()
+ {
+ SecureKey key = resolver.getSecureKey("test");
+
+ assertNotNull(key);
+ when(store.get("test")).thenReturn(key);
+
+ SecureKey sameKey = resolver.getSecureKey("test");
+
+ assertSame(key, sameKey);
+ }
+
+ /**
+ * Method description
+ *
+ */
+ @Test
+ public void testResolveSigningKeyBytes()
+ {
+ SecureKey key = resolver.getSecureKey("test");
+
+ when(store.get("test")).thenReturn(key);
+
+ byte[] bytes = resolver.resolveSigningKeyBytes(null,
+ Jwts.claims().setSubject("test"));
+
+ assertArrayEquals(key.getBytes(), bytes);
+ }
+
+ /**
+ * Method description
+ *
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testResolveSigningKeyBytesWithoutKey()
+ {
+ resolver.resolveSigningKeyBytes(null, Jwts.claims().setSubject("test"));
+ }
+
+ /**
+ * Method description
+ *
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testResolveSigningKeyBytesWithoutSubject()
+ {
+ resolver.resolveSigningKeyBytes(null, Jwts.claims());
+ }
+
+ //~--- set methods ----------------------------------------------------------
+
+ /**
+ * Method description
+ *
+ */
+ @Before
+ public void setUp()
+ {
+ ConfigurationEntryStoreFactory factory =
+ mock(ConfigurationEntryStoreFactory.class);
+
+ when(factory.getStore(SecureKey.class,
+ SecureKeyResolver.STORE_NAME)).thenReturn(store);
+ resolver = new SecureKeyResolver(factory);
+ }
+
+ //~--- fields ---------------------------------------------------------------
+
+ /** Field description */
+ private SecureKeyResolver resolver;
+
+ /** Field description */
+ @Mock
+ private ConfigurationEntryStore store;
+}