Add support for JWT/JWS

Fixes gh-4434
This commit is contained in:
Joe Grandja 2017-07-04 17:30:54 -04:00
parent d8a678df6f
commit c986b6f4b5
9 changed files with 527 additions and 0 deletions

View File

@ -0,0 +1,8 @@
apply plugin: 'io.spring.convention.spring-module'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-oauth2-core')
compile springCoreDependency
compile 'com.nimbusds:nimbus-jose-jwt'
}

View File

@ -0,0 +1,91 @@
/*
* 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.
* 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.jose.jws;
/**
* The cryptographic algorithms defined by the <i>JSON Web Algorithms (JWA)</i> specification
* and used by <i>JSON Web Signature (JWS)</i> to digitally sign or create a MAC
* of the contents of the JWS Protected Header and JWS Payload.
*
* @author Joe Grandja
* @since 5.0
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</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/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
*/
public interface JwsAlgorithm {
/**
* HMAC using SHA-256 (Required)
*/
String HS256 = "HS256";
/**
* HMAC using SHA-384 (Optional)
*/
String HS384 = "HS384";
/**
* HMAC using SHA-512 (Optional)
*/
String HS512 = "HS512";
/**
* RSASSA-PKCS1-v1_5 using SHA-256 (Recommended)
*/
String RS256 = "RS256";
/**
* RSASSA-PKCS1-v1_5 using SHA-384 (Optional)
*/
String RS384 = "RS384";
/**
* RSASSA-PKCS1-v1_5 using SHA-512 (Optional)
*/
String RS512 = "RS512";
/**
* ECDSA using P-256 and SHA-256 (Recommended+)
*/
String ES256 = "ES256";
/**
* ECDSA using P-384 and SHA-384 (Optional)
*/
String ES384 = "ES384";
/**
* ECDSA using P-521 and SHA-512 (Optional)
*/
String ES512 = "ES512";
/**
* RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (Optional)
*/
String PS256 = "PS256";
/**
* RSASSA-PSS using SHA-384 and MGF1 with SHA-384 (Optional)
*/
String PS384 = "PS384";
/**
* RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (Optional)
*/
String PS512 = "PS512";
}

View File

@ -0,0 +1,64 @@
/*
* 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.
* 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.jwt;
import org.springframework.security.oauth2.core.AbstractToken;
import org.springframework.util.Assert;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* An implementation of an {@link AbstractToken} representing a <i>JSON Web Token (JWT)</i>.
*
* <p>
* JWTs represent a set of &quot;Claims&quot; as a JSON object that is encoded in a
* <i>JSON Web Signature (JWS)</i> and/or <i>JSON Web Encryption (JWE)</i> structure.
* The JSON object, also known as the <i>JWT Claims Set</i>, consists of one or more Claim Name/Claim Value pairs.
* The Claim Name is a <code>String</code> and the Claim Value is an arbitrary JSON object.
*
* @author Joe Grandja
* @since 5.0
* @see AbstractToken
* @see JwtClaimAccessor
* @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>
*/
public class Jwt extends AbstractToken implements JwtClaimAccessor {
private final Map<String, Object> headers;
private final Map<String, Object> claims;
public Jwt(String tokenValue, Instant issuedAt, Instant expiresAt,
Map<String, Object> headers, Map<String, Object> claims) {
super(tokenValue, issuedAt, expiresAt);
Assert.notEmpty(headers, "headers cannot be empty");
Assert.notEmpty(claims, "claims cannot be empty");
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
}
public Map<String, Object> getHeaders() {
return this.headers;
}
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.
* 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.jwt;
/**
* The &quot;Registered Claim Names&quot; defined by the <i>JSON Web Token (JWT)</i> specification
* that may be contained in the JSON object <i>JWT Claims Set</i>.
*
* @author Joe Grandja
* @since 5.0
* @see JwtClaimAccessor
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims</a>
*/
public interface JwtClaim {
String ISS = "iss";
String SUB = "sub";
String AUD = "aud";
String EXP = "exp";
String NBF = "nbf";
String IAT = "iat";
String JTI = "jti";
}

View File

@ -0,0 +1,64 @@
/*
* 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.
* 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.jwt;
import org.springframework.security.oauth2.core.ClaimAccessor;
import java.net.URI;
import java.time.Instant;
/**
* A {@link ClaimAccessor} for the &quot;Registered Claim Names&quot;
* that may be contained in the JSON object <i>JWT Claims Set</i> of a <i>JSON Web Token (JWT)</i>.
*
* @author Joe Grandja
* @since 5.0
* @see ClaimAccessor
* @see JwtClaim
* @see Jwt
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4.1">Registered Claim Names</a>
*/
public interface JwtClaimAccessor extends ClaimAccessor {
default URI getIssuer() {
return this.getClaimAsURI(JwtClaim.ISS);
}
default String getSubject() {
return this.getClaimAsString(JwtClaim.SUB);
}
default String getAudience() {
// FIXME Should return String[]
return this.getClaimAsString(JwtClaim.AUD);
}
default Instant getExpiresAt() {
return this.getClaimAsInstant(JwtClaim.EXP);
}
default Instant getNotBefore() {
return this.getClaimAsInstant(JwtClaim.NBF);
}
default Instant getIssuedAt() {
return this.getClaimAsInstant(JwtClaim.IAT);
}
default String getId() {
return this.getClaimAsString(JwtClaim.JTI);
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.
* 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.jwt;
/**
* Implementations of this interface are responsible for &quot;decoding&quot;
* a <i>JSON Web Token (JWT)</i> from it's compact claims representation format to a {@link Jwt}.
*
* <p>
* JWTs may be represented using the JWS Compact Serialization format for a
* <i>JSON Web Signature (JWS)</i> structure or JWE Compact Serialization format for a
* <i>JSON Web Encryption (JWE)</i> structure. Therefore, implementors are responsible
* for verifying a JWS and/or decrypting a JWE.
*
* @author Joe Grandja
* @since 5.0
* @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>
*/
@FunctionalInterface
public interface JwtDecoder {
Jwt decode(String token) throws JwtException;
}

View File

@ -0,0 +1,33 @@
/*
* 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.
* 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.jwt;
/**
* Base exception for all <i>JSON Web Token (JWT)</i> related errors.
*
* @author Joe Grandja
* @since 5.0
*/
public class JwtException extends RuntimeException {
public JwtException(String message) {
super(message);
}
public JwtException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,113 @@
/*
* 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.
* 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.jwt.nimbus;
import com.nimbusds.jose.JWSAlgorithm;
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.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import org.springframework.security.jose.jws.JwsAlgorithm;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtDecoder;
import org.springframework.security.jwt.JwtException;
import org.springframework.util.Assert;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* An implementation of a {@link JwtDecoder} that &quot;decodes&quot; a
* <i>JSON Web Token (JWT)</i> and additionally verifies it's digital signature if the JWT is a
* <i>JSON Web Signature (JWS)</i>. The public key used for verification is obtained from the
* <i>JSON Web Key (JWK)</i> Set <code>URL</code> which is supplied via the constructor.
*
* <p>
* <b>NOTE:</b> This implementation uses the <b>Nimbus JOSE + JWT SDK</b> internally.
*
* @author Joe Grandja
* @since 5.0
* @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 class NimbusJwtDecoderJwkSupport implements JwtDecoder {
private final URL jwkSetUrl;
private final JWSAlgorithm jwsAlgorithm;
private final ConfigurableJWTProcessor<SecurityContext> jwtProcessor;
public NimbusJwtDecoderJwkSupport(String jwkSetUrl) {
this(jwkSetUrl, JwsAlgorithm.RS256);
}
public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) {
Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
try {
this.jwkSetUrl = new URL(jwkSetUrl);
} catch (MalformedURLException ex) {
throw new IllegalArgumentException("Invalid JWK Set URL: " + ex.getMessage(), ex);
}
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
this.jwtProcessor = new DefaultJWTProcessor<>();
JWKSource jwkSource = new RemoteJWKSet(this.jwkSetUrl);
JWSKeySelector<SecurityContext> jwsKeySelector =
new JWSVerificationKeySelector<SecurityContext>(this.jwsAlgorithm, jwkSource);
this.jwtProcessor.setJWSKeySelector(jwsKeySelector);
}
@Override
public Jwt decode(String token) throws JwtException {
Jwt jwt;
try {
JWT parsedJwt = JWTParser.parse(token);
// Verify the signature
JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
Instant expiresAt = jwtClaimsSet.getExpirationTime().toInstant();
Instant issuedAt;
if (jwtClaimsSet.getIssueTime() != null) {
issuedAt = jwtClaimsSet.getIssueTime().toInstant();
} else {
// issuedAt is required in AbstractToken so let's default to expiresAt - 1 second
issuedAt = Instant.from(expiresAt).minusSeconds(1);
}
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
jwt = new Jwt(token, issuedAt, expiresAt, headers, jwtClaimsSet.getClaims());
} catch (Exception ex) {
throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex);
}
return jwt;
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.
* 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.core;
import org.springframework.util.Assert;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Map;
/**
* An &quot;accessor&quot; for a set of claims that may be used for assertions.
*
* @author Joe Grandja
* @since 5.0
*/
public interface ClaimAccessor {
Map<String, Object> getClaims();
default Boolean containsClaim(String claim) {
Assert.notNull(claim, "claim cannot be null");
return this.getClaims().containsKey(claim);
}
default String getClaimAsString(String claim) {
return (this.containsClaim(claim) ? this.getClaims().get(claim).toString() : null);
}
default Boolean getClaimAsBoolean(String claim) {
return (this.containsClaim(claim) ? Boolean.valueOf(this.getClaimAsString(claim)) : null);
}
default Instant getClaimAsInstant(String claim) {
if (!this.containsClaim(claim)) {
return null;
}
try {
return Instant.ofEpochSecond(Long.valueOf(this.getClaimAsString(claim)));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to Instant: " + ex.getMessage(), ex);
}
}
default URI getClaimAsURI(String claim) {
if (!this.containsClaim(claim)) {
return null;
}
try {
return new URI(this.getClaimAsString(claim));
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to URI: " + ex.getMessage(), ex);
}
}
}