2 changed files with 207 additions and 0 deletions
@ -0,0 +1,157 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2018 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 |
||||||
|
* |
||||||
|
* http://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 org.springframework.security.oauth2.jwt; |
||||||
|
|
||||||
|
import com.nimbusds.jose.JOSEException; |
||||||
|
import com.nimbusds.jose.JWSAlgorithm; |
||||||
|
import com.nimbusds.jose.jwk.JWK; |
||||||
|
import com.nimbusds.jose.jwk.JWKSelector; |
||||||
|
import com.nimbusds.jose.jwk.source.JWKSource; |
||||||
|
import com.nimbusds.jose.proc.BadJOSEException; |
||||||
|
import com.nimbusds.jose.proc.JWSKeySelector; |
||||||
|
import com.nimbusds.jose.proc.JWSVerificationKeySelector; |
||||||
|
import com.nimbusds.jwt.JWT; |
||||||
|
import com.nimbusds.jwt.JWTClaimsSet; |
||||||
|
import com.nimbusds.jwt.JWTParser; |
||||||
|
import com.nimbusds.jwt.SignedJWT; |
||||||
|
import com.nimbusds.jwt.proc.DefaultJWTProcessor; |
||||||
|
import com.nimbusds.jwt.proc.JWTProcessor; |
||||||
|
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import java.time.Instant; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* An implementation of a {@link JwtDecoder} that "decodes" a |
||||||
|
* JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a |
||||||
|
* JSON Web Signature (JWS). The public key used for verification is obtained from the |
||||||
|
* JSON Web Key (JWK) Set {@code URL} supplied via the constructor. |
||||||
|
* |
||||||
|
* <p> |
||||||
|
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK internally. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
* @since 5.1 |
||||||
|
* @see JwtDecoder |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a> |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a> |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a> |
||||||
|
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a> |
||||||
|
*/ |
||||||
|
public final class NimbusJwkReactiveJwtDecoder implements ReactiveJwtDecoder { |
||||||
|
private final JWTProcessor<JWKContext> jwtProcessor; |
||||||
|
|
||||||
|
private final ReactiveRemoteJWKSource reactiveJwkSource; |
||||||
|
|
||||||
|
private final JWKSelectorFactory jwkSelectorFactory; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. |
||||||
|
* |
||||||
|
* @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL} |
||||||
|
*/ |
||||||
|
public NimbusJwkReactiveJwtDecoder(String jwkSetUrl) { |
||||||
|
this(jwkSetUrl, JwsAlgorithms.RS256); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. |
||||||
|
* |
||||||
|
* @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL} |
||||||
|
* @param jwsAlgorithm the JSON Web Algorithm (JWA) used for verifying the digital signatures |
||||||
|
*/ |
||||||
|
public NimbusJwkReactiveJwtDecoder(String jwkSetUrl, String jwsAlgorithm) { |
||||||
|
Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty"); |
||||||
|
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty"); |
||||||
|
|
||||||
|
JWSAlgorithm algorithm = JWSAlgorithm.parse(jwsAlgorithm); |
||||||
|
JWKSource jwkSource = new JWKContextJWKSource(); |
||||||
|
JWSKeySelector<JWKContext> jwsKeySelector = |
||||||
|
new JWSVerificationKeySelector<>(algorithm, jwkSource); |
||||||
|
|
||||||
|
DefaultJWTProcessor<JWKContext> jwtProcessor = new DefaultJWTProcessor<>(); |
||||||
|
jwtProcessor.setJWSKeySelector(jwsKeySelector); |
||||||
|
this.jwtProcessor = jwtProcessor; |
||||||
|
|
||||||
|
this.reactiveJwkSource = new ReactiveRemoteJWKSource(jwkSetUrl); |
||||||
|
|
||||||
|
this.jwkSelectorFactory = new JWKSelectorFactory(algorithm); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Jwt> decode(String token) throws JwtException { |
||||||
|
JWT jwt = parse(token); |
||||||
|
if (jwt instanceof SignedJWT) { |
||||||
|
return this.decode((SignedJWT) jwt); |
||||||
|
} |
||||||
|
return Mono.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
private JWT parse(String token) { |
||||||
|
try { |
||||||
|
return JWTParser.parse(token); |
||||||
|
} catch (Exception ex) { |
||||||
|
throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private Mono<Jwt> decode(SignedJWT parsedToken) { |
||||||
|
try { |
||||||
|
JWKSelector selector = this.jwkSelectorFactory |
||||||
|
.createSelector(parsedToken.getHeader()); |
||||||
|
return this.reactiveJwkSource.get(selector) |
||||||
|
.map(jwkList -> createJwkSet(parsedToken, jwkList)) |
||||||
|
.map(set -> createJwt(parsedToken, set)); |
||||||
|
} catch (Exception ex) { |
||||||
|
throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private JWTClaimsSet createJwkSet(JWT parsedToken, List<JWK> jwkList) { |
||||||
|
try { |
||||||
|
return this.jwtProcessor.process(parsedToken, new JWKContext(jwkList)); |
||||||
|
} |
||||||
|
catch (BadJOSEException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
catch (JOSEException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { |
||||||
|
Instant expiresAt = null; |
||||||
|
if (jwtClaimsSet.getExpirationTime() != null) { |
||||||
|
expiresAt = jwtClaimsSet.getExpirationTime().toInstant(); |
||||||
|
} |
||||||
|
Instant issuedAt = null; |
||||||
|
if (jwtClaimsSet.getIssueTime() != null) { |
||||||
|
issuedAt = jwtClaimsSet.getIssueTime().toInstant(); |
||||||
|
} else if (expiresAt != null) { |
||||||
|
// Default to expiresAt - 1 second
|
||||||
|
issuedAt = Instant.from(expiresAt).minusSeconds(1); |
||||||
|
} |
||||||
|
|
||||||
|
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); |
||||||
|
|
||||||
|
return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, jwtClaimsSet.getClaims()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2018 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 |
||||||
|
* |
||||||
|
* http://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 org.springframework.security.oauth2.jwt; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implementations of this interface are responsible for "decoding" |
||||||
|
* a JSON Web Token (JWT) from it's compact claims representation format to a {@link Jwt}. |
||||||
|
* |
||||||
|
* <p> |
||||||
|
* JWTs may be represented using the JWS Compact Serialization format for a |
||||||
|
* JSON Web Signature (JWS) structure or JWE Compact Serialization format for a |
||||||
|
* JSON Web Encryption (JWE) structure. Therefore, implementors are responsible |
||||||
|
* for verifying a JWS and/or decrypting a JWE. |
||||||
|
* |
||||||
|
* @author Rob Winch |
||||||
|
* @since 5.1 |
||||||
|
* @see Jwt |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a> |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a> |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a> |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a> |
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-3.1">JWE Compact Serialization</a> |
||||||
|
*/ |
||||||
|
public interface ReactiveJwtDecoder { |
||||||
|
|
||||||
|
/** |
||||||
|
* Decodes the JWT from it's compact claims representation format and returns a {@link Jwt}. |
||||||
|
* |
||||||
|
* @param token the JWT value |
||||||
|
* @return a {@link Jwt} |
||||||
|
* @throws JwtException if an error occurs while attempting to decode the JWT |
||||||
|
*/ |
||||||
|
Mono<Jwt> decode(String token) throws JwtException; |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue