mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 23:45:44 +01:00
@@ -31,6 +31,7 @@
|
|||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,6 +80,8 @@ public interface AccessToken {
|
|||||||
*/
|
*/
|
||||||
Date getExpiration();
|
Date getExpiration();
|
||||||
|
|
||||||
|
Optional<Date> getRefreshExpiration();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
|
* 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
|
* token. For example we could issue a token which can only be used to read a single repository. for more informations
|
||||||
@@ -104,4 +107,9 @@ public interface AccessToken {
|
|||||||
* @return compact representation
|
* @return compact representation
|
||||||
*/
|
*/
|
||||||
String compact();
|
String compact();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns read only map of all claim keys with their values.
|
||||||
|
*/
|
||||||
|
Map<String, Object> getClaims();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,12 +74,22 @@ public interface AccessTokenBuilder {
|
|||||||
* Sets the expiration for the token.
|
* Sets the expiration for the token.
|
||||||
*
|
*
|
||||||
* @param count expiration count
|
* @param count expiration count
|
||||||
* @param unit expirtation unit
|
* @param unit expiration unit
|
||||||
*
|
*
|
||||||
* @return {@code this}
|
* @return {@code this}
|
||||||
*/
|
*/
|
||||||
AccessTokenBuilder expiresIn(long count, TimeUnit unit);
|
AccessTokenBuilder expiresIn(long count, TimeUnit unit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the time how long this token may be refreshed. Set this to 0 (zero) to disable automatic refresh.
|
||||||
|
*
|
||||||
|
* @param count Time unit count. If set to 0, automatic refresh is disabled.
|
||||||
|
* @param unit time unit
|
||||||
|
*
|
||||||
|
* @return {@code this}
|
||||||
|
*/
|
||||||
|
AccessTokenBuilder refreshableFor(long count, TimeUnit unit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduces the permissions of the token by providing a scope.
|
* Reduces the permissions of the token by providing a scope.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -72,8 +72,18 @@
|
|||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt</artifactId>
|
<artifactId>jjwt-impl</artifactId>
|
||||||
<version>0.4</version>
|
<version>${jjwt.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>${jjwt.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>${jjwt.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- json -->
|
<!-- json -->
|
||||||
@@ -540,6 +550,7 @@
|
|||||||
<scm.stage>DEVELOPMENT</scm.stage>
|
<scm.stage>DEVELOPMENT</scm.stage>
|
||||||
<scm.home>target/scm-it</scm.home>
|
<scm.home>target/scm-it</scm.home>
|
||||||
<environment.profile>default</environment.profile>
|
<environment.profile>default</environment.profile>
|
||||||
|
<jjwt.version>0.10.5</jjwt.version>
|
||||||
<selenium.version>2.53.1</selenium.version>
|
<selenium.version>2.53.1</selenium.version>
|
||||||
<wagon.version>1.0</wagon.version>
|
<wagon.version>1.0</wagon.version>
|
||||||
<mustache.version>0.8.17</mustache.version>
|
<mustache.version>0.8.17</mustache.version>
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ import sonia.scm.security.AuthorizationChangedEventProducer;
|
|||||||
import sonia.scm.security.CipherHandler;
|
import sonia.scm.security.CipherHandler;
|
||||||
import sonia.scm.security.CipherUtil;
|
import sonia.scm.security.CipherUtil;
|
||||||
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
||||||
|
import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy;
|
||||||
import sonia.scm.security.DefaultKeyGenerator;
|
import sonia.scm.security.DefaultKeyGenerator;
|
||||||
import sonia.scm.security.DefaultSecuritySystem;
|
import sonia.scm.security.DefaultSecuritySystem;
|
||||||
|
import sonia.scm.security.JwtAccessTokenRefreshStrategy;
|
||||||
import sonia.scm.security.KeyGenerator;
|
import sonia.scm.security.KeyGenerator;
|
||||||
import sonia.scm.security.LoginAttemptHandler;
|
import sonia.scm.security.LoginAttemptHandler;
|
||||||
import sonia.scm.security.SecuritySystem;
|
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;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jwt implementation of {@link AccessToken}.
|
* Jwt implementation of {@link AccessToken}.
|
||||||
*
|
*
|
||||||
@@ -42,6 +47,8 @@ import java.util.Optional;
|
|||||||
*/
|
*/
|
||||||
public final class JwtAccessToken implements AccessToken {
|
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 Claims claims;
|
||||||
private final String compact;
|
private final String compact;
|
||||||
|
|
||||||
@@ -75,6 +82,15 @@ public final class JwtAccessToken implements AccessToken {
|
|||||||
return claims.getExpiration();
|
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
|
@Override
|
||||||
public Scope getScope() {
|
public Scope getScope() {
|
||||||
return Scopes.fromClaims(claims);
|
return Scopes.fromClaims(claims);
|
||||||
@@ -91,4 +107,8 @@ public final class JwtAccessToken implements AccessToken {
|
|||||||
return 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.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
@@ -61,18 +63,24 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
|
|
||||||
private final KeyGenerator keyGenerator;
|
private final KeyGenerator keyGenerator;
|
||||||
private final SecureKeyResolver keyResolver;
|
private final SecureKeyResolver keyResolver;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
private String subject;
|
private String subject;
|
||||||
private String issuer;
|
private String issuer;
|
||||||
private long expiresIn = 60l;
|
private long expiresIn = 1;
|
||||||
private TimeUnit expiresInUnit = TimeUnit.MINUTES;
|
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 Scope scope = Scope.empty();
|
||||||
|
|
||||||
private final Map<String,Object> custom = Maps.newHashMap();
|
private final Map<String,Object> custom = Maps.newHashMap();
|
||||||
|
|
||||||
JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {
|
JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Clock clock) {
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
this.keyResolver = keyResolver;
|
this.keyResolver = keyResolver;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,7 +100,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtAccessTokenBuilder scope(Scope scope) {
|
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;
|
this.scope = scope;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -106,8 +114,8 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtAccessTokenBuilder expiresIn(long count, TimeUnit unit) {
|
public JwtAccessTokenBuilder expiresIn(long count, TimeUnit unit) {
|
||||||
Preconditions.checkArgument(count > 0, "expires in must be greater than 0");
|
Preconditions.checkArgument(count > 0, "count must be greater than 0");
|
||||||
Preconditions.checkArgument(unit != null, "unit can not be null");
|
Preconditions.checkArgument(unit != null, "unit cannot be null");
|
||||||
|
|
||||||
this.expiresIn = count;
|
this.expiresIn = count;
|
||||||
this.expiresInUnit = unit;
|
this.expiresInUnit = unit;
|
||||||
@@ -115,6 +123,28 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
return this;
|
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(){
|
private String getSubject(){
|
||||||
if (subject == null) {
|
if (subject == null) {
|
||||||
Subject currentSubject = SecurityUtils.getSubject();
|
Subject currentSubject = SecurityUtils.getSubject();
|
||||||
@@ -139,14 +169,27 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
// add scope to custom claims
|
// add scope to custom claims
|
||||||
Scopes.toClaims(customClaims, scope);
|
Scopes.toClaims(customClaims, scope);
|
||||||
|
|
||||||
Date now = new Date();
|
Instant now = clock.instant();
|
||||||
long expiration = expiresInUnit.toMillis(expiresIn);
|
long expiration = expiresInUnit.toMillis(expiresIn);
|
||||||
|
|
||||||
Claims claims = Jwts.claims(customClaims)
|
Claims claims = Jwts.claims(customClaims)
|
||||||
.setSubject(sub)
|
.setSubject(sub)
|
||||||
.setId(id)
|
.setId(id)
|
||||||
.setIssuedAt(now)
|
.setIssuedAt(Date.from(now))
|
||||||
.setExpiration(new Date(now.getTime() + expiration));
|
.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 ) {
|
if ( issuer != null ) {
|
||||||
claims.setIssuer(issuer);
|
claims.setIssuer(issuer);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
*/
|
*/
|
||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
@@ -46,19 +47,25 @@ public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFac
|
|||||||
private final KeyGenerator keyGenerator;
|
private final KeyGenerator keyGenerator;
|
||||||
private final SecureKeyResolver keyResolver;
|
private final SecureKeyResolver keyResolver;
|
||||||
private final Set<AccessTokenEnricher> enrichers;
|
private final Set<AccessTokenEnricher> enrichers;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public JwtAccessTokenBuilderFactory(
|
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.keyGenerator = keyGenerator;
|
||||||
this.keyResolver = keyResolver;
|
this.keyResolver = keyResolver;
|
||||||
this.enrichers = enrichers;
|
this.enrichers = enrichers;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtAccessTokenBuilder create() {
|
public JwtAccessTokenBuilder create() {
|
||||||
JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver);
|
JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver, clock);
|
||||||
|
|
||||||
// enrich access token builder
|
// enrich access token builder
|
||||||
enrichers.forEach((enricher) -> {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,6 @@ import sonia.scm.user.UserDAO;
|
|||||||
import sonia.scm.user.UserTestData;
|
import sonia.scm.user.UserTestData;
|
||||||
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -71,6 +70,7 @@ import static org.junit.Assert.assertThat;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.any;
|
import static org.mockito.Mockito.any;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link BearerRealm}.
|
* Unit tests for {@link BearerRealm}.
|
||||||
@@ -256,12 +256,6 @@ private String createCompactToken(String subject, SecureKey key) {
|
|||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecureKey createSecureKey() {
|
|
||||||
byte[] bytes = new byte[32];
|
|
||||||
random.nextBytes(bytes);
|
|
||||||
return new SecureKey(bytes, System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resolveKey(SecureKey key) {
|
private void resolveKey(SecureKey key) {
|
||||||
when(
|
when(
|
||||||
keyResolver.resolveSigningKey(
|
keyResolver.resolveSigningKey(
|
||||||
@@ -272,16 +266,13 @@ private String createCompactToken(String subject, SecureKey key) {
|
|||||||
.thenReturn(
|
.thenReturn(
|
||||||
new SecretKeySpec(
|
new SecretKeySpec(
|
||||||
key.getBytes(),
|
key.getBytes(),
|
||||||
SignatureAlgorithm.HS256.getValue()
|
SignatureAlgorithm.HS256.getJcaName()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final SecureRandom random = new SecureRandom();
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private DAORealmHelperFactory helperFactory;
|
private DAORealmHelperFactory helperFactory;
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@@ -56,6 +55,7 @@ import static org.junit.Assert.assertThat;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.anyString;
|
import static org.mockito.Mockito.anyString;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link JwtAccessTokenBuilder}.
|
* Unit test for {@link JwtAccessTokenBuilder}.
|
||||||
@@ -162,11 +162,4 @@ public class JwtAccessTokenBuilderTest {
|
|||||||
assertEquals("b", token.getCustom("a").get());
|
assertEquals("b", token.getCustom("a").get());
|
||||||
assertEquals("[\"repo:*\"]", token.getScope().toString());
|
assertEquals("[\"repo:*\"]", token.getScope().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecureKey createSecureKey() {
|
|
||||||
byte[] bytes = new byte[32];
|
|
||||||
new Random().nextBytes(bytes);
|
|
||||||
return new SecureKey(bytes, System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import java.sql.Date;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.time.Duration.ofMinutes;
|
||||||
|
import static java.time.temporal.ChronoUnit.SECONDS;
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
|
||||||
|
|
||||||
|
@SubjectAware(
|
||||||
|
username = "user",
|
||||||
|
password = "secret",
|
||||||
|
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||||
|
)
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class JwtAccessTokenRefresherTest {
|
||||||
|
|
||||||
|
private static final Instant NOW = Instant.now().truncatedTo(SECONDS);
|
||||||
|
private static final Instant TOKEN_CREATION = NOW.minus(ofMinutes(1));
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SecureKeyResolver keyResolver;
|
||||||
|
@Mock
|
||||||
|
private JwtAccessTokenRefreshStrategy refreshStrategy;
|
||||||
|
@Mock
|
||||||
|
private Clock refreshClock;
|
||||||
|
|
||||||
|
private KeyGenerator keyGenerator = () -> "key";
|
||||||
|
|
||||||
|
private JwtAccessTokenRefresher refresher;
|
||||||
|
private JwtAccessTokenBuilder tokenBuilder;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initKeyResolver() {
|
||||||
|
when(keyResolver.getSecureKey(any())).thenReturn(createSecureKey());
|
||||||
|
|
||||||
|
Clock creationClock = mock(Clock.class);
|
||||||
|
when(creationClock.instant()).thenReturn(TOKEN_CREATION);
|
||||||
|
tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet(), creationClock).create();
|
||||||
|
|
||||||
|
JwtAccessTokenBuilderFactory refreshBuilderFactory = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet(), refreshClock);
|
||||||
|
refresher = new JwtAccessTokenRefresher(refreshBuilderFactory, refreshStrategy, refreshClock);
|
||||||
|
when(refreshClock.instant()).thenReturn(NOW);
|
||||||
|
when(refreshStrategy.shouldBeRefreshed(any())).thenReturn(true);
|
||||||
|
|
||||||
|
// set default expiration values
|
||||||
|
tokenBuilder
|
||||||
|
.expiresIn(5, MINUTES)
|
||||||
|
.refreshableFor(10, MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotRefreshTokenWithDisabledRefresh() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder
|
||||||
|
.refreshableFor(0, MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
assertThat(refreshedToken).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotRefreshTokenWhenTokenExpired() {
|
||||||
|
Instant afterNormalExpiration = NOW.plus(ofMinutes(6));
|
||||||
|
when(refreshClock.instant()).thenReturn(afterNormalExpiration);
|
||||||
|
JwtAccessToken oldToken = tokenBuilder.build();
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
assertThat(refreshedToken).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotRefreshTokenWhenRefreshExpired() {
|
||||||
|
Instant afterRefreshExpiration = Instant.now().plus(ofMinutes(2));
|
||||||
|
when(refreshClock.instant()).thenReturn(afterRefreshExpiration);
|
||||||
|
JwtAccessToken oldToken = tokenBuilder
|
||||||
|
.refreshableFor(1, MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
assertThat(refreshedToken).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotRefreshTokenWhenStrategyDoesNotSaySo() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder.build();
|
||||||
|
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(false);
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
assertThat(refreshedToken).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRefreshTokenWithParentId() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder.build();
|
||||||
|
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true);
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedTokenResult = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
assertThat(refreshedTokenResult).isNotEmpty();
|
||||||
|
JwtAccessToken refreshedToken = refreshedTokenResult.get();
|
||||||
|
assertThat(refreshedToken.getParentKey()).get().isEqualTo("key");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRefreshTokenWithSameExpiration() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder.build();
|
||||||
|
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true);
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedTokenResult = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
assertThat(refreshedTokenResult).isNotEmpty();
|
||||||
|
JwtAccessToken refreshedToken = refreshedTokenResult.get();
|
||||||
|
assertThat(refreshedToken.getExpiration()).isEqualTo(Date.from(NOW.plus(ofMinutes(5))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRefreshTokenWithSameRefreshExpiration() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder.build();
|
||||||
|
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true);
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedTokenResult = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
assertThat(refreshedTokenResult).isNotEmpty();
|
||||||
|
JwtAccessToken refreshedToken = refreshedTokenResult.get();
|
||||||
|
assertThat(refreshedToken.getRefreshExpiration()).get().isEqualTo(Date.from(TOKEN_CREATION.plus(ofMinutes(10))));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,8 @@ import org.junit.runner.RunWith;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
|
||||||
|
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,12 +216,6 @@ public class JwtAccessTokenResolverTest {
|
|||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecureKey createSecureKey() {
|
|
||||||
byte[] bytes = new byte[32];
|
|
||||||
random.nextBytes(bytes);
|
|
||||||
return new SecureKey(bytes, System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resolveKey(SecureKey key) {
|
private void resolveKey(SecureKey key) {
|
||||||
when(
|
when(
|
||||||
keyResolver.resolveSigningKey(
|
keyResolver.resolveSigningKey(
|
||||||
@@ -230,7 +226,7 @@ public class JwtAccessTokenResolverTest {
|
|||||||
.thenReturn(
|
.thenReturn(
|
||||||
new SecretKeySpec(
|
new SecretKeySpec(
|
||||||
key.getBytes(),
|
key.getBytes(),
|
||||||
SignatureAlgorithm.HS256.getValue()
|
SignatureAlgorithm.HS256.getJcaName()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static java.time.temporal.ChronoUnit.MINUTES;
|
||||||
|
import static java.time.temporal.ChronoUnit.SECONDS;
|
||||||
|
import static java.util.concurrent.TimeUnit.HOURS;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
|
||||||
|
|
||||||
|
@SubjectAware(
|
||||||
|
username = "user",
|
||||||
|
password = "secret",
|
||||||
|
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||||
|
)
|
||||||
|
public class PercentageJwtAccessTokenRefreshStrategyTest {
|
||||||
|
|
||||||
|
private static final Instant TOKEN_CREATION = Instant.now().truncatedTo(SECONDS);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
|
private KeyGenerator keyGenerator = () -> "key";
|
||||||
|
|
||||||
|
private Clock refreshClock = mock(Clock.class);
|
||||||
|
|
||||||
|
private JwtAccessTokenBuilder tokenBuilder;
|
||||||
|
private PercentageJwtAccessTokenRefreshStrategy refreshStrategy;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initToken() {
|
||||||
|
SecureKeyResolver keyResolver = mock(SecureKeyResolver.class);
|
||||||
|
when(keyResolver.getSecureKey(any())).thenReturn(createSecureKey());
|
||||||
|
|
||||||
|
Clock creationClock = mock(Clock.class);
|
||||||
|
when(creationClock.instant()).thenReturn(TOKEN_CREATION);
|
||||||
|
|
||||||
|
tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet(), creationClock).create();
|
||||||
|
tokenBuilder.expiresIn(1, HOURS);
|
||||||
|
tokenBuilder.refreshableFor(1, HOURS);
|
||||||
|
|
||||||
|
refreshStrategy = new PercentageJwtAccessTokenRefreshStrategy(refreshClock, 0.5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotRefreshWhenTokenIsYoung() {
|
||||||
|
when(refreshClock.instant()).thenReturn(TOKEN_CREATION.plus(29, MINUTES));
|
||||||
|
assertThat(refreshStrategy.shouldBeRefreshed(tokenBuilder.build())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRefreshWhenTokenIsOld() {
|
||||||
|
when(refreshClock.instant()).thenReturn(TOKEN_CREATION.plus(31, MINUTES));
|
||||||
|
assertThat(refreshStrategy.shouldBeRefreshed(tokenBuilder.build())).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public class SecureKeyTestUtil {
|
||||||
|
public static SecureKey createSecureKey() {
|
||||||
|
byte[] bytes = new byte[32];
|
||||||
|
new SecureRandom().nextBytes(bytes);
|
||||||
|
return new SecureKey(bytes, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package sonia.scm.web.security;
|
||||||
|
|
||||||
|
import org.apache.shiro.authc.AuthenticationToken;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
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 javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static java.util.Collections.singleton;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith({MockitoExtension.class})
|
||||||
|
class TokenRefreshFilterTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Set<WebTokenGenerator> tokenGenerators;
|
||||||
|
@Mock
|
||||||
|
private WebTokenGenerator tokenGenerator;
|
||||||
|
@Mock
|
||||||
|
private JwtAccessTokenRefresher refresher;
|
||||||
|
@Mock
|
||||||
|
private AccessTokenResolver resolver;
|
||||||
|
@Mock
|
||||||
|
private AccessTokenCookieIssuer issuer;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private TokenRefreshFilter filter;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
@Mock
|
||||||
|
private HttpServletResponse response;
|
||||||
|
@Mock
|
||||||
|
private FilterChain filterChain;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void initGenerators() {
|
||||||
|
when(tokenGenerators.iterator()).thenReturn(singleton(tokenGenerator).iterator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContinueChain() throws IOException, ServletException {
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
verify(issuer, never()).authenticate(any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRefreshNonBearerToken() throws IOException, ServletException {
|
||||||
|
AuthenticationToken token = mock(AuthenticationToken.class);
|
||||||
|
when(tokenGenerator.createToken(request)).thenReturn(token);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verify(issuer, never()).authenticate(any(), any(), any());
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRefreshNonJwtToken() throws IOException, ServletException {
|
||||||
|
BearerToken token = mock(BearerToken.class);
|
||||||
|
JwtAccessToken jwtToken = mock(JwtAccessToken.class);
|
||||||
|
when(tokenGenerator.createToken(request)).thenReturn(token);
|
||||||
|
when(resolver.resolve(token)).thenReturn(jwtToken);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verify(issuer, never()).authenticate(any(), any(), any());
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRefreshIfRefreshable() throws IOException, ServletException {
|
||||||
|
BearerToken token = mock(BearerToken.class);
|
||||||
|
JwtAccessToken jwtToken = mock(JwtAccessToken.class);
|
||||||
|
JwtAccessToken newJwtToken = mock(JwtAccessToken.class);
|
||||||
|
when(tokenGenerator.createToken(request)).thenReturn(token);
|
||||||
|
when(resolver.resolve(token)).thenReturn(jwtToken);
|
||||||
|
when(refresher.refresh(jwtToken)).thenReturn(of(newJwtToken));
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verify(issuer).authenticate(request, response, newJwtToken);
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ dent = secret, creator, heartOfGold, puzzle42
|
|||||||
unpriv = secret
|
unpriv = secret
|
||||||
crato = secret, creator
|
crato = secret, creator
|
||||||
community = secret, oss
|
community = secret, oss
|
||||||
|
user = secret, user
|
||||||
|
|
||||||
[roles]
|
[roles]
|
||||||
admin = *
|
admin = *
|
||||||
@@ -11,3 +12,4 @@ creator = repository:create
|
|||||||
heartOfGold = "repository:read,modify,delete:hof"
|
heartOfGold = "repository:read,modify,delete:hof"
|
||||||
puzzle42 = "repository:read,write:p42"
|
puzzle42 = "repository:read,write:p42"
|
||||||
oss = "repository:pull"
|
oss = "repository:pull"
|
||||||
|
user = *
|
||||||
|
|||||||
Reference in New Issue
Block a user