From 2388cfd35de0824832a14beee2e18fd8de2f1bc5 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 17 Jan 2017 14:40:50 +0100 Subject: [PATCH] create a more flexible interface for the creation of access tokens Provide a AccessTokenBuilderFactory to simplify the creation of access tokens and a default implementation which is based on JWT. Added also an AccessTokenCookieIssuer to unify the creation of access token cookies. Removed old BearerTokenGenerator. --- .../java/sonia/scm/security/AccessToken.java | 107 +++++++++++ .../scm/security/AccessTokenBuilder.java | 99 ++++++++++ .../security/AccessTokenBuilderFactory.java | 51 ++++++ .../main/java/sonia/scm/util/HttpUtil.java | 3 + .../resources/AuthenticationResource.java | 71 +++---- .../scm/security/AccessTokenCookieIssuer.java | 131 +++++++++++++ .../scm/security/BearerTokenGenerator.java | 151 --------------- .../sonia/scm/security/JwtAccessToken.java | 93 ++++++++++ .../scm/security/JwtAccessTokenBuilder.java | 173 ++++++++++++++++++ .../JwtAccessTokenBuilderFactory.java | 64 +++++++ .../main/java/sonia/scm/security/Scopes.java | 2 +- .../web/CookieBearerWebTokenGenerator.java | 10 +- .../security/BearerTokenGeneratorTest.java | 142 -------------- .../security/JwtAccessTokenBuilderTest.java | 161 ++++++++++++++++ .../CookieBearerWebTokenGeneratorTest.java | 3 +- 15 files changed, 912 insertions(+), 349 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/security/AccessToken.java create mode 100644 scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java create mode 100644 scm-core/src/main/java/sonia/scm/security/AccessTokenBuilderFactory.java create mode 100644 scm-webapp/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java create mode 100644 scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java create mode 100644 scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java create mode 100644 scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java delete mode 100644 scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java diff --git a/scm-core/src/main/java/sonia/scm/security/AccessToken.java b/scm-core/src/main/java/sonia/scm/security/AccessToken.java new file mode 100644 index 0000000000..714b09eff8 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AccessToken.java @@ -0,0 +1,107 @@ +/** + * 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; + +import java.util.Date; +import java.util.Optional; + +/** + * An access token can be used to access scm-manager without providing username and password. An {@link AccessToken} can + * be issued from a restful webservice endpoint by providing credentials. After the token was issued, the token must be + * send along with every request. The token should be send in its compact representation as bearer authorization header + * or as cookie. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public interface AccessToken { + + /** + * Returns unique id of the access token. + * + * @return unique id + */ + String getId(); + + /** + * Returns name of subject which identifies the principal. + * + * @return name of subject + */ + String getSubject(); + + /** + * Returns optional issuer. The issuer identifies the principal that issued the token. + * + * @return optional issuer + */ + Optional getIssuer(); + + /** + * Returns time at which the token was issued. + * + * @return time at which the token was issued + */ + Date getIssuedAt(); + + /** + * Returns the expiration time of token. + * + * @return expiration time + */ + Date getExpiration(); + + /** + * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this + * token. For example we could issue a token which can only be used to read a single repository. for more informations + * please have a look at {@link Scope}. + * + * @return scope of token. + */ + Scope getScope(); + + /** + * Returns an optional value of a custom token field. + * + * @param type of field + * @param key key of token field + * + * @return optional value of custom field + */ + Optional getCustom(String key); + + /** + * Returns compact representation of token. + * + * @return compact representation + */ + String compact(); +} diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java new file mode 100644 index 0000000000..dd7986c22a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java @@ -0,0 +1,99 @@ +/** + * 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; + +import java.util.concurrent.TimeUnit; + +/** + * The access token builder is able to create {@link AccessToken}. For more informations about access tokens have look + * at {@link AccessToken}. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public interface AccessTokenBuilder { + + /** + * Sets the subject for the token. + * If the subject is not set the currently authenticated subject will be used instead. + * + * @param subject subject of token + * + * @return * @return {@code this} + */ + AccessTokenBuilder subject(String subject); + + /** + * Adds a custom entry to the token. + * + * @param key key of custom entry + * @param value value of entry + * + * @return {@code this} + */ + AccessTokenBuilder custom(String key, Object value); + + /** + * Sets the issuer for the token. + * + * @param issuer issuer name or url + * + * @return {@code this} + */ + AccessTokenBuilder issuer(String issuer); + + /** + * Sets the expiration for the token. + * + * @param count expiration count + * @param unit expirtation unit + * + * @return {@code this} + */ + AccessTokenBuilder expiresIn(long count, TimeUnit unit); + + /** + * Reduces the permissions of the token by providing a scope. + * + * @param scope scope of token + * + * @return {@code this} + */ + AccessTokenBuilder scope(Scope scope); + + /** + * Creates a new {@link AccessToken} with the provided settings. + * + * @return new {@link AccessToken} + */ + AccessToken build(); + +} diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilderFactory.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilderFactory.java new file mode 100644 index 0000000000..9ec8bb5d8a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilderFactory.java @@ -0,0 +1,51 @@ +/** + * 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; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * Creates new {@link AccessTokenBuilder}. The AccessTokenBuilderFactory resolves all required dependencies for the + * access token builder. The builder factory is the main entry point for creating {@link AccessToken}. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +@ExtensionPoint(multi = false) +public interface AccessTokenBuilderFactory { + + /** + * Creates a new {@link AccessTokenBuilder}. + * + * @return new {@link AccessTokenBuilder} + */ + AccessTokenBuilder create(); +} diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index 89d1646d5e..27abcaffbe 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -86,6 +86,9 @@ public final class HttpUtil /** * Name of bearer authentication cookie. + * + * TODO find a better place + * * @since 2.0.0 */ public static final String COOKIE_BEARER_AUTHENTICATION = "X-Bearer-Token"; diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java index 7f77eb5ee8..68b58095c1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AuthenticationResource.java @@ -57,17 +57,8 @@ import sonia.scm.ScmState; import sonia.scm.ScmStateFactory; import sonia.scm.api.rest.RestActionResult; import sonia.scm.config.ScmConfiguration; -import sonia.scm.security.BearerTokenGenerator; import sonia.scm.security.Tokens; -import sonia.scm.user.User; import sonia.scm.util.HttpUtil; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.concurrent.TimeUnit; - -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -84,6 +75,10 @@ import javax.ws.rs.core.Response; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import sonia.scm.security.AccessToken; +import sonia.scm.security.AccessTokenBuilder; +import sonia.scm.security.AccessTokenBuilderFactory; +import sonia.scm.security.AccessTokenCookieIssuer; import sonia.scm.security.Scope; /** @@ -118,15 +113,17 @@ public class AuthenticationResource * * @param configuration * @param stateFactory - * @param tokenGenerator + * @param tokenBuilderFactory + * @param cookieIssuer */ @Inject public AuthenticationResource(ScmConfiguration configuration, - ScmStateFactory stateFactory, BearerTokenGenerator tokenGenerator) + ScmStateFactory stateFactory, AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer) { this.configuration = configuration; this.stateFactory = stateFactory; - this.tokenGenerator = tokenGenerator; + this.tokenBuilderFactory = tokenBuilderFactory; + this.cookieIssuer = cookieIssuer; } //~--- methods -------------------------------------------------------------- @@ -170,33 +167,20 @@ public class AuthenticationResource try { - subject.login(Tokens.createAuthenticationToken(request, username, - password)); - - User user = subject.getPrincipals().oneByType(User.class); - - String token = tokenGenerator.createBearerToken(user, scope != null ? Scope.valueOf(scope) : Scope.empty()); + subject.login(Tokens.createAuthenticationToken(request, username, password)); + AccessTokenBuilder tokenBuilder = tokenBuilderFactory.create(); + if ( scope != null ) { + tokenBuilder.scope(Scope.valueOf(scope)); + } + AccessToken token = tokenBuilder.build(); ScmState state; - if (cookie) - { - Cookie c = new Cookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, token); - - c.setPath(request.getContextPath()); - - // TODO: should be configureable - c.setMaxAge((int) TimeUnit.SECONDS.convert(10, TimeUnit.HOURS)); - - // set http only flag only xsrf protection is disabled, - // because we have to extract the xsrf key with javascript in the wui - c.setHttpOnly(!configuration.isEnabledXsrfProtection()); - response.addCookie(c); + if (cookie) { + cookieIssuer.authenticate(request, response, token); state = stateFactory.createState(subject); - } - else - { - state = stateFactory.createState(subject, token); + } else { + state = stateFactory.createState(subject, token.compact()); } res = Response.ok(state).build(); @@ -276,16 +260,8 @@ public class AuthenticationResource subject.logout(); - // remove bearer authentication cookie - Cookie c = new Cookie( - HttpUtil.COOKIE_BEARER_AUTHENTICATION, - Util.EMPTY_STRING - ); - c.setPath(request.getContextPath()); - c.setMaxAge(0); - c.setHttpOnly(true); - - response.addCookie(c); + // remove authentication cookie + cookieIssuer.invalidate(request, response); Response resp; @@ -481,5 +457,8 @@ public class AuthenticationResource private final ScmStateFactory stateFactory; /** Field description */ - private final BearerTokenGenerator tokenGenerator; + private final AccessTokenBuilderFactory tokenBuilderFactory; + + /** Field description */ + private final AccessTokenCookieIssuer cookieIssuer; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java b/scm-webapp/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java new file mode 100644 index 0000000000..52760e1c9a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java @@ -0,0 +1,131 @@ +/** + * 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; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Date; + +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; +import sonia.scm.util.Util; + +/** + * Generates cookies and invalidates access token cookies. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class AccessTokenCookieIssuer { + + /** + * the logger for AccessTokenCookieIssuer + */ + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenCookieIssuer.class); + + private final ScmConfiguration configuration; + + /** + * Constructs a new instance. + * + * @param configuration scm main configuration + */ + @Inject + public AccessTokenCookieIssuer(ScmConfiguration configuration) { + this.configuration = configuration; + } + + /** + * Creates a cookie for token authentication and attaches it to the response. + * + * @param request http servlet request + * @param response http servlet response + * @param accessToken access token + */ + public void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken) { + LOG.trace("create and attach cookie for access token {}", accessToken.getId()); + Cookie c = new Cookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, accessToken.compact()); + c.setPath(request.getContextPath()); + c.setMaxAge(getMaxAge(accessToken)); + c.setHttpOnly(isHttpOnly()); + c.setSecure(isSecure(request)); + + // attach cookie to response + response.addCookie(c); + } + + /** + * Invalidates the authentication cookie. + * + * @param request http servlet request + * @param response http servlet response + */ + public void invalidate(HttpServletRequest request, HttpServletResponse response) { + LOG.trace("invalidates access token cookie"); + + Cookie c = new Cookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, Util.EMPTY_STRING); + c.setPath(request.getContextPath()); + c.setMaxAge(0); + c.setHttpOnly(isHttpOnly()); + c.setSecure(isSecure(request)); + + // attach empty cookie, that the browser can remove it + response.addCookie(c); + } + + private int getMaxAge(AccessToken accessToken){ + long maxAgeMs = accessToken.getExpiration().getTime() - new Date().getTime(); + return (int) TimeUnit.MILLISECONDS.toSeconds(maxAgeMs); + } + + private boolean isSecure(HttpServletRequest request){ + boolean secure = request.isSecure(); + if (!secure) { + LOG.warn("issuet a non secure cookie, protect your scm-manager instance with tls https://goo.gl/lVm0ph"); + } + return secure; + } + + private boolean isHttpOnly(){ + // set http only flag only xsrf protection is disabled, + // because we have to extract the xsrf key with javascript in the wui + return !configuration.isEnabledXsrfProtection(); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java deleted file mode 100644 index 768bf7d236..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java +++ /dev/null @@ -1,151 +0,0 @@ -/** - * 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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.user.User; - -import static com.google.common.base.Preconditions.*; -import com.google.common.collect.ImmutableSet; - -import com.google.common.collect.Maps; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Date; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -/** - * Creates bearer token for a given user. - * - * @author Sebastian Sdorra - * @since 2.0.0 - */ -public final class BearerTokenGenerator -{ - - /** - * the logger for BearerTokenGenerator - */ - private static final Logger logger = - LoggerFactory.getLogger(BearerTokenGenerator.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs a new token generator. - * - * - * @param keyGenerator key generator - * @param keyResolver secure key resolver - * @param enrichers token claims modifier - */ - @Inject - public BearerTokenGenerator( - KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers - ) { - this.keyGenerator = keyGenerator; - this.keyResolver = keyResolver; - this.enrichers = enrichers; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Creates a new bearer token for the given user. - * - * - * @param user user - * @param scope scope of token - * - * @return bearer token - */ - public String createBearerToken(User user, Scope scope) { - checkNotNull(user, "user is required"); - - String username = user.getName(); - - String id = keyGenerator.createKey(); - - logger.trace("create new token {} for user {}", id, username); - - SecureKey key = keyResolver.getSecureKey(username); - - Date now = new Date(); - - // TODO: should be configurable - long expiration = TimeUnit.MILLISECONDS.convert(10, TimeUnit.HOURS); - - Map claims = Maps.newHashMap(); - - // add scope to claims - Scopes.toClaims(claims, scope); - - // enrich claims with registered enrichers - enrichers.forEach((enricher) -> { - enricher.enrich(claims); - }); - - //J- - return Jwts.builder() - .setClaims(claims) - .setSubject(username) - .setId(id) - .signWith(SignatureAlgorithm.HS256, key.getBytes()) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + expiration)) - .compact(); - //J+ - } - - //~--- fields --------------------------------------------------------------- - - /** token claims modifier **/ - private final Set enrichers; - - /** key generator */ - private final KeyGenerator keyGenerator; - - /** secure key resolver */ - private final SecureKeyResolver keyResolver; -} diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java new file mode 100644 index 0000000000..9089444ebd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java @@ -0,0 +1,93 @@ +/** + * 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; + +import io.jsonwebtoken.Claims; +import java.util.Date; +import java.util.Optional; + +/** + * Jwt implementation of {@link AccessToken}. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class JwtAccessToken implements AccessToken { + + private final Claims claims; + private final String compact; + + JwtAccessToken(Claims claims, String compact) { + this.claims = claims; + this.compact = compact; + } + + @Override + public String getId() { + return claims.getId(); + } + + @Override + public String getSubject() { + return claims.getSubject(); + } + + @Override + public Optional getIssuer() { + return Optional.ofNullable(claims.getIssuer()); + } + + @Override + public Date getIssuedAt() { + return claims.getIssuedAt(); + } + + @Override + public Date getExpiration() { + return claims.getExpiration(); + } + + @Override + public Scope getScope() { + return Scopes.fromClaims(claims); + } + + @Override + public Optional getCustom(String key) { + return Optional.ofNullable(claims.get(key)); + } + + @Override + public String compact() { + return compact; + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java new file mode 100644 index 0000000000..0c27f1cb88 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -0,0 +1,173 @@ +/** + * 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; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Jwt implementation of {@link AccessTokenBuilder}. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public final class JwtAccessTokenBuilder implements AccessTokenBuilder { + + /** + * the logger for JwtAccessTokenBuilder + */ + private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenBuilder.class); + + private final KeyGenerator keyGenerator; + private final SecureKeyResolver keyResolver; + private final Set enrichers; + + private String subject; + private String issuer; + private long expiresIn = 10l; + private TimeUnit expiresInUnit = TimeUnit.MINUTES; + private Scope scope = Scope.empty(); + + private final Map custom = Maps.newHashMap(); + + JwtAccessTokenBuilder( + KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers + ) { + this.keyGenerator = keyGenerator; + this.keyResolver = keyResolver; + this.enrichers = enrichers; + } + + @Override + public JwtAccessTokenBuilder subject(String subject) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(subject), "null or empty value not allowed"); + this.subject = subject; + return this; + } + + @Override + public JwtAccessTokenBuilder custom(String key, Object value) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed"); + Preconditions.checkArgument(value != null, "null or empty value not allowed"); + this.custom.put(key, value); + return this; + } + + @Override + public JwtAccessTokenBuilder scope(Scope scope) { + Preconditions.checkArgument(scope != null, "scope can not be null"); + this.scope = scope; + return this; + } + + @Override + public JwtAccessTokenBuilder issuer(String issuer) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(issuer), "null or empty value not allowed"); + this.issuer = issuer; + return this; + } + + @Override + public JwtAccessTokenBuilder expiresIn(long count, TimeUnit unit) { + Preconditions.checkArgument(count > 0, "expires in must be greater than 0"); + Preconditions.checkArgument(unit != null, "unit can not be null"); + + this.expiresIn = count; + this.expiresInUnit = unit; + + return this; + } + + private String getSubject(){ + if (subject == null) { + Subject currentSubject = SecurityUtils.getSubject(); + // TODO find a better way + currentSubject.checkRole(Role.USER); + return currentSubject.getPrincipal().toString(); + } + return subject; + } + + @Override + public JwtAccessToken build() { + String id = keyGenerator.createKey(); + + String sub = getSubject(); + + LOG.trace("create new token {} for user {}", id, subject); + SecureKey key = keyResolver.getSecureKey(sub); + + Map customClaims = new HashMap<>(custom); + + // add scope to custom claims + Scopes.toClaims(customClaims, scope); + + // enrich claims with registered enrichers + enrichers.forEach((enricher) -> { + enricher.enrich(customClaims); + }); + + Date now = new Date(); + long expiration = expiresInUnit.toMillis(expiresIn); + + Claims claims = Jwts.claims(customClaims) + .setSubject(sub) + .setId(id) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + expiration)); + + if ( issuer != null ) { + claims.setIssuer(issuer); + } + + // sign token and create compact version + String compact = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS256, key.getBytes()) + .compact(); + + return new JwtAccessToken(claims, compact); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java new file mode 100644 index 0000000000..7eda91be01 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java @@ -0,0 +1,64 @@ +/** + * 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; + +import java.util.Set; +import javax.inject.Inject; +import sonia.scm.plugin.Extension; + +/** + * Jwt implementation of {@link AccessTokenBuilderFactory}. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +@Extension +public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFactory { + + private final KeyGenerator keyGenerator; + private final SecureKeyResolver keyResolver; + private final Set enrichers; + + @Inject + public JwtAccessTokenBuilderFactory( + KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers + ) { + this.keyGenerator = keyGenerator; + this.keyResolver = keyResolver; + this.enrichers = enrichers; + } + + @Override + public JwtAccessTokenBuilder create() { + return new JwtAccessTokenBuilder(keyGenerator, keyResolver, enrichers); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/Scopes.java b/scm-webapp/src/main/java/sonia/scm/security/Scopes.java index 67c2e6cc4c..d5d6b74021 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/Scopes.java +++ b/scm-webapp/src/main/java/sonia/scm/security/Scopes.java @@ -72,7 +72,7 @@ public final class Scopes { public static Scope fromClaims(Map claims) { Scope scope = Scope.empty(); if (claims.containsKey(Scopes.CLAIMS_KEY)) { - scope = Scope.valueOf((List)claims.get(Scopes.CLAIMS_KEY)); + scope = Scope.valueOf((Iterable)claims.get(Scopes.CLAIMS_KEY)); } return scope; } diff --git a/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java index ca9f1d7d22..ffc14cb625 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java @@ -33,7 +33,6 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; import sonia.scm.plugin.Extension; import sonia.scm.security.BearerAuthenticationToken; @@ -41,6 +40,7 @@ import sonia.scm.security.BearerAuthenticationToken; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import sonia.scm.util.HttpUtil; /** * Creates an {@link BearerAuthenticationToken} from the {@link #COOKIE_NAME} @@ -53,12 +53,6 @@ import javax.servlet.http.HttpServletRequest; public class CookieBearerWebTokenGenerator implements WebTokenGenerator { - /** cookie name */ - @VisibleForTesting - static final String COOKIE_NAME = "X-Bearer-Token"; - - //~--- methods -------------------------------------------------------------- - /** * Creates an {@link BearerAuthenticationToken} from the {@link #COOKIE_NAME} * cookie. @@ -77,7 +71,7 @@ public class CookieBearerWebTokenGenerator implements WebTokenGenerator { for (Cookie cookie : cookies) { - if (COOKIE_NAME.equals(cookie.getName())) + if (HttpUtil.COOKIE_BEARER_AUTHENTICATION.equals(cookie.getName())) { token = new BearerAuthenticationToken(cookie.getValue()); diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java deleted file mode 100644 index 688edc8eb1..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/** - * 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.collect.Sets; -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; -import java.util.Set; - -/** - * Tests {@link BearerTokenGenerator}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -public class BearerTokenGeneratorTest -{ - - private final SecureRandom random = new SecureRandom(); - - @Mock - private KeyGenerator keyGenerator; - - @Mock - private SecureKeyResolver keyResolver; - - private BearerTokenGenerator tokenGenerator; - - /** - * Set up mocks and object under test. - */ - @Before - public void setUp() { - Set enrichers = Sets.newHashSet(); - enrichers.add((claims) -> {claims.put("abc", "123");}); - tokenGenerator = new BearerTokenGenerator(keyGenerator, keyResolver, enrichers); - } - - - /** - * Tests {@link BearerTokenGenerator#createBearerToken(User, Scope)}. - */ - @Test - public void testCreateBearerToken() - { - Claims claims = createAssertAndParseToken(UserTestData.createTrillian(), "sid", Scope.empty()); - - assertEquals("123", claims.get("abc")); - assertNull(claims.get(Scopes.CLAIMS_KEY)); - } - - /** - * Tests {@link BearerTokenGenerator#createBearerToken(User, Scope)} with scope. - */ - @Test - @SuppressWarnings("unchecked") - public void testCreateBearerTokenWithScope(){ - Claims claims = createAssertAndParseToken(UserTestData.createTrillian(), "sid", Scope.valueOf("repo:*", "user:*")); - assertEquals("123", claims.get("abc")); - - Scope scope = Scopes.fromClaims(claims); - assertThat(scope, containsInAnyOrder("repo:*", "user:*")); - } - - private Claims createAssertAndParseToken(User user, String id, Scope scope){ - SecureKey key = createSecureKey(); - - when(keyGenerator.createKey()).thenReturn(id); - when(keyResolver.getSecureKey(user.getName())).thenReturn(key); - - String token = tokenGenerator.createBearerToken(user, scope); - - assertThat(token, not(isEmptyOrNullString())); - assertTrue(Jwts.parser().isSigned(token)); - - Claims claims = Jwts.parser().setSigningKey(key.getBytes()).parseClaimsJws(token).getBody(); - - assertEquals(user.getName(), claims.getSubject()); - assertEquals(id, claims.getId()); - - return claims; - } - - private SecureKey createSecureKey() { - byte[] bytes = new byte[32]; - random.nextBytes(bytes); - return new SecureKey(bytes, System.currentTimeMillis()); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java new file mode 100644 index 0000000000..61216d8db0 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -0,0 +1,161 @@ +/** + * 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; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.Sets; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * Unit test for {@link JwtAccessTokenBuilder}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class JwtAccessTokenBuilderTest { + + @Mock + private KeyGenerator keyGenerator; + + @Mock + private SecureKeyResolver secureKeyResolver; + + private Set enrichers; + + private JwtAccessTokenBuilder builder; + + @Rule + public ShiroRule shiro = new ShiroRule(); + + /** + * Prepare mocks and set up object under test. + */ + @Before + public void setUpObjectUnderTest() { + when(keyGenerator.createKey()).thenReturn("42"); + when(secureKeyResolver.getSecureKey(anyString())).thenReturn(createSecureKey()); + enrichers = Sets.newHashSet(); + JwtAccessTokenBuilderFactory factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers); + builder = factory.create(); + } + + /** + * Tests {@link JwtAccessTokenBuilder#build()} with subject from shiro context. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini", + username = "trillian", + password = "secret" + ) + public void testBuildWithoutSubject() { + JwtAccessToken token = builder.build(); + assertEquals("trillian", token.getSubject()); + } + + /** + * Tests {@link JwtAccessTokenBuilder#build()} with explicit subject. + */ + @Test + public void testBuildWithSubject() { + JwtAccessToken token = builder.subject("dent").build(); + assertEquals("dent", token.getSubject()); + } + + /** + * Tests {@link JwtAccessTokenBuilder#build()} with enricher. + */ + @Test + public void testBuildWithEnricher() { + enrichers.add((claims) -> claims.put("c", "d")); + JwtAccessToken token = builder.subject("dent").build(); + assertEquals("d", token.getCustom("c").get()); + } + + /** + * Tests {@link JwtAccessTokenBuilder#build()}. + */ + @Test + public void testBuild(){ + JwtAccessToken token = builder.subject("dent") + .issuer("https://www.scm-manager.org") + .expiresIn(5, TimeUnit.SECONDS) + .custom("a", "b") + .scope(Scope.valueOf("repo:*")) + .build(); + + // assert claims + assertClaims(token); + + // reparse and assert again + String compact = token.compact(); + assertThat(compact, not(isEmptyOrNullString())); + Claims claims = Jwts.parser() + .setSigningKey(secureKeyResolver.getSecureKey("dent").getBytes()) + .parseClaimsJws(compact) + .getBody(); + assertClaims(new JwtAccessToken(claims, compact)); + } + + private void assertClaims(JwtAccessToken token){ + assertThat(token.getId(), not(isEmptyOrNullString())); + assertNotNull( token.getIssuedAt() ); + assertNotNull( token.getExpiration()); + assertTrue(token.getExpiration().getTime() > token.getIssuedAt().getTime()); + assertEquals("dent", token.getSubject()); + assertTrue(token.getIssuer().isPresent()); + assertEquals(token.getIssuer().get(), "https://www.scm-manager.org"); + assertEquals("b", token.getCustom("a").get()); + assertEquals("[\"repo:*\"]", token.getScope().toString()); + } + + private SecureKey createSecureKey() { + byte[] bytes = new byte[32]; + new Random().nextBytes(bytes); + return new SecureKey(bytes, System.currentTimeMillis()); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java index c9a1400570..44c2edcc0e 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java @@ -51,6 +51,7 @@ import static org.mockito.Mockito.*; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import sonia.scm.util.HttpUtil; /** * @@ -69,7 +70,7 @@ public class CookieBearerWebTokenGeneratorTest { Cookie c = mock(Cookie.class); - when(c.getName()).thenReturn(CookieBearerWebTokenGenerator.COOKIE_NAME); + when(c.getName()).thenReturn(HttpUtil.COOKIE_BEARER_AUTHENTICATION); when(c.getValue()).thenReturn("value"); when(request.getCookies()).thenReturn(new Cookie[] { c });