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.
This commit is contained in:
Sebastian Sdorra
2017-01-17 14:40:50 +01:00
parent e7d6f50fd9
commit 2388cfd35d
15 changed files with 912 additions and 349 deletions

View File

@@ -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<String> 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 <T> type of field
* @param key key of token field
*
* @return optional value of custom field
*/
<T> Optional<T> getCustom(String key);
/**
* Returns compact representation of token.
*
* @return compact representation
*/
String compact();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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";

View File

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

View File

@@ -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();
}
}

View File

@@ -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<TokenClaimsEnricher> 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<String,Object> 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<TokenClaimsEnricher> enrichers;
/** key generator */
private final KeyGenerator keyGenerator;
/** secure key resolver */
private final SecureKeyResolver keyResolver;
}

View File

@@ -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<String> 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<Object> getCustom(String key) {
return Optional.ofNullable(claims.get(key));
}
@Override
public String compact() {
return compact;
}
}

View File

@@ -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<TokenClaimsEnricher> enrichers;
private String subject;
private String issuer;
private long expiresIn = 10l;
private TimeUnit expiresInUnit = TimeUnit.MINUTES;
private Scope scope = Scope.empty();
private final Map<String,Object> custom = Maps.newHashMap();
JwtAccessTokenBuilder(
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<TokenClaimsEnricher> 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<String,Object> 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);
}
}

View File

@@ -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<TokenClaimsEnricher> enrichers;
@Inject
public JwtAccessTokenBuilderFactory(
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<TokenClaimsEnricher> enrichers
) {
this.keyGenerator = keyGenerator;
this.keyResolver = keyResolver;
this.enrichers = enrichers;
}
@Override
public JwtAccessTokenBuilder create() {
return new JwtAccessTokenBuilder(keyGenerator, keyResolver, enrichers);
}
}

View File

@@ -72,7 +72,7 @@ public final class Scopes {
public static Scope fromClaims(Map<String,Object> claims) {
Scope scope = Scope.empty();
if (claims.containsKey(Scopes.CLAIMS_KEY)) {
scope = Scope.valueOf((List<String>)claims.get(Scopes.CLAIMS_KEY));
scope = Scope.valueOf((Iterable<String>)claims.get(Scopes.CLAIMS_KEY));
}
return scope;
}

View File

@@ -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());

View File

@@ -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<TokenClaimsEnricher> 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());
}
}

View File

@@ -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<TokenClaimsEnricher> 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());
}
}

View File

@@ -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 });