Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
jgrandja authored and Ryan Murfitt committed Mar 5, 2017
1 parent 2030fbc commit 082d069
Show file tree
Hide file tree
Showing 17 changed files with 275 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand Down Expand Up @@ -162,18 +162,16 @@ static PublicKeyUse fromValue(String value) {
* The defined Algorithm ("alg") values.
*/
enum CryptoAlgorithm {
RS256("SHA256withRSA", "RS256", "RSASSA-PKCS1-v1_5 using SHA-256"),
RS384("SHA384withRSA", "RS384", "RSASSA-PKCS1-v1_5 using SHA-384"),
RS512("SHA512withRSA", "RS512", "RSASSA-PKCS1-v1_5 using SHA-512");
RS256("SHA256withRSA", "RS256"),
RS384("SHA384withRSA", "RS384"),
RS512("SHA512withRSA", "RS512");

private final String standardName; // JCA Standard Name
private final String headerParamValue;
private final String description;

CryptoAlgorithm(String standardName, String headerParamValue, String description) {
CryptoAlgorithm(String standardName, String headerParamValue) {
this.standardName = standardName;
this.headerParamValue = headerParamValue;
this.description = description;
}

String standardName() {
Expand All @@ -184,10 +182,6 @@ String headerParamValue() {
return this.headerParamValue;
}

String description() {
return this.description;
}

static CryptoAlgorithm fromHeaderParamValue(String headerParamValue) {
CryptoAlgorithm result = null;
for (CryptoAlgorithm algorithm : values()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand Down Expand Up @@ -27,11 +27,10 @@
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentHashMap;

/**
* A source for JSON Web Key(s) (JWK) that is solely responsible for fetching (and caching)
Expand All @@ -46,9 +45,8 @@
*/
class JwkDefinitionSource {
private final URL jwkSetUrl;
private final JwkSetConverter jwkSetConverter = new JwkSetConverter();
private final AtomicReference<Map<JwkDefinition, SignatureVerifier>> jwkDefinitions =
new AtomicReference<Map<JwkDefinition, SignatureVerifier>>(new HashMap<JwkDefinition, SignatureVerifier>());
private final Map<String, JwkDefinitionHolder> jwkDefinitions = new ConcurrentHashMap<String, JwkDefinitionHolder>();
private static final JwkSetConverter jwkSetConverter = new JwkSetConverter();

/**
* Creates a new instance using the provided URL as the location for the JWK Set.
Expand All @@ -71,29 +69,28 @@ class JwkDefinitionSource {
*/
JwkDefinition getDefinition(String keyId) {
JwkDefinition result = null;
for (JwkDefinition jwkDefinition : this.jwkDefinitions.get().keySet()) {
if (jwkDefinition.getKeyId().equals(keyId)) {
result = jwkDefinition;
break;
}
JwkDefinitionHolder jwkDefinitionHolder = this.jwkDefinitions.get(keyId);
if (jwkDefinitionHolder != null) {
result = jwkDefinitionHolder.getJwkDefinition();
}
return result;
}

/**
* Returns the JWK definition matching the provided keyId (&quot;kid&quot;).
* If the JWK definition is not available in the internal cache then {@link #refreshJwkDefinitions()}
* will be called (to refresh the cache) and then followed-up with a second attempt to locate the JWK definition.
* If the JWK definition is not available in the internal cache then {@link #loadJwkDefinitions(URL)}
* will be called (to re-load the cache) and then followed-up with a second attempt to locate the JWK definition.
*
* @param keyId the Key ID (&quot;kid&quot;)
* @return the matching {@link JwkDefinition} or null if not found
*/
JwkDefinition getDefinitionRefreshIfNecessary(String keyId) {
JwkDefinition getDefinitionLoadIfNecessary(String keyId) {
JwkDefinition result = this.getDefinition(keyId);
if (result != null) {
return result;
}
this.refreshJwkDefinitions();
this.jwkDefinitions.clear();
this.jwkDefinitions.putAll(loadJwkDefinitions(this.jwkSetUrl));
return this.getDefinition(keyId);
}

Expand All @@ -105,42 +102,47 @@ JwkDefinition getDefinitionRefreshIfNecessary(String keyId) {
*/
SignatureVerifier getVerifier(String keyId) {
SignatureVerifier result = null;
JwkDefinition jwkDefinition = this.getDefinitionRefreshIfNecessary(keyId);
JwkDefinition jwkDefinition = this.getDefinitionLoadIfNecessary(keyId);
if (jwkDefinition != null) {
result = this.jwkDefinitions.get().get(jwkDefinition);
result = this.jwkDefinitions.get(keyId).getSignatureVerifier();
}
return result;
}

/**
* Refreshes the internal cache of association(s) between {@link JwkDefinition} and {@link SignatureVerifier}.
* Fetches the JWK Set from the provided <code>URL</code> and
* returns a <code>Map</code> keyed by the JWK keyId (&quot;kid&quot;)
* and mapped to an association of the {@link JwkDefinition} and {@link SignatureVerifier}.
* Uses a {@link JwkSetConverter} to convert the JWK Set URL source to a set of {@link JwkDefinition}(s)
* followed by the instantiation of a {@link SignatureVerifier} which is mapped to it's {@link JwkDefinition}.
* followed by the instantiation of a {@link SignatureVerifier} which is associated to it's {@link JwkDefinition}.
*
* @param jwkSetUrl the JWK Set URL
* @return a <code>Map</code> keyed by the JWK keyId and mapped to an association of {@link JwkDefinition} and {@link SignatureVerifier}
* @see JwkSetConverter
*/
void refreshJwkDefinitions() {
static Map<String, JwkDefinitionHolder> loadJwkDefinitions(URL jwkSetUrl) {
InputStream jwkSetSource;
try {
jwkSetSource = this.jwkSetUrl.openStream();
jwkSetSource = jwkSetUrl.openStream();
} catch (IOException ex) {
throw new JwkException("An I/O error occurred while reading from the JWK Set source: " + ex.getMessage(), ex);
}

Set<JwkDefinition> jwkDefinitionSet = this.jwkSetConverter.convert(jwkSetSource);
Set<JwkDefinition> jwkDefinitionSet = jwkSetConverter.convert(jwkSetSource);

Map<JwkDefinition, SignatureVerifier> refreshedJwkDefinitions = new LinkedHashMap<JwkDefinition, SignatureVerifier>();
Map<String, JwkDefinitionHolder> jwkDefinitions = new LinkedHashMap<String, JwkDefinitionHolder>();

for (JwkDefinition jwkDefinition : jwkDefinitionSet) {
if (JwkDefinition.KeyType.RSA.equals(jwkDefinition.getKeyType())) {
refreshedJwkDefinitions.put(jwkDefinition, this.createRSAVerifier((RSAJwkDefinition)jwkDefinition));
jwkDefinitions.put(jwkDefinition.getKeyId(),
new JwkDefinitionHolder(jwkDefinition, createRsaVerifier((RsaJwkDefinition) jwkDefinition)));
}
}

this.jwkDefinitions.set(refreshedJwkDefinitions);
return jwkDefinitions;
}

private RsaVerifier createRSAVerifier(RSAJwkDefinition rsaDefinition) {
private static RsaVerifier createRsaVerifier(RsaJwkDefinition rsaDefinition) {
RsaVerifier result;
try {
BigInteger modulus = new BigInteger(Codecs.b64UrlDecode(rsaDefinition.getModulus()));
Expand All @@ -157,4 +159,22 @@ private RsaVerifier createRSAVerifier(RSAJwkDefinition rsaDefinition) {
}
return result;
}
}

static class JwkDefinitionHolder {
private final JwkDefinition jwkDefinition;
private final SignatureVerifier signatureVerifier;

private JwkDefinitionHolder(JwkDefinition jwkDefinition, SignatureVerifier signatureVerifier) {
this.jwkDefinition = jwkDefinition;
this.signatureVerifier = signatureVerifier;
}

private JwkDefinition getJwkDefinition() {
return jwkDefinition;
}

private SignatureVerifier getSignatureVerifier() {
return signatureVerifier;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand Down Expand Up @@ -119,20 +119,21 @@ private JwkDefinition createJwkDefinition(Map<String, String> attributes) {

if (!JwkDefinition.KeyType.RSA.equals(keyType)) {
throw new JwkException((keyType != null ? keyType.value() : "unknown") +
" (" + KEY_TYPE + ") is currently not supported.");
" (" + KEY_TYPE + ") is currently not supported." +
" Valid values for '" + KEY_TYPE + "' are: " + JwkDefinition.KeyType.RSA.value());
}

return this.createRSAJwkDefinition(attributes);
return this.createRsaJwkDefinition(attributes);
}

/**
* Creates a {@link RSAJwkDefinition} based on the supplied attributes.
* Creates a {@link RsaJwkDefinition} based on the supplied attributes.
*
* @param attributes the attributes used to create the {@link RSAJwkDefinition}
* @param attributes the attributes used to create the {@link RsaJwkDefinition}
* @return a {@link JwkDefinition} representation of a RSA Key
* @throws JwkException if at least one attribute value is missing or invalid for a RSA Key
*/
private JwkDefinition createRSAJwkDefinition(Map<String, String> attributes) {
private JwkDefinition createRsaJwkDefinition(Map<String, String> attributes) {
// kid
String keyId = attributes.get(KEY_ID);
if (!StringUtils.hasText(keyId)) {
Expand Down Expand Up @@ -169,7 +170,7 @@ private JwkDefinition createRSAJwkDefinition(Map<String, String> attributes) {
throw new JwkException(RSA_PUBLIC_KEY_EXPONENT + " is a required attribute for a RSA JWK.");
}

RSAJwkDefinition jwkDefinition = new RSAJwkDefinition(
RsaJwkDefinition jwkDefinition = new RsaJwkDefinition(
keyId, publicKeyUse, algorithm, modulus, exponent);

return jwkDefinition;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand All @@ -15,7 +15,6 @@
*/
package org.springframework.security.oauth2.provider.token.store.jwk;

import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
Expand Down Expand Up @@ -60,36 +59,33 @@
* <br>
*
* This implementation delegates to an internal instance of a {@link JwtTokenStore} which uses a
* specialized extension of {@link JwtAccessTokenConverter}, specifically, {@link JwkVerifyingJwtAccessTokenConverter}.
* The {@link JwkVerifyingJwtAccessTokenConverter} is associated with a {@link JwkDefinitionSource} which is responsible
* for fetching (and caching) the JWK Set (a set of JWKs) from the URL supplied to the constructor of this implementation.
* specialized extension of {@link JwtAccessTokenConverter}.
* This specialized {@link JwtAccessTokenConverter} is capable of fetching (and caching)
* the JWK Set (a set of JWKs) from the URL supplied to the constructor of this implementation.
* <br>
* <br>
*
* The {@link JwkVerifyingJwtAccessTokenConverter} will verify the JWS in the following step sequence:
* The {@link JwtAccessTokenConverter} will verify the JWS in the following step sequence:
* <br>
* <br>
* <ol>
* <li>Extract the <b>&quot;kid&quot;</b> parameter from the JWT header.</li>
* <li>Find the matching {@link JwkDefinition} from the {@link JwkDefinitionSource} with the corresponding <b>&quot;kid&quot;</b> attribute.</li>
* <li>Obtain the {@link SignatureVerifier} associated with the {@link JwkDefinition} via the {@link JwkDefinitionSource} and verify the signature.</li>
* <li>Find the matching JWK with the corresponding <b>&quot;kid&quot;</b> attribute.</li>
* <li>Obtain the <code>SignatureVerifier</code> associated with the JWK and verify the signature.</li>
* </ol>
* <br>
* <b>NOTE:</b> The algorithms currently supported by this implementation are: RS256, RS384 and RS512.
* <br>
* <br>
*
* @see JwtTokenStore
* @see JwkVerifyingJwtAccessTokenConverter
* @see JwkDefinitionSource
* @see JwkDefinition
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a>
* @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>
*
* @author Joe Grandja
*/
public class JwkTokenStore implements TokenStore {
public final class JwkTokenStore implements TokenStore {
private final JwtTokenStore delegate;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand Down Expand Up @@ -100,7 +100,7 @@ protected Map<String, Object> decode(String token) {
if (keyIdHeader == null) {
throw new InvalidTokenException("Invalid JWT/JWS: " + KEY_ID + " is a required JOSE Header");
}
JwkDefinition jwkDefinition = this.jwkDefinitionSource.getDefinitionRefreshIfNecessary(keyIdHeader);
JwkDefinition jwkDefinition = this.jwkDefinitionSource.getDefinitionLoadIfNecessary(keyIdHeader);
if (jwkDefinition == null) {
throw new InvalidTokenException("Invalid JOSE Header " + KEY_ID + " (" + keyIdHeader + ")");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2017 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.
Expand All @@ -23,7 +23,7 @@
*
* @author Joe Grandja
*/
final class RSAJwkDefinition extends JwkDefinition {
final class RsaJwkDefinition extends JwkDefinition {
private final String modulus;
private final String exponent;

Expand All @@ -36,7 +36,7 @@ final class RSAJwkDefinition extends JwkDefinition {
* @param modulus the modulus value for the Public Key
* @param exponent the exponent value for the Public Key
*/
RSAJwkDefinition(String keyId,
RsaJwkDefinition(String keyId,
PublicKeyUse publicKeyUse,
CryptoAlgorithm algorithm,
String modulus,
Expand Down
Loading

0 comments on commit 082d069

Please sign in to comment.