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; +}