Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
top.dcenter.ums.security.jwt.decoder.UmsNimbusJwtDecoder Maven / Gradle / Ivy
Go to download
ums-jwt feature: JWT 创建(通过接口自定义 Claims, 通过配置设置算法等), 校验(通过接口自定义校验规则),
刷新(自动刷新, 直接拒绝, 通过 refreshToken 刷新), 刷新的 JWT 使旧 JWT 失效引发的并发访问问题及黑名单.
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.dcenter.ums.security.jwt.decoder;
import com.nimbusds.jose.Header;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.PlainHeader;
import com.nimbusds.jose.RemoteKeySourceException;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
import com.nimbusds.jose.jwk.source.JWKSetCache;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.proc.SingleKeyJWSKeySelector;
import com.nimbusds.jose.util.Resource;
import com.nimbusds.jose.util.ResourceRetriever;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.JwtValidationException;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import top.dcenter.ums.security.common.enums.ErrorCodeEnum;
import top.dcenter.ums.security.jwt.JwtContext;
import top.dcenter.ums.security.jwt.api.endpoind.service.JwkSetUriConfig;
import top.dcenter.ums.security.jwt.api.validator.service.ReAuthService;
import top.dcenter.ums.security.jwt.enums.JwtRefreshHandlerPolicy;
import top.dcenter.ums.security.jwt.exception.JwtInvalidException;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNull;
import static top.dcenter.ums.security.common.utils.ReflectionUtil.invokeToJsonObjectMethod;
import static top.dcenter.ums.security.core.mdc.utils.MdcUtil.getMdcTraceId;
import static top.dcenter.ums.security.jwt.JwtContext.getClockSkew;
/**
* 1. 增加 jwt 刷新的处理:
* - 因刷新 jwt, 用 oldJwt 失效而添加进黑名单, 值为 newJwt, 此时如果有携带 oldJwt 并发访问请求时,
* 会自动替换 oldJwt 为 newJwt.
* 2. 增加是否需要重新登录认证处理.
* 3. 增加只针对 refreshToken 的 decode 方法, 以提高效率.
* 4. 增加只针对 刚刷新的 Jwt 无校验的 decode 方法, 以提高效率.
*
* A low-level Nimbus implementation of {@link JwtDecoder} which takes a raw Nimbus
* configuration.
*
* @author Josh Cummings
* @author Joe Grandja
* @author Mykyta Bezverkhyi
* @author YongWu zheng
* @since 5.2
*/
@SuppressWarnings("unused")
@Slf4j
public final class UmsNimbusJwtDecoder implements JwtDecoder {
private static final String DECODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to decode the Jwt: %s";
/**
* {@link Header#toJSONObject()} 的 {@link Method}, 增加对 nimbus-jose-jwt:9.x.x/8.x.x 的兼容性.
*/
private static final Method TO_JSON_OBJECT_METHOD_FOR_PLAIN = ReflectionUtils.findMethod(PlainHeader.class, "toJSONObject");
/**
* {@link Header#toJSONObject()} 的 {@link Method}, 增加对 nimbus-jose-jwt:9.x.x/8.x.x 的兼容性.
*/
private static final Method TO_JSON_OBJECT_METHOD_FOR_JWE = ReflectionUtils.findMethod(JWEHeader.class,
"toJSONObject");
/**
* {@link Header#toJSONObject()} 的 {@link Method}, 增加对 nimbus-jose-jwt:9.x.x/8.x.x 的兼容性.
*/
private static final Method TO_JSON_OBJECT_METHOD_FOR_JWS = ReflectionUtils.findMethod(JWSHeader.class,
"toJSONObject");
private final JWTProcessor jwtProcessor;
private final JwtRefreshHandlerPolicy refreshHandlerPolicy;
/**
* 当 {@link JwtRefreshHandlerPolicy#AUTO_RENEW} 时, JWT 剩余的有效期间隔小于此值后自动刷新 JWT;
* 当 {@link JwtRefreshHandlerPolicy#REFRESH_TOKEN} 时, JWT 剩余的有效期间隔小于此值后通过 refreshToken 才会刷新新的 JWT,
* 否则直接返回旧的 JWT.
*/
@Getter
private final Duration remainingRefreshInterval;
private final Boolean isReAuth;
private Converter, Map> claimSetConverter = MappedJwtClaimSetConverter
.withDefaults(Collections.emptyMap());
private OAuth2TokenValidator jwtValidator = JwtValidators.createDefault();
@Setter
private ReAuthService reAuthService;
/**
* Configures a {@link UmsNimbusJwtDecoder} with the given parameters
* @param jwtProcessor the {@link JWTProcessor} to use
* @param refreshHandlerPolicy {@link Jwt} 刷新处理策略
* @param remainingRefreshInterval JWT 剩余的有效期间隔小于此值后自动刷新 JWT, 此配置在 {@link JwtRefreshHandlerPolicy#AUTO_RENEW} 时有效
* @param isReAuth 是否需要重新认证检查
*/
public UmsNimbusJwtDecoder(JWTProcessor jwtProcessor,
JwtRefreshHandlerPolicy refreshHandlerPolicy,
Duration remainingRefreshInterval, Boolean isReAuth) {
Assert.notNull(jwtProcessor, "jwtProcessor cannot be null");
Assert.notNull(refreshHandlerPolicy, "refreshHandlerPolicy cannot be null");
Assert.notNull(remainingRefreshInterval, "remainingRefreshInterval cannot be null");
Assert.notNull(isReAuth, "isReAuth cannot be null");
this.refreshHandlerPolicy = refreshHandlerPolicy;
this.remainingRefreshInterval = remainingRefreshInterval;
this.jwtProcessor = jwtProcessor;
this.isReAuth = isReAuth;
}
/**
* Use this {@link Jwt} Validator
* @param jwtValidator - the Jwt Validator to use
*/
public void setJwtValidator(OAuth2TokenValidator jwtValidator) {
Assert.notNull(jwtValidator, "jwtValidator cannot be null");
this.jwtValidator = jwtValidator;
}
/**
* Use the following {@link Converter} for manipulating the JWT's claim set
* @param claimSetConverter the {@link Converter} to use
*/
public void setClaimSetConverter(Converter, Map> claimSetConverter) {
Assert.notNull(claimSetConverter, "claimSetConverter cannot be null");
this.claimSetConverter = claimSetConverter;
}
/**
* Decode and validate and refresh the JWT from its compact claims representation format
* @param token the JWT value
* @return a validated {@link Jwt}
* @throws JwtException JwtException
*/
@Override
public Jwt decode(String token) throws JwtException {
JWT jwt = parse(token);
if (jwt instanceof PlainJWT) {
log.trace("Failed to decode unsigned token");
throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm());
}
Jwt createdJwt = createJwt(token, jwt);
// AUTO_RENEW 策略时 前置校验
if (JwtRefreshHandlerPolicy.AUTO_RENEW.equals(this.refreshHandlerPolicy)) {
createdJwt = validateJwt(createdJwt);
}
if (refreshHandlerPolicy.isRefresh(createdJwt, remainingRefreshInterval,
getClockSkew(), reAuthService, isReAuth)) {
createdJwt = refreshHandlerPolicy.refreshHandle(createdJwt, this);
}
// 不是 AUTO_RENEW 策略时 后置校验
if (!JwtRefreshHandlerPolicy.AUTO_RENEW.equals(this.refreshHandlerPolicy)) {
createdJwt = validateJwt(createdJwt);
}
return createdJwt;
}
/**
* Decode and validate the JWT from its compact claims representation format
* @param token the JWT value
* @return a validated {@link Jwt}
* @throws JwtException JwtException
*/
public Jwt decodeNotRefreshToken(String token) throws JwtException {
JWT jwt = parse(token);
if (jwt instanceof PlainJWT) {
log.trace("Failed to decode unsigned token");
throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm());
}
Jwt createdJwt = createJwt(token, jwt);
createdJwt = validateJwt(createdJwt);
return createdJwt;
}
/**
* Decode and not validate the JWT from its compact claims representation format
* @param token the JWT value
* @return a not validated {@link Jwt}
* @throws JwtInvalidException 转换 token 到 Jwt 时发生错误
*/
public Jwt decodeNotValidate(String token) throws JwtInvalidException {
try {
JWT parsedJwt = JWTParser.parse(token);
// Verify the signature
JWTClaimsSet jwtClaimsSet = parsedJwt.getJWTClaimsSet();
// 增加对 nimbus-jose-jwt:9.x.x/8.x.x 的兼容性.
Map headers = invokeToJsonObjectMethod(parsedJwt.getHeader(),
getToJsonObjectMethod(parsedJwt));
Map claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims());
requireNonNull(claims, "转换 jwtClaimsSet 到 claims 是返回 null 值");
// @formatter:off
return Jwt.withTokenValue(token)
.headers((h) -> h.putAll(headers))
.claims((c) -> c.putAll(claims))
.build();
// @formatter:on
}
catch (Exception ex) {
log.error("转换 token 到 Jwt 时发生错误", ex);
throw new JwtInvalidException(ErrorCodeEnum.JWT_INVALID, getMdcTraceId());
}
}
/**
* Decode and validate the refresh token JWT from its compact claims representation format
* @param token the JWT value
* @return a validated {@link Jwt}
* @throws JwtInvalidException 转换 token 到 Jwt 时发生错误
*/
public Jwt decodeRefreshTokenOfJwt(String token) throws JwtInvalidException {
try {
JWT parsedJwt = JWTParser.parse(token);
// Verify the signature
JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
// 增加对 nimbus-jose-jwt:9.x.x/8.x.x 的兼容性.
Map headers = invokeToJsonObjectMethod(parsedJwt.getHeader(),
getToJsonObjectMethod(parsedJwt));
Map claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims());
requireNonNull(claims, "转换 jwtClaimsSet 到 claims 是返回 null 值");
// @formatter:off
Jwt createJwt = Jwt.withTokenValue(token)
.headers((h) -> h.putAll(headers))
.claims((c) -> c.putAll(claims))
.build();
// @formatter:on
Instant expiresAt = createJwt.getExpiresAt();
if (nonNull(expiresAt) && Instant.now().minusSeconds(getClockSkew().getSeconds()).isAfter(expiresAt)) {
throw new JwtInvalidException(ErrorCodeEnum.JWT_INVALID, getMdcTraceId());
}
// 检查是否在黑名单中.
if (JwtContext.isRefreshJwtInTheBlacklist(createJwt)) {
throw new JwtInvalidException(ErrorCodeEnum.JWT_REFRESH_TOKEN_INVALID, getMdcTraceId());
}
if (reAuthService.isReAuth(createJwt)) {
JwtContext.addBlacklistForRefreshToken(createJwt);
throw new JwtInvalidException(ErrorCodeEnum.JWT_RE_AUTH, getMdcTraceId());
}
return createJwt;
}
catch (JwtInvalidException ex) {
log.error("因需要重新认证, refresh token 失效", ex);
throw ex;
}
catch (Exception ex) {
log.error("转换 token 到 Jwt 时发生错误", ex);
throw new JwtInvalidException(ErrorCodeEnum.JWT_INVALID, getMdcTraceId());
}
}
private JWT parse(String token) {
try {
return JWTParser.parse(token);
}
catch (Exception ex) {
log.trace("Failed to parse token", ex);
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
private Jwt createJwt(String token, JWT parsedJwt) {
try {
// Verify the signature
JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
// 增加对 nimbus-jose-jwt:9.x.x/8.x.x 的兼容性.
Map headers = invokeToJsonObjectMethod(parsedJwt.getHeader(),
getToJsonObjectMethod(parsedJwt));
Map claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims());
// @formatter:off
//noinspection ConstantConditions
return Jwt.withTokenValue(token)
.headers((h) -> h.putAll(headers))
.claims((c) -> c.putAll(claims))
.build();
// @formatter:on
}
catch (RemoteKeySourceException ex) {
log.trace("Failed to retrieve JWK set", ex);
if (ex.getCause() instanceof ParseException) {
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set"));
}
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
catch (JOSEException ex) {
log.trace("Failed to process JWT", ex);
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
catch (Exception ex) {
log.trace("Failed to process JWT", ex);
if (ex.getCause() instanceof ParseException) {
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed payload"));
}
throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
@NonNull
private static Method getToJsonObjectMethod(@NonNull JWT jwt) {
if (jwt instanceof SignedJWT)
{
return TO_JSON_OBJECT_METHOD_FOR_JWS;
}
if (jwt instanceof EncryptedJWT)
{
return TO_JSON_OBJECT_METHOD_FOR_JWE;
}
return TO_JSON_OBJECT_METHOD_FOR_PLAIN;
}
private Jwt validateJwt(Jwt jwt) {
// 检查黑名单中是否有刷新 Jwt , 有的话替换.
Jwt refreshJwt = validateJti(jwt);
if (!Objects.equals(refreshJwt, jwt)) {
return refreshJwt;
}
OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
if (result.hasErrors()) {
Collection errors = result.getErrors();
String validationErrorString = getJwtValidationExceptionMessage(errors);
throw new JwtValidationException(validationErrorString, errors);
}
return jwt;
}
/**
* 校验 jwt 的 jti 是否存在黑名单中, 如果存在且带有刷新后的 jwt, 返回刷新后的 jwt.
* @param jwt 需要校验的 jwt
* @return 原样返回或返回刷新后的 jwt.
*/
private Jwt validateJti(Jwt jwt) {
// 1. 校验 jwt 是否在黑名单, 是否需要重新认证(reAuth)
JwtContext.BlacklistType blacklistType = JwtContext.jtiInTheBlacklist(jwt);
try {
/* - newJwtString == null 表示不在黑名单中,
- newJwtString != null 表示在黑名单中, 但缓存中存储有新的且有效 JWT 则返回新的 jwt 字符串,
- 抛出异常表示在黑名单中, 但缓存中没有新的 jwt 字符串, 则需要重新认证.
*/
String newJwtString = JwtContext.inBlacklistAndHasNewJwt(blacklistType);
if (nonNull(newJwtString)) {
// 在黑名单中, 但缓存中存储有新的且有效 JWT, 返回此 JWT
return decodeNotValidate(newJwtString);
}
// jwt 有效, 原样返回
return jwt;
}
catch (JwtInvalidException e) {
OAuth2TokenValidatorResult result =
OAuth2TokenValidatorResult.failure(
new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
"The " + JwtClaimNames.JTI + " claim is not valid",
null)
);
Collection errors = result.getErrors();
String validationErrorString = getJwtValidationExceptionMessage(errors);
throw new JwtValidationException(validationErrorString, errors);
}
}
private String getJwtValidationExceptionMessage(Collection errors) {
for (OAuth2Error oAuth2Error : errors) {
if (!StringUtils.isEmpty(oAuth2Error.getDescription())) {
return String.format(DECODING_ERROR_MESSAGE_TEMPLATE, oAuth2Error.getDescription());
}
}
return "Unable to validate Jwt";
}
/**
* Use the given JWK Set
* uri.
* @param jwkSetUri the JWK Set uri to use
* @param refreshHandlerPolicy {@link Jwt} 刷新处理策略
* @param remainingRefreshInterval JWT 剩余的有效期间隔小于此值后自动刷新 JWT, 此配置在 {@link JwtRefreshHandlerPolicy#AUTO_RENEW} 时有效
* @param jwkSetUriConfig 用于从 jwk set uri 获取 JWk 时传递 header 的参数
* @param isReAuth 是否需要重新认证检查
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
*/
public static JwkSetUriJwtDecoderBuilder withJwkSetUri(String jwkSetUri,
JwtRefreshHandlerPolicy refreshHandlerPolicy,
Duration remainingRefreshInterval,
@Nullable JwkSetUriConfig jwkSetUriConfig,
Boolean isReAuth) {
return new JwkSetUriJwtDecoderBuilder(jwkSetUri, refreshHandlerPolicy,
remainingRefreshInterval,
jwkSetUriConfig,
isReAuth);
}
/**
* Use the given public key to validate JWTs
* @param key the public key to use
* @param refreshHandlerPolicy {@link Jwt} 刷新处理策略
* @param remainingRefreshInterval JWT 剩余的有效期间隔小于此值后自动刷新 JWT, 此配置在 {@link JwtRefreshHandlerPolicy#AUTO_RENEW} 时有效
* @param isReAuth 是否需要重新认证检查
* @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
*/
public static PublicKeyJwtDecoderBuilder withPublicKey(RSAPublicKey key,
JwtRefreshHandlerPolicy refreshHandlerPolicy,
Duration remainingRefreshInterval,
Boolean isReAuth) {
return new PublicKeyJwtDecoderBuilder(key, refreshHandlerPolicy, remainingRefreshInterval, isReAuth);
}
/**
* Use the given {@code SecretKey} to validate the MAC on a JSON Web Signature (JWS).
* @param secretKey the {@code SecretKey} used to validate the MAC
* @param refreshHandlerPolicy {@link Jwt} 刷新处理策略
* @param remainingRefreshInterval JWT 剩余的有效期间隔小于此值后自动刷新 JWT, 此配置在 {@link JwtRefreshHandlerPolicy#AUTO_RENEW} 时有效
* @param isReAuth 是否需要重新认证检查
* @return a {@link SecretKeyJwtDecoderBuilder} for further configurations
*/
public static SecretKeyJwtDecoderBuilder withSecretKey(SecretKey secretKey,
JwtRefreshHandlerPolicy refreshHandlerPolicy,
Duration remainingRefreshInterval,
Boolean isReAuth) {
return new SecretKeyJwtDecoderBuilder(secretKey, refreshHandlerPolicy, remainingRefreshInterval, isReAuth);
}
/**
* A builder for creating {@link UmsNimbusJwtDecoder} instances based on a
* JWK Set
* uri.
*/
public static final class JwkSetUriJwtDecoderBuilder {
private final String jwkSetUri;
private final JwtRefreshHandlerPolicy refreshHandlerPolicy;
private final Duration remainingRefreshInterval;
private final JwkSetUriConfig jwkSetUriConfig;
private final Boolean isReAuth;
@SuppressWarnings("FieldMayBeFinal")
private Set signatureAlgorithms = new HashSet<>();
private RestOperations restOperations = new RestTemplate();
private Cache cache;
private Consumer> jwtProcessorCustomizer;
private JwkSetUriJwtDecoderBuilder(String jwkSetUri,
JwtRefreshHandlerPolicy refreshHandlerPolicy,
Duration remainingRefreshInterval,
@Nullable JwkSetUriConfig jwkSetUriConfig,
Boolean isReAuth) {
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
Assert.notNull(refreshHandlerPolicy, "refreshHandlerPolicy cannot be null");
Assert.notNull(remainingRefreshInterval, "remainingRefreshInterval cannot be null");
Assert.notNull(isReAuth, "isReAuth cannot be null");
this.jwkSetUri = jwkSetUri;
this.refreshHandlerPolicy = refreshHandlerPolicy;
this.remainingRefreshInterval = remainingRefreshInterval;
this.jwkSetUriConfig = jwkSetUriConfig;
this.isReAuth = isReAuth;
this.jwtProcessorCustomizer = (processor) -> {
};
}
/**
* Append the given signing
* algorithm to the set of algorithms to use.
* @param signatureAlgorithm the algorithm to use
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
*/
public JwkSetUriJwtDecoderBuilder jwsAlgorithm(SignatureAlgorithm signatureAlgorithm) {
Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
this.signatureAlgorithms.add(signatureAlgorithm);
return this;
}
/**
* Configure the list of
* algorithms to use with the given {@link Consumer}.
* @param signatureAlgorithmsConsumer a {@link Consumer} for further configuring
* the algorithm list
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
*/
public JwkSetUriJwtDecoderBuilder jwsAlgorithms(Consumer> signatureAlgorithmsConsumer) {
Assert.notNull(signatureAlgorithmsConsumer, "signatureAlgorithmsConsumer cannot be null");
signatureAlgorithmsConsumer.accept(this.signatureAlgorithms);
return this;
}
/**
* Use the given {@link RestOperations} to coordinate with the authorization
* servers indicated in the
* JWK Set uri as well
* as the Issuer .
* @param restOperations {@link RestOperations}
* @return {@link JwkSetUriJwtDecoderBuilder}
*/
public JwkSetUriJwtDecoderBuilder restOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
return this;
}
/**
* Use the given {@link Cache} to store
* JWK Set .
* @param cache the {@link Cache} to be used to store JWK Set
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
* @since 5.4
*/
public JwkSetUriJwtDecoderBuilder cache(Cache cache) {
Assert.notNull(cache, "cache cannot be null");
this.cache = cache;
return this;
}
/**
* Use the given {@link Consumer} to customize the {@link JWTProcessor
* ConfigurableJWTProcessor} before passing it to the build
* {@link UmsNimbusJwtDecoder}.
* @param jwtProcessorCustomizer the callback used to alter the processor
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
* @since 5.4
*/
public JwkSetUriJwtDecoderBuilder jwtProcessorCustomizer(
Consumer> jwtProcessorCustomizer) {
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
this.jwtProcessorCustomizer = jwtProcessorCustomizer;
return this;
}
JWSKeySelector jwsKeySelector(JWKSource jwkSource) {
if (this.signatureAlgorithms.isEmpty()) {
return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
}
Set jwsAlgorithms = new HashSet<>();
for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
jwsAlgorithms.add(jwsAlgorithm);
}
return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
}
JWKSource jwkSource(ResourceRetriever jwkSetRetriever) {
if (this.cache == null) {
return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever);
}
ResourceRetriever cachingJwkSetRetriever = new CachingResourceRetriever(this.cache, jwkSetRetriever);
if (nonNull(this.jwkSetUriConfig)) {
return new RemoteJWKSet<>(toURL(this.jwkSetUri), cachingJwkSetRetriever,
new DefaultJWKSetCache(this.jwkSetUriConfig.lifespan(),
this.jwkSetUriConfig.refreshTime(),
this.jwkSetUriConfig.timeUnit()));
}
else {
return new RemoteJWKSet<>(toURL(this.jwkSetUri), cachingJwkSetRetriever, new DefaultJWKSetCache());
}
}
JWTProcessor processor() {
ResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations,
this.jwkSetUriConfig);
JWKSource jwkSource = jwkSource(jwkSetRetriever);
ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
this.jwtProcessorCustomizer.accept(jwtProcessor);
return jwtProcessor;
}
/**
* Build the configured {@link UmsNimbusJwtDecoder}.
* @return the configured {@link UmsNimbusJwtDecoder}
*/
public UmsNimbusJwtDecoder build() {
return new UmsNimbusJwtDecoder(processor(), refreshHandlerPolicy, remainingRefreshInterval, isReAuth);
}
@SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
private static URL toURL(String url) {
try {
return new URL(url);
}
catch (MalformedURLException ex) {
throw new IllegalArgumentException("Invalid JWK Set URL \"" + url + "\" : " + ex.getMessage(), ex);
}
}
private static class NoOpJwkSetCache implements JWKSetCache {
@Override
public void put(JWKSet jwkSet) {
}
@Override
public JWKSet get() {
return null;
}
@Override
public boolean requiresRefresh() {
return true;
}
}
private static class CachingResourceRetriever implements ResourceRetriever {
private final Cache cache;
private final ResourceRetriever resourceRetriever;
CachingResourceRetriever(Cache cache, ResourceRetriever resourceRetriever) {
this.cache = cache;
this.resourceRetriever = resourceRetriever;
}
@Override
public Resource retrieveResource(URL url) throws IOException {
try {
String jwkSet = this.cache.get(url.toString(),
() -> this.resourceRetriever.retrieveResource(url).getContent());
//noinspection ConstantConditions
return new Resource(jwkSet, "UTF-8");
}
catch (Cache.ValueRetrievalException ex) {
Throwable thrownByValueLoader = ex.getCause();
if (thrownByValueLoader instanceof IOException) {
throw (IOException) thrownByValueLoader;
}
throw new IOException(thrownByValueLoader);
}
catch (Exception ex) {
throw new IOException(ex);
}
}
}
private static class RestOperationsResourceRetriever implements ResourceRetriever {
private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json");
private final RestOperations restOperations;
private final Map headers;
RestOperationsResourceRetriever(RestOperations restOperations,
@Nullable JwkSetUriConfig jwkSetUriConfig) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
if (nonNull(jwkSetUriConfig)) {
this.headers = jwkSetUriConfig.headers();
}
else {
this.headers = null;
}
}
@Override
public Resource retrieveResource(URL url) throws IOException {
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
if (nonNull(this.headers)) {
this.headers.forEach((key, value) -> httpHeaders.set(key, (String) value));
}
ResponseEntity response = getResponse(url, httpHeaders);
if (response.getStatusCodeValue() != 200) {
throw new IOException(response.toString());
}
//noinspection ConstantConditions
return new Resource(response.getBody(), "UTF-8");
}
private ResponseEntity getResponse(URL url, HttpHeaders headers) throws IOException {
try {
RequestEntity request = new RequestEntity<>(headers, HttpMethod.GET, url.toURI());
return this.restOperations.exchange(request, String.class);
}
catch (Exception ex) {
throw new IOException(ex);
}
}
}
}
/**
* A builder for creating {@link UmsNimbusJwtDecoder} instances based on a public key.
*/
public static final class PublicKeyJwtDecoderBuilder {
private JWSAlgorithm jwsAlgorithm;
private final RSAPublicKey key;
private final JwtRefreshHandlerPolicy refreshHandlerPolicy;
private final Duration remainingRefreshInterval;
private final Boolean isReAuth;
private Consumer> jwtProcessorCustomizer;
private PublicKeyJwtDecoderBuilder(RSAPublicKey key,
JwtRefreshHandlerPolicy refreshHandlerPolicy,
Duration remainingRefreshInterval,
Boolean isReAuth) {
Assert.notNull(key, "key cannot be null");
Assert.notNull(refreshHandlerPolicy, "refreshHandlerPolicy cannot be null");
Assert.notNull(remainingRefreshInterval, "remainingRefreshInterval cannot be null");
Assert.notNull(isReAuth, "isReAuth cannot be null");
this.jwsAlgorithm = JWSAlgorithm.RS256;
this.key = key;
this.refreshHandlerPolicy = refreshHandlerPolicy;
this.remainingRefreshInterval = remainingRefreshInterval;
this.isReAuth = isReAuth;
this.jwtProcessorCustomizer = (processor) -> {
};
}
/**
* Use the given signing
* algorithm .
*
* The value should be one of
* RS256, RS384, or RS512 .
* @param signatureAlgorithm the algorithm to use
* @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
*/
public PublicKeyJwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
return this;
}
/**
* Use the given {@link Consumer} to customize the {@link JWTProcessor
* ConfigurableJWTProcessor} before passing it to the build
* {@link UmsNimbusJwtDecoder}.
* @param jwtProcessorCustomizer the callback used to alter the processor
* @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
* @since 5.4
*/
public PublicKeyJwtDecoderBuilder jwtProcessorCustomizer(
Consumer> jwtProcessorCustomizer) {
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
this.jwtProcessorCustomizer = jwtProcessorCustomizer;
return this;
}
JWTProcessor processor() {
Assert.state(JWSAlgorithm.Family.RSA.contains(this.jwsAlgorithm),
() -> "The provided key is of type RSA; however the signature algorithm is of some other type: "
+ this.jwsAlgorithm + ". Please indicate one of RS256, RS384, or RS512.");
JWSKeySelector jwsKeySelector = new SingleKeyJWSKeySelector<>(this.jwsAlgorithm, this.key);
DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
this.jwtProcessorCustomizer.accept(jwtProcessor);
return jwtProcessor;
}
/**
* Build the configured {@link UmsNimbusJwtDecoder}.
* @return the configured {@link UmsNimbusJwtDecoder}
*/
public UmsNimbusJwtDecoder build() {
return new UmsNimbusJwtDecoder(processor(), refreshHandlerPolicy, remainingRefreshInterval, isReAuth);
}
}
/**
* A builder for creating {@link UmsNimbusJwtDecoder} instances based on a
* {@code SecretKey}.
*/
public static final class SecretKeyJwtDecoderBuilder {
private final SecretKey secretKey;
private final JwtRefreshHandlerPolicy refreshHandlerPolicy;
private final Duration remainingRefreshInterval;
private final Boolean isReAuth;
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
private Consumer> jwtProcessorCustomizer;
private SecretKeyJwtDecoderBuilder(SecretKey secretKey,
JwtRefreshHandlerPolicy refreshHandlerPolicy,
Duration remainingRefreshInterval,
Boolean isReAuth) {
Assert.notNull(secretKey, "secretKey cannot be null");
Assert.notNull(refreshHandlerPolicy, "refreshHandlerPolicy cannot be null");
Assert.notNull(remainingRefreshInterval, "remainingRefreshInterval cannot be null");
Assert.notNull(isReAuth, "isReAuth cannot be null");
this.secretKey = secretKey;
this.refreshHandlerPolicy = refreshHandlerPolicy;
this.remainingRefreshInterval = remainingRefreshInterval;
this.isReAuth = isReAuth;
this.jwtProcessorCustomizer = (processor) -> {
};
}
/**
* Use the given
* algorithm when generating the MAC.
*
* The value should be one of
* HS256, HS384 or HS512 .
* @param macAlgorithm the MAC algorithm to use
* @return a {@link SecretKeyJwtDecoderBuilder} for further configurations
*/
public SecretKeyJwtDecoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) {
Assert.notNull(macAlgorithm, "macAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(macAlgorithm.getName());
return this;
}
/**
* Use the given {@link Consumer} to customize the {@link JWTProcessor
* ConfigurableJWTProcessor} before passing it to the build
* {@link UmsNimbusJwtDecoder}.
* @param jwtProcessorCustomizer the callback used to alter the processor
* @return a {@link SecretKeyJwtDecoderBuilder} for further configurations
* @since 5.4
*/
public SecretKeyJwtDecoderBuilder jwtProcessorCustomizer(
Consumer> jwtProcessorCustomizer) {
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
this.jwtProcessorCustomizer = jwtProcessorCustomizer;
return this;
}
/**
* Build the configured {@link UmsNimbusJwtDecoder}.
* @return the configured {@link UmsNimbusJwtDecoder}
*/
public UmsNimbusJwtDecoder build() {
return new UmsNimbusJwtDecoder(processor(), refreshHandlerPolicy, remainingRefreshInterval, isReAuth);
}
JWTProcessor processor() {
JWSKeySelector jwsKeySelector = new SingleKeyJWSKeySelector<>(this.jwsAlgorithm,
this.secretKey);
DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
this.jwtProcessorCustomizer.accept(jwtProcessor);
return jwtProcessor;
}
}
}