mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 23:45:44 +01:00
@@ -83,8 +83,10 @@ import sonia.scm.security.AuthorizationChangedEventProducer;
|
||||
import sonia.scm.security.CipherHandler;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
||||
import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy;
|
||||
import sonia.scm.security.DefaultKeyGenerator;
|
||||
import sonia.scm.security.DefaultSecuritySystem;
|
||||
import sonia.scm.security.JwtAccessTokenRefreshStrategy;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.security.LoginAttemptHandler;
|
||||
import sonia.scm.security.SecuritySystem;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
@Extension
|
||||
public class DefaultJwtAccessTokenRefreshStrategy extends PercentageJwtAccessTokenRefreshStrategy {
|
||||
public DefaultJwtAccessTokenRefreshStrategy() {
|
||||
super(0.5F);
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,14 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
/**
|
||||
* Jwt implementation of {@link AccessToken}.
|
||||
*
|
||||
@@ -41,7 +46,9 @@ import java.util.Optional;
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class JwtAccessToken implements AccessToken {
|
||||
|
||||
|
||||
public static final String REFRESHABLE_UNTIL_CLAIM_KEY = "scm-manager.refreshExpiration";
|
||||
public static final String PARENT_TOKEN_ID_CLAIM_KEY = "scm-manager.parentTokenId";
|
||||
private final Claims claims;
|
||||
private final String compact;
|
||||
|
||||
@@ -75,6 +82,15 @@ public final class JwtAccessToken implements AccessToken {
|
||||
return claims.getExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Date> getRefreshExpiration() {
|
||||
return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class));
|
||||
}
|
||||
|
||||
public Optional<String> getParentKey() {
|
||||
return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scope getScope() {
|
||||
return Scopes.fromClaims(claims);
|
||||
@@ -90,5 +106,9 @@ public final class JwtAccessToken implements AccessToken {
|
||||
public String compact() {
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return Collections.unmodifiableMap(claims);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,12 @@ import com.google.common.collect.Maps;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
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;
|
||||
@@ -48,7 +50,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Jwt implementation of {@link AccessTokenBuilder}.
|
||||
*
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -58,21 +60,27 @@ 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 KeyGenerator keyGenerator;
|
||||
private final SecureKeyResolver keyResolver;
|
||||
private final Clock clock;
|
||||
|
||||
private String subject;
|
||||
private String issuer;
|
||||
private long expiresIn = 60l;
|
||||
private TimeUnit expiresInUnit = TimeUnit.MINUTES;
|
||||
private long expiresIn = 1;
|
||||
private TimeUnit expiresInUnit = TimeUnit.HOURS;
|
||||
private long refreshableFor = 12;
|
||||
private TimeUnit refreshableForUnit = TimeUnit.HOURS;
|
||||
private Instant refreshExpiration;
|
||||
private String parentKeyId;
|
||||
private Scope scope = Scope.empty();
|
||||
|
||||
|
||||
private final Map<String,Object> custom = Maps.newHashMap();
|
||||
|
||||
JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {
|
||||
|
||||
JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Clock clock) {
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.keyResolver = keyResolver;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,7 +89,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
||||
this.subject = subject;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public JwtAccessTokenBuilder custom(String key, Object value) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed");
|
||||
@@ -92,11 +100,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
||||
|
||||
@Override
|
||||
public JwtAccessTokenBuilder scope(Scope scope) {
|
||||
Preconditions.checkArgument(scope != null, "scope can not be null");
|
||||
Preconditions.checkArgument(scope != null, "scope cannot be null");
|
||||
this.scope = scope;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public JwtAccessTokenBuilder issuer(String issuer) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(issuer), "null or empty value not allowed");
|
||||
@@ -106,15 +114,37 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
||||
|
||||
@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");
|
||||
|
||||
Preconditions.checkArgument(count > 0, "count must be greater than 0");
|
||||
Preconditions.checkArgument(unit != null, "unit cannot be null");
|
||||
|
||||
this.expiresIn = count;
|
||||
this.expiresInUnit = unit;
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public JwtAccessTokenBuilder refreshableFor(long count, TimeUnit unit) {
|
||||
Preconditions.checkArgument(count >= 0, "count must be greater or equal to 0");
|
||||
Preconditions.checkArgument(unit != null, "unit cannot be null");
|
||||
|
||||
this.refreshableFor = count;
|
||||
this.refreshableForUnit = unit;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) {
|
||||
this.refreshExpiration = refreshExpiration;
|
||||
this.refreshableFor = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JwtAccessTokenBuilder parentKey(String parentKeyId) {
|
||||
this.parentKeyId = parentKeyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
private String getSubject(){
|
||||
if (subject == null) {
|
||||
Subject currentSubject = SecurityUtils.getSubject();
|
||||
@@ -130,35 +160,48 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
||||
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);
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
Instant now = clock.instant();
|
||||
long expiration = expiresInUnit.toMillis(expiresIn);
|
||||
|
||||
|
||||
Claims claims = Jwts.claims(customClaims)
|
||||
.setSubject(sub)
|
||||
.setId(id)
|
||||
.setIssuedAt(now)
|
||||
.setExpiration(new Date(now.getTime() + expiration));
|
||||
|
||||
.setIssuedAt(Date.from(now))
|
||||
.setExpiration(new Date(now.toEpochMilli() + expiration));
|
||||
|
||||
|
||||
if (refreshableFor > 0) {
|
||||
long refreshExpiration = refreshableForUnit.toMillis(refreshableFor);
|
||||
claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, new Date(now.toEpochMilli() + refreshExpiration).getTime());
|
||||
} else if (refreshExpiration != null) {
|
||||
claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, Date.from(refreshExpiration));
|
||||
}
|
||||
if (parentKeyId == null) {
|
||||
claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, id);
|
||||
} else {
|
||||
claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, parentKeyId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
*/
|
||||
package sonia.scm.security;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import sonia.scm.plugin.Extension;
|
||||
@@ -46,19 +47,25 @@ public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFac
|
||||
private final KeyGenerator keyGenerator;
|
||||
private final SecureKeyResolver keyResolver;
|
||||
private final Set<AccessTokenEnricher> enrichers;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
public JwtAccessTokenBuilderFactory(
|
||||
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<AccessTokenEnricher> enrichers
|
||||
) {
|
||||
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<AccessTokenEnricher> enrichers) {
|
||||
this(keyGenerator, keyResolver, enrichers, Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
JwtAccessTokenBuilderFactory(
|
||||
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<AccessTokenEnricher> enrichers, Clock clock) {
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.keyResolver = keyResolver;
|
||||
this.enrichers = enrichers;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtAccessTokenBuilder create() {
|
||||
JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver);
|
||||
JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver, clock);
|
||||
|
||||
// enrich access token builder
|
||||
enrichers.forEach((enricher) -> {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import sonia.scm.plugin.ExtensionPoint;
|
||||
|
||||
@ExtensionPoint(multi = false)
|
||||
public interface JwtAccessTokenRefreshStrategy {
|
||||
boolean shouldBeRefreshed(JwtAccessToken oldToken);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.time.Clock;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class JwtAccessTokenRefresher {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JwtAccessTokenRefresher.class);
|
||||
|
||||
private final JwtAccessTokenBuilderFactory builderFactory;
|
||||
private final JwtAccessTokenRefreshStrategy refreshStrategy;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
public JwtAccessTokenRefresher(JwtAccessTokenBuilderFactory builderFactory, JwtAccessTokenRefreshStrategy refreshStrategy) {
|
||||
this(builderFactory, refreshStrategy, Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
JwtAccessTokenRefresher(JwtAccessTokenBuilderFactory builderFactory, JwtAccessTokenRefreshStrategy refreshStrategy, Clock clock) {
|
||||
this.builderFactory = builderFactory;
|
||||
this.refreshStrategy = refreshStrategy;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@SuppressWarnings("squid:S3655") // the refresh expiration cannot be null at the time building the new token, because
|
||||
// we checked this before in tokenCanBeRefreshed
|
||||
public Optional<JwtAccessToken> refresh(JwtAccessToken oldToken) {
|
||||
JwtAccessTokenBuilder builder = builderFactory.create();
|
||||
Map<String, Object> claims = oldToken.getClaims();
|
||||
claims.forEach(builder::custom);
|
||||
|
||||
if (canBeRefreshed(oldToken) && shouldBeRefreshed(oldToken)) {
|
||||
Optional<Object> parentTokenId = oldToken.getCustom("scm-manager.parentTokenId");
|
||||
if (!parentTokenId.isPresent()) {
|
||||
log.warn("no parent token id found in token; could not refresh");
|
||||
return Optional.empty();
|
||||
}
|
||||
builder.expiresIn(computeOldExpirationInMillis(oldToken), TimeUnit.MILLISECONDS);
|
||||
builder.parentKey(parentTokenId.get().toString());
|
||||
builder.refreshExpiration(oldToken.getRefreshExpiration().get().toInstant());
|
||||
return Optional.of(builder.build());
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private long computeOldExpirationInMillis(JwtAccessToken oldToken) {
|
||||
return oldToken.getExpiration().getTime() - oldToken.getIssuedAt().getTime();
|
||||
}
|
||||
|
||||
private boolean canBeRefreshed(JwtAccessToken oldToken) {
|
||||
return tokenIsValid(oldToken) && tokenCanBeRefreshed(oldToken);
|
||||
}
|
||||
|
||||
private boolean shouldBeRefreshed(JwtAccessToken oldToken) {
|
||||
return refreshStrategy.shouldBeRefreshed(oldToken);
|
||||
}
|
||||
|
||||
private boolean tokenCanBeRefreshed(JwtAccessToken oldToken) {
|
||||
return oldToken.getRefreshExpiration().map(this::isAfterNow).orElse(false);
|
||||
}
|
||||
|
||||
private boolean tokenIsValid(JwtAccessToken oldToken) {
|
||||
return isAfterNow(oldToken.getExpiration());
|
||||
}
|
||||
|
||||
private boolean isAfterNow(Date expiration) {
|
||||
return expiration.toInstant().isAfter(clock.instant());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import java.time.Clock;
|
||||
|
||||
public class PercentageJwtAccessTokenRefreshStrategy implements JwtAccessTokenRefreshStrategy {
|
||||
|
||||
private final Clock clock;
|
||||
private final float refreshPercentage;
|
||||
|
||||
public PercentageJwtAccessTokenRefreshStrategy(float refreshPercentage) {
|
||||
this(Clock.systemDefaultZone(), refreshPercentage);
|
||||
}
|
||||
|
||||
PercentageJwtAccessTokenRefreshStrategy(Clock clock, float refreshPercentage) {
|
||||
this.clock = clock;
|
||||
this.refreshPercentage = refreshPercentage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeRefreshed(JwtAccessToken oldToken) {
|
||||
long liveSpan = oldToken.getExpiration().getTime() - oldToken.getIssuedAt().getTime();
|
||||
long age = clock.instant().toEpochMilli() - oldToken.getIssuedAt().getTime();
|
||||
return (float)age/liveSpan > refreshPercentage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package sonia.scm.web.security;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.filter.Filters;
|
||||
import sonia.scm.filter.WebElement;
|
||||
import sonia.scm.security.AccessToken;
|
||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||
import sonia.scm.security.AccessTokenResolver;
|
||||
import sonia.scm.security.BearerToken;
|
||||
import sonia.scm.security.JwtAccessToken;
|
||||
import sonia.scm.security.JwtAccessTokenRefresher;
|
||||
import sonia.scm.web.WebTokenGenerator;
|
||||
import sonia.scm.web.filter.HttpFilter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
@Priority(Filters.PRIORITY_POST_AUTHENTICATION)
|
||||
@WebElement(value = Filters.PATTERN_RESTAPI,
|
||||
morePatterns = { Filters.PATTERN_DEBUG })
|
||||
public class TokenRefreshFilter extends HttpFilter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TokenRefreshFilter.class);
|
||||
|
||||
private final Set<WebTokenGenerator> tokenGenerators;
|
||||
private final JwtAccessTokenRefresher refresher;
|
||||
private final AccessTokenResolver resolver;
|
||||
private final AccessTokenCookieIssuer issuer;
|
||||
|
||||
@Inject
|
||||
public TokenRefreshFilter(Set<WebTokenGenerator> tokenGenerators, JwtAccessTokenRefresher refresher, AccessTokenResolver resolver, AccessTokenCookieIssuer issuer) {
|
||||
this.tokenGenerators = tokenGenerators;
|
||||
this.refresher = refresher;
|
||||
this.resolver = resolver;
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
extractToken(request).ifPresent(token -> examineToken(request, response, token));
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private Optional<BearerToken> extractToken(HttpServletRequest request) {
|
||||
for (WebTokenGenerator generator : tokenGenerators) {
|
||||
AuthenticationToken token = generator.createToken(request);
|
||||
if (token instanceof BearerToken) {
|
||||
return of((BearerToken) token);
|
||||
}
|
||||
}
|
||||
return empty();
|
||||
}
|
||||
|
||||
private void examineToken(HttpServletRequest request, HttpServletResponse response, BearerToken token) {
|
||||
AccessToken accessToken = resolver.resolve(token);
|
||||
if (accessToken instanceof JwtAccessToken) {
|
||||
refresher.refresh((JwtAccessToken) accessToken)
|
||||
.ifPresent(jwtAccessToken -> refreshToken(request, response, jwtAccessToken));
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshToken(HttpServletRequest request, HttpServletResponse response, JwtAccessToken jwtAccessToken) {
|
||||
LOG.debug("refreshing authentication token");
|
||||
issuer.authenticate(request, response, jwtAccessToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user