parent
339a05312e
commit
5830fda2fa
|
@ -52,4 +52,5 @@
|
|||
<suppress files="WebSecurityConfigurationTests\.java" checks="SpringMethodVisibility"/>
|
||||
<suppress files="WithSecurityContextTestExecutionListenerTests\.java" checks="SpringMethodVisibility"/>
|
||||
<suppress files="AbstractOAuth2AuthorizationGrantRequestEntityConverter\.java" checks="SpringMethodVisibility"/>
|
||||
<suppress files="JoseHeader\.java" checks="SpringMethodVisibility"/>
|
||||
</suppressions>
|
||||
|
|
|
@ -40,7 +40,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
@ -122,7 +127,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
|
|||
throw new OAuth2AuthorizationException(oauth2Error);
|
||||
}
|
||||
|
||||
JoseHeader.Builder headersBuilder = JoseHeader.withAlgorithm(jwsAlgorithm);
|
||||
JwsHeader.Builder headersBuilder = JwsHeader.with(jwsAlgorithm);
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(Duration.ofSeconds(60));
|
||||
|
@ -137,7 +142,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
|
|||
.expiresAt(expiresAt);
|
||||
// @formatter:on
|
||||
|
||||
JoseHeader joseHeader = headersBuilder.build();
|
||||
JwsHeader jwsHeader = headersBuilder.build();
|
||||
JwtClaimsSet jwtClaimsSet = claimsBuilder.build();
|
||||
|
||||
JwsEncoderHolder jwsEncoderHolder = this.jwsEncoders.compute(clientRegistration.getRegistrationId(),
|
||||
|
@ -146,11 +151,11 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
|
|||
return currentJwsEncoderHolder;
|
||||
}
|
||||
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
||||
return new JwsEncoderHolder(new NimbusJwsEncoder(jwkSource), jwk);
|
||||
return new JwsEncoderHolder(new NimbusJwtEncoder(jwkSource), jwk);
|
||||
});
|
||||
|
||||
NimbusJwsEncoder jwsEncoder = jwsEncoderHolder.getJwsEncoder();
|
||||
Jwt jws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
JwtEncoder jwsEncoder = jwsEncoderHolder.getJwsEncoder();
|
||||
Jwt jws = jwsEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
|
||||
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE);
|
||||
|
@ -186,16 +191,16 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
|
|||
|
||||
private static final class JwsEncoderHolder {
|
||||
|
||||
private final NimbusJwsEncoder jwsEncoder;
|
||||
private final JwtEncoder jwsEncoder;
|
||||
|
||||
private final JWK jwk;
|
||||
|
||||
private JwsEncoderHolder(NimbusJwsEncoder jwsEncoder, JWK jwk) {
|
||||
private JwsEncoderHolder(JwtEncoder jwsEncoder, JWK jwk) {
|
||||
this.jwsEncoder = jwsEncoder;
|
||||
this.jwk = jwk;
|
||||
}
|
||||
|
||||
private NimbusJwsEncoder getJwsEncoder() {
|
||||
private JwtEncoder getJwsEncoder() {
|
||||
return this.jwsEncoder;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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 org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.jose.JwaAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for {@link JoseHeader}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class JoseHeaderTests {
|
||||
|
||||
@Test
|
||||
public void withAlgorithmWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JoseHeader.withAlgorithm(null))
|
||||
.isInstanceOf(IllegalArgumentException.class).withMessage("jwaAlgorithm cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllHeadersProvidedThenAllHeadersAreSet() {
|
||||
JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build();
|
||||
|
||||
// @formatter:off
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(expectedJoseHeader.getAlgorithm())
|
||||
.jwkSetUrl(expectedJoseHeader.getJwkSetUrl().toExternalForm())
|
||||
.jwk(expectedJoseHeader.getJwk())
|
||||
.keyId(expectedJoseHeader.getKeyId())
|
||||
.x509Url(expectedJoseHeader.getX509Url().toExternalForm())
|
||||
.x509CertificateChain(expectedJoseHeader.getX509CertificateChain())
|
||||
.x509SHA1Thumbprint(expectedJoseHeader.getX509SHA1Thumbprint())
|
||||
.x509SHA256Thumbprint(expectedJoseHeader.getX509SHA256Thumbprint())
|
||||
.type(expectedJoseHeader.getType())
|
||||
.contentType(expectedJoseHeader.getContentType())
|
||||
.headers((headers) -> headers.put("custom-header-name", "custom-header-value"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
assertThat(joseHeader.<JwaAlgorithm>getAlgorithm()).isEqualTo(expectedJoseHeader.getAlgorithm());
|
||||
assertThat(joseHeader.getJwkSetUrl()).isEqualTo(expectedJoseHeader.getJwkSetUrl());
|
||||
assertThat(joseHeader.getJwk()).isEqualTo(expectedJoseHeader.getJwk());
|
||||
assertThat(joseHeader.getKeyId()).isEqualTo(expectedJoseHeader.getKeyId());
|
||||
assertThat(joseHeader.getX509Url()).isEqualTo(expectedJoseHeader.getX509Url());
|
||||
assertThat(joseHeader.getX509CertificateChain()).isEqualTo(expectedJoseHeader.getX509CertificateChain());
|
||||
assertThat(joseHeader.getX509SHA1Thumbprint()).isEqualTo(expectedJoseHeader.getX509SHA1Thumbprint());
|
||||
assertThat(joseHeader.getX509SHA256Thumbprint()).isEqualTo(expectedJoseHeader.getX509SHA256Thumbprint());
|
||||
assertThat(joseHeader.getType()).isEqualTo(expectedJoseHeader.getType());
|
||||
assertThat(joseHeader.getContentType()).isEqualTo(expectedJoseHeader.getContentType());
|
||||
assertThat(joseHeader.<String>getHeader("custom-header-name")).isEqualTo("custom-header-value");
|
||||
assertThat(joseHeader.getHeaders()).isEqualTo(expectedJoseHeader.getHeaders());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JoseHeader.from(null))
|
||||
.isInstanceOf(IllegalArgumentException.class).withMessage("headers cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenHeadersProvidedThenCopied() {
|
||||
JoseHeader expectedJoseHeader = TestJoseHeaders.joseHeader().build();
|
||||
JoseHeader joseHeader = JoseHeader.from(expectedJoseHeader).build();
|
||||
assertThat(joseHeader.getHeaders()).isEqualTo(expectedJoseHeader.getHeaders());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerWhenNameNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).header(null, "value"))
|
||||
.withMessage("name cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerWhenValueNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).header("name", null))
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHeaderWhenNullThenThrowIllegalArgumentException() {
|
||||
JoseHeader joseHeader = TestJoseHeaders.joseHeader().build();
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> joseHeader.getHeader(null))
|
||||
.isInstanceOf(IllegalArgumentException.class).withMessage("name cannot be empty");
|
||||
}
|
||||
|
||||
}
|
|
@ -38,6 +38,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
|
|
|
@ -14,11 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -26,24 +27,8 @@ import java.util.function.Consumer;
|
|||
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.jose.JwaAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* The JOSE header is a JSON object representing the header parameters of a JSON Web
|
||||
* Token, whether the JWT is a JWS or JWE, that describe the cryptographic operations
|
||||
|
@ -51,7 +36,7 @@ import org.springframework.util.Assert;
|
|||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 5.5
|
||||
* @since 5.6
|
||||
* @see Jwt
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE
|
||||
* Header</a>
|
||||
|
@ -60,11 +45,12 @@ import org.springframework.util.Assert;
|
|||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE
|
||||
* Header</a>
|
||||
*/
|
||||
final class JoseHeader {
|
||||
class JoseHeader {
|
||||
|
||||
private final Map<String, Object> headers;
|
||||
|
||||
private JoseHeader(Map<String, Object> headers) {
|
||||
protected JoseHeader(Map<String, Object> headers) {
|
||||
Assert.notEmpty(headers, "headers cannot be empty");
|
||||
this.headers = Collections.unmodifiableMap(new HashMap<>(headers));
|
||||
}
|
||||
|
||||
|
@ -74,7 +60,7 @@ final class JoseHeader {
|
|||
* @return the {@link JwaAlgorithm}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
<T extends JwaAlgorithm> T getAlgorithm() {
|
||||
public <T extends JwaAlgorithm> T getAlgorithm() {
|
||||
return (T) getHeader(JoseHeaderNames.ALG);
|
||||
}
|
||||
|
||||
|
@ -84,7 +70,7 @@ final class JoseHeader {
|
|||
* the JWE.
|
||||
* @return the JWK Set URL
|
||||
*/
|
||||
URL getJwkSetUrl() {
|
||||
public URL getJwkSetUrl() {
|
||||
return getHeader(JoseHeaderNames.JKU);
|
||||
}
|
||||
|
||||
|
@ -93,7 +79,7 @@ final class JoseHeader {
|
|||
* to digitally sign the JWS or encrypt the JWE.
|
||||
* @return the JSON Web Key
|
||||
*/
|
||||
Map<String, Object> getJwk() {
|
||||
public Map<String, Object> getJwk() {
|
||||
return getHeader(JoseHeaderNames.JWK);
|
||||
}
|
||||
|
||||
|
@ -102,7 +88,7 @@ final class JoseHeader {
|
|||
* or JWE.
|
||||
* @return the key ID
|
||||
*/
|
||||
String getKeyId() {
|
||||
public String getKeyId() {
|
||||
return getHeader(JoseHeaderNames.KID);
|
||||
}
|
||||
|
||||
|
@ -112,7 +98,7 @@ final class JoseHeader {
|
|||
* the JWS or encrypt the JWE.
|
||||
* @return the X.509 URL
|
||||
*/
|
||||
URL getX509Url() {
|
||||
public URL getX509Url() {
|
||||
return getHeader(JoseHeaderNames.X5U);
|
||||
}
|
||||
|
||||
|
@ -124,7 +110,7 @@ final class JoseHeader {
|
|||
* {@code List} is a Base64-encoded DER PKIX certificate value.
|
||||
* @return the X.509 certificate chain
|
||||
*/
|
||||
List<String> getX509CertificateChain() {
|
||||
public List<String> getX509CertificateChain() {
|
||||
return getHeader(JoseHeaderNames.X5C);
|
||||
}
|
||||
|
||||
|
@ -134,7 +120,7 @@ final class JoseHeader {
|
|||
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
* @return the X.509 certificate SHA-1 thumbprint
|
||||
*/
|
||||
String getX509SHA1Thumbprint() {
|
||||
public String getX509SHA1Thumbprint() {
|
||||
return getHeader(JoseHeaderNames.X5T);
|
||||
}
|
||||
|
||||
|
@ -144,7 +130,7 @@ final class JoseHeader {
|
|||
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
* @return the X.509 certificate SHA-256 thumbprint
|
||||
*/
|
||||
String getX509SHA256Thumbprint() {
|
||||
public String getX509SHA256Thumbprint() {
|
||||
return getHeader(JoseHeaderNames.X5T_S256);
|
||||
}
|
||||
|
||||
|
@ -152,7 +138,7 @@ final class JoseHeader {
|
|||
* Returns the type header that declares the media type of the JWS/JWE.
|
||||
* @return the type header
|
||||
*/
|
||||
String getType() {
|
||||
public String getType() {
|
||||
return getHeader(JoseHeaderNames.TYP);
|
||||
}
|
||||
|
||||
|
@ -161,7 +147,7 @@ final class JoseHeader {
|
|||
* (the payload).
|
||||
* @return the content type header
|
||||
*/
|
||||
String getContentType() {
|
||||
public String getContentType() {
|
||||
return getHeader(JoseHeaderNames.CTY);
|
||||
}
|
||||
|
||||
|
@ -170,7 +156,7 @@ final class JoseHeader {
|
|||
* specifications are being used that MUST be understood and processed.
|
||||
* @return the critical headers
|
||||
*/
|
||||
Set<String> getCritical() {
|
||||
public Set<String> getCritical() {
|
||||
return getHeader(JoseHeaderNames.CRIT);
|
||||
}
|
||||
|
||||
|
@ -178,7 +164,7 @@ final class JoseHeader {
|
|||
* Returns the headers.
|
||||
* @return the headers
|
||||
*/
|
||||
Map<String, Object> getHeaders() {
|
||||
public Map<String, Object> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
|
@ -189,53 +175,38 @@ final class JoseHeader {
|
|||
* @return the header value
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> T getHeader(String name) {
|
||||
public <T> T getHeader(String name) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
return (T) getHeaders().get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@link JwaAlgorithm}.
|
||||
* @param jwaAlgorithm the {@link JwaAlgorithm}
|
||||
* @return the {@link Builder}
|
||||
* A builder for subclasses of {@link JoseHeader}.
|
||||
*/
|
||||
static Builder withAlgorithm(JwaAlgorithm jwaAlgorithm) {
|
||||
return new Builder(jwaAlgorithm);
|
||||
abstract static class AbstractBuilder<T extends JoseHeader, B extends AbstractBuilder<T, B>> {
|
||||
|
||||
private final Map<String, Object> headers = new HashMap<>();
|
||||
|
||||
protected AbstractBuilder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@code headers}.
|
||||
* @param headers the headers
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
static Builder from(JoseHeader headers) {
|
||||
return new Builder(headers);
|
||||
protected Map<String, Object> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link JoseHeader}.
|
||||
*/
|
||||
static final class Builder {
|
||||
|
||||
final Map<String, Object> headers = new HashMap<>();
|
||||
|
||||
private Builder(JwaAlgorithm jwaAlgorithm) {
|
||||
algorithm(jwaAlgorithm);
|
||||
}
|
||||
|
||||
private Builder(JoseHeader headers) {
|
||||
Assert.notNull(headers, "headers cannot be null");
|
||||
this.headers.putAll(headers.getHeaders());
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final B getThis() {
|
||||
return (B) this; // avoid unchecked casts in subclasses by using "getThis()"
|
||||
// instead of "(B) this"
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link JwaAlgorithm JWA algorithm} used to digitally sign the JWS or
|
||||
* encrypt the JWE.
|
||||
* @param jwaAlgorithm the {@link JwaAlgorithm}
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder algorithm(JwaAlgorithm jwaAlgorithm) {
|
||||
Assert.notNull(jwaAlgorithm, "jwaAlgorithm cannot be null");
|
||||
public B algorithm(JwaAlgorithm jwaAlgorithm) {
|
||||
return header(JoseHeaderNames.ALG, jwaAlgorithm);
|
||||
}
|
||||
|
||||
|
@ -244,9 +215,9 @@ final class JoseHeader {
|
|||
* public keys, one of which corresponds to the key used to digitally sign the JWS
|
||||
* or encrypt the JWE.
|
||||
* @param jwkSetUrl the JWK Set URL
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder jwkSetUrl(String jwkSetUrl) {
|
||||
public B jwkSetUrl(String jwkSetUrl) {
|
||||
return header(JoseHeaderNames.JKU, convertAsURL(JoseHeaderNames.JKU, jwkSetUrl));
|
||||
}
|
||||
|
||||
|
@ -254,9 +225,9 @@ final class JoseHeader {
|
|||
* Sets the JSON Web Key which is the public key that corresponds to the key used
|
||||
* to digitally sign the JWS or encrypt the JWE.
|
||||
* @param jwk the JSON Web Key
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder jwk(Map<String, Object> jwk) {
|
||||
public B jwk(Map<String, Object> jwk) {
|
||||
return header(JoseHeaderNames.JWK, jwk);
|
||||
}
|
||||
|
||||
|
@ -264,9 +235,9 @@ final class JoseHeader {
|
|||
* Sets the key ID that is a hint indicating which key was used to secure the JWS
|
||||
* or JWE.
|
||||
* @param keyId the key ID
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder keyId(String keyId) {
|
||||
public B keyId(String keyId) {
|
||||
return header(JoseHeaderNames.KID, keyId);
|
||||
}
|
||||
|
||||
|
@ -275,9 +246,9 @@ final class JoseHeader {
|
|||
* certificate or certificate chain corresponding to the key used to digitally
|
||||
* sign the JWS or encrypt the JWE.
|
||||
* @param x509Url the X.509 URL
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder x509Url(String x509Url) {
|
||||
public B x509Url(String x509Url) {
|
||||
return header(JoseHeaderNames.X5U, convertAsURL(JoseHeaderNames.X5U, x509Url));
|
||||
}
|
||||
|
||||
|
@ -288,9 +259,9 @@ final class JoseHeader {
|
|||
* {@code List} of certificate value {@code String}s. Each {@code String} in the
|
||||
* {@code List} is a Base64-encoded DER PKIX certificate value.
|
||||
* @param x509CertificateChain the X.509 certificate chain
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder x509CertificateChain(List<String> x509CertificateChain) {
|
||||
public B x509CertificateChain(List<String> x509CertificateChain) {
|
||||
return header(JoseHeaderNames.X5C, x509CertificateChain);
|
||||
}
|
||||
|
||||
|
@ -299,9 +270,9 @@ final class JoseHeader {
|
|||
* thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
|
||||
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
* @param x509SHA1Thumbprint the X.509 certificate SHA-1 thumbprint
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder x509SHA1Thumbprint(String x509SHA1Thumbprint) {
|
||||
public B x509SHA1Thumbprint(String x509SHA1Thumbprint) {
|
||||
return header(JoseHeaderNames.X5T, x509SHA1Thumbprint);
|
||||
}
|
||||
|
||||
|
@ -310,18 +281,18 @@ final class JoseHeader {
|
|||
* SHA-256 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
|
||||
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
|
||||
* @param x509SHA256Thumbprint the X.509 certificate SHA-256 thumbprint
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder x509SHA256Thumbprint(String x509SHA256Thumbprint) {
|
||||
public B x509SHA256Thumbprint(String x509SHA256Thumbprint) {
|
||||
return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type header that declares the media type of the JWS/JWE.
|
||||
* @param type the type header
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder type(String type) {
|
||||
public B type(String type) {
|
||||
return header(JoseHeaderNames.TYP, type);
|
||||
}
|
||||
|
||||
|
@ -329,54 +300,56 @@ final class JoseHeader {
|
|||
* Sets the content type header that declares the media type of the secured
|
||||
* content (the payload).
|
||||
* @param contentType the content type header
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder contentType(String contentType) {
|
||||
public B contentType(String contentType) {
|
||||
return header(JoseHeaderNames.CTY, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA
|
||||
* Sets the critical header that indicates which extensions to the JWS/JWE/JWA
|
||||
* specifications are being used that MUST be understood and processed.
|
||||
* @param headerNames the critical header names
|
||||
* @return the {@link Builder}
|
||||
* @param name the critical header name
|
||||
* @param value the critical header value
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder critical(Set<String> headerNames) {
|
||||
return header(JoseHeaderNames.CRIT, headerNames);
|
||||
@SuppressWarnings("unchecked")
|
||||
public B criticalHeader(String name, Object value) {
|
||||
header(name, value);
|
||||
getHeaders().computeIfAbsent(JoseHeaderNames.CRIT, (k) -> new HashSet<String>());
|
||||
((Set<String>) getHeaders().get(JoseHeaderNames.CRIT)).add(name);
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the header.
|
||||
* @param name the header name
|
||||
* @param value the header value
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder header(String name, Object value) {
|
||||
public B header(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.headers.put(name, value);
|
||||
return this;
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Consumer} to be provided access to the headers allowing the ability to
|
||||
* add, replace, or remove.
|
||||
* @param headersConsumer a {@code Consumer} of the headers
|
||||
* @return the {@link Builder}
|
||||
* @return the {@link AbstractBuilder}
|
||||
*/
|
||||
Builder headers(Consumer<Map<String, Object>> headersConsumer) {
|
||||
public B headers(Consumer<Map<String, Object>> headersConsumer) {
|
||||
headersConsumer.accept(this.headers);
|
||||
return this;
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link JoseHeader}.
|
||||
* @return a {@link JoseHeader}
|
||||
*/
|
||||
JoseHeader build() {
|
||||
Assert.notEmpty(this.headers, "headers cannot be empty");
|
||||
return new JoseHeader(this.headers);
|
||||
}
|
||||
public abstract T build();
|
||||
|
||||
private static URL convertAsURL(String header, String value) {
|
||||
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
|
|
@ -14,22 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
/**
|
||||
* The Registered Header Parameter Names defined by the JSON Web Token (JWT), JSON Web
|
||||
|
@ -38,8 +23,7 @@ package org.springframework.security.oauth2.client.endpoint;
|
|||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 5.5
|
||||
* @see JoseHeader
|
||||
* @since 5.6
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE
|
||||
* Header</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE
|
||||
|
@ -47,53 +31,53 @@ package org.springframework.security.oauth2.client.endpoint;
|
|||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE
|
||||
* Header</a>
|
||||
*/
|
||||
final class JoseHeaderNames {
|
||||
public final class JoseHeaderNames {
|
||||
|
||||
/**
|
||||
* {@code alg} - the algorithm header identifies the cryptographic algorithm used to
|
||||
* secure a JWS or JWE
|
||||
*/
|
||||
static final String ALG = "alg";
|
||||
public static final String ALG = "alg";
|
||||
|
||||
/**
|
||||
* {@code jku} - the JWK Set URL header is a URI that refers to a resource for a set
|
||||
* of JSON-encoded public keys, one of which corresponds to the key used to digitally
|
||||
* sign a JWS or encrypt a JWE
|
||||
*/
|
||||
static final String JKU = "jku";
|
||||
public static final String JKU = "jku";
|
||||
|
||||
/**
|
||||
* {@code jwk} - the JSON Web Key header is the public key that corresponds to the key
|
||||
* used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
static final String JWK = "jwk";
|
||||
public static final String JWK = "jwk";
|
||||
|
||||
/**
|
||||
* {@code kid} - the key ID header is a hint indicating which key was used to secure a
|
||||
* JWS or JWE
|
||||
*/
|
||||
static final String KID = "kid";
|
||||
public static final String KID = "kid";
|
||||
|
||||
/**
|
||||
* {@code x5u} - the X.509 URL header is a URI that refers to a resource for the X.509
|
||||
* public key certificate or certificate chain corresponding to the key used to
|
||||
* digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
static final String X5U = "x5u";
|
||||
public static final String X5U = "x5u";
|
||||
|
||||
/**
|
||||
* {@code x5c} - the X.509 certificate chain header contains the X.509 public key
|
||||
* certificate or certificate chain corresponding to the key used to digitally sign a
|
||||
* JWS or encrypt a JWE
|
||||
*/
|
||||
static final String X5C = "x5c";
|
||||
public static final String X5C = "x5c";
|
||||
|
||||
/**
|
||||
* {@code x5t} - the X.509 certificate SHA-1 thumbprint header is a base64url-encoded
|
||||
* SHA-1 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
|
||||
* corresponding to the key used to digitally sign a JWS or encrypt a JWE
|
||||
*/
|
||||
static final String X5T = "x5t";
|
||||
public static final String X5T = "x5t";
|
||||
|
||||
/**
|
||||
* {@code x5t#S256} - the X.509 certificate SHA-256 thumbprint header is a
|
||||
|
@ -101,25 +85,25 @@ final class JoseHeaderNames {
|
|||
* X.509 certificate corresponding to the key used to digitally sign a JWS or encrypt
|
||||
* a JWE
|
||||
*/
|
||||
static final String X5T_S256 = "x5t#S256";
|
||||
public static final String X5T_S256 = "x5t#S256";
|
||||
|
||||
/**
|
||||
* {@code typ} - the type header is used by JWS/JWE applications to declare the media
|
||||
* type of a JWS/JWE
|
||||
*/
|
||||
static final String TYP = "typ";
|
||||
public static final String TYP = "typ";
|
||||
|
||||
/**
|
||||
* {@code cty} - the content type header is used by JWS/JWE applications to declare
|
||||
* the media type of the secured content (the payload)
|
||||
*/
|
||||
static final String CTY = "cty";
|
||||
public static final String CTY = "cty";
|
||||
|
||||
/**
|
||||
* {@code crit} - the critical header indicates that extensions to the JWS/JWE/JWA
|
||||
* specifications are being used that MUST be understood and processed
|
||||
*/
|
||||
static final String CRIT = "crit";
|
||||
public static final String CRIT = "crit";
|
||||
|
||||
private JoseHeaderNames() {
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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 org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The JSON Web Signature (JWS) header is a JSON object representing the header parameters
|
||||
* of a JSON Web Token, that describe the cryptographic operations used to digitally sign
|
||||
* or create a MAC of the contents of the JWS Protected Header and JWS Payload.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.6
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE
|
||||
* Header</a>
|
||||
*/
|
||||
public final class JwsHeader extends JoseHeader {
|
||||
|
||||
private JwsHeader(Map<String, Object> headers) {
|
||||
super(headers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public JwsAlgorithm getAlgorithm() {
|
||||
return super.getAlgorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@link JwsAlgorithm}.
|
||||
* @param jwsAlgorithm the {@link JwsAlgorithm}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(JwsAlgorithm jwsAlgorithm) {
|
||||
return new Builder(jwsAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the provided {@code headers}.
|
||||
* @param headers the headers
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder from(JwsHeader headers) {
|
||||
return new Builder(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link JwsHeader}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<JwsHeader, Builder> {
|
||||
|
||||
private Builder(JwsAlgorithm jwsAlgorithm) {
|
||||
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
|
||||
algorithm(jwsAlgorithm);
|
||||
}
|
||||
|
||||
private Builder(JwsHeader headers) {
|
||||
Assert.notNull(headers, "headers cannot be null");
|
||||
getHeaders().putAll(headers.getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link JwsHeader}.
|
||||
* @return a {@link JwsHeader}
|
||||
*/
|
||||
@Override
|
||||
public JwsHeader build() {
|
||||
return new JwsHeader(getHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
|
@ -25,39 +25,21 @@ import java.util.Map;
|
|||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimAccessor;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* The {@link Jwt JWT} Claims Set is a JSON object representing the claims conveyed by a
|
||||
* JSON Web Token.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 5.5
|
||||
* @since 5.6
|
||||
* @see Jwt
|
||||
* @see JwtClaimAccessor
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims
|
||||
* Set</a>
|
||||
*/
|
||||
final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
public final class JwtClaimsSet implements JwtClaimAccessor {
|
||||
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
|
@ -74,7 +56,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* Returns a new {@link Builder}.
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
static Builder builder() {
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
|
@ -83,16 +65,16 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param claims a JWT claims set
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
static Builder from(JwtClaimsSet claims) {
|
||||
public static Builder from(JwtClaimsSet claims) {
|
||||
return new Builder(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link JwtClaimsSet}.
|
||||
*/
|
||||
static final class Builder {
|
||||
public static final class Builder {
|
||||
|
||||
final Map<String, Object> claims = new HashMap<>();
|
||||
private final Map<String, Object> claims = new HashMap<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
@ -108,7 +90,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param issuer the issuer identifier
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder issuer(String issuer) {
|
||||
public Builder issuer(String issuer) {
|
||||
return claim(JwtClaimNames.ISS, issuer);
|
||||
}
|
||||
|
||||
|
@ -118,7 +100,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param subject the subject identifier
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder subject(String subject) {
|
||||
public Builder subject(String subject) {
|
||||
return claim(JwtClaimNames.SUB, subject);
|
||||
}
|
||||
|
||||
|
@ -128,7 +110,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param audience the audience that this JWT is intended for
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder audience(List<String> audience) {
|
||||
public Builder audience(List<String> audience) {
|
||||
return claim(JwtClaimNames.AUD, audience);
|
||||
}
|
||||
|
||||
|
@ -139,7 +121,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* processing
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder expiresAt(Instant expiresAt) {
|
||||
public Builder expiresAt(Instant expiresAt) {
|
||||
return claim(JwtClaimNames.EXP, expiresAt);
|
||||
}
|
||||
|
||||
|
@ -150,7 +132,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* processing
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder notBefore(Instant notBefore) {
|
||||
public Builder notBefore(Instant notBefore) {
|
||||
return claim(JwtClaimNames.NBF, notBefore);
|
||||
}
|
||||
|
||||
|
@ -160,7 +142,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param issuedAt the time at which the JWT was issued
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder issuedAt(Instant issuedAt) {
|
||||
public Builder issuedAt(Instant issuedAt) {
|
||||
return claim(JwtClaimNames.IAT, issuedAt);
|
||||
}
|
||||
|
||||
|
@ -170,7 +152,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param jti the unique identifier for the JWT
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder id(String jti) {
|
||||
public Builder id(String jti) {
|
||||
return claim(JwtClaimNames.JTI, jti);
|
||||
}
|
||||
|
||||
|
@ -180,7 +162,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* @param value the claim value
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
Builder claim(String name, Object value) {
|
||||
public Builder claim(String name, Object value) {
|
||||
Assert.hasText(name, "name cannot be empty");
|
||||
Assert.notNull(value, "value cannot be null");
|
||||
this.claims.put(name, value);
|
||||
|
@ -192,7 +174,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* add, replace, or remove.
|
||||
* @param claimsConsumer a {@code Consumer} of the claims
|
||||
*/
|
||||
Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
|
||||
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
|
||||
claimsConsumer.accept(this.claims);
|
||||
return this;
|
||||
}
|
||||
|
@ -201,7 +183,7 @@ final class JwtClaimsSet implements JwtClaimAccessor {
|
|||
* Builds a new {@link JwtClaimsSet}.
|
||||
* @return a {@link JwtClaimsSet}
|
||||
*/
|
||||
JwtClaimsSet build() {
|
||||
public JwtClaimsSet build() {
|
||||
Assert.notEmpty(this.claims, "claims cannot be empty");
|
||||
|
||||
// The value of the 'iss' claim is a String or URL (StringOrURI).
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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 org.springframework.security.oauth2.jwt;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for encoding a JSON Web Token (JWT)
|
||||
* to it's compact claims representation format.
|
||||
*
|
||||
* <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 signing a JWS and/or
|
||||
* encrypting a JWE.
|
||||
*
|
||||
* @author Anoop Garlapati
|
||||
* @author Joe Grandja
|
||||
* @since 5.6
|
||||
* @see Jwt
|
||||
* @see JwtEncoderParameters
|
||||
* @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/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 JwtEncoder {
|
||||
|
||||
/**
|
||||
* Encode the JWT to it's compact claims representation format.
|
||||
* @param parameters the parameters containing the JOSE header and JWT Claims Set
|
||||
* @return a {@link Jwt}
|
||||
* @throws JwtEncodingException if an error occurs while attempting to encode the JWT
|
||||
*/
|
||||
Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException;
|
||||
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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 org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A holder of parameters containing the JWS headers and JWT Claims Set.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.6
|
||||
* @see JwsHeader
|
||||
* @see JwtClaimsSet
|
||||
* @see JwtEncoder
|
||||
*/
|
||||
public final class JwtEncoderParameters {
|
||||
|
||||
private final JwsHeader jwsHeader;
|
||||
|
||||
private final JwtClaimsSet claims;
|
||||
|
||||
private JwtEncoderParameters(JwsHeader jwsHeader, JwtClaimsSet claims) {
|
||||
this.jwsHeader = jwsHeader;
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link JwtEncoderParameters}, initialized with the provided
|
||||
* {@link JwtClaimsSet}.
|
||||
* @param claims the {@link JwtClaimsSet}
|
||||
* @return the {@link JwtEncoderParameters}
|
||||
*/
|
||||
public static JwtEncoderParameters from(JwtClaimsSet claims) {
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
return new JwtEncoderParameters(null, claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link JwtEncoderParameters}, initialized with the provided
|
||||
* {@link JwsHeader} and {@link JwtClaimsSet}.
|
||||
* @param jwsHeader the {@link JwsHeader}
|
||||
* @param claims the {@link JwtClaimsSet}
|
||||
* @return the {@link JwtEncoderParameters}
|
||||
*/
|
||||
public static JwtEncoderParameters from(JwsHeader jwsHeader, JwtClaimsSet claims) {
|
||||
Assert.notNull(jwsHeader, "jwsHeader cannot be null");
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
return new JwtEncoderParameters(jwsHeader, claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JwsHeader JWS headers}.
|
||||
* @return the {@link JwsHeader}, or {@code null} if not specified
|
||||
*/
|
||||
@Nullable
|
||||
public JwsHeader getJwsHeader() {
|
||||
return this.jwsHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JwtClaimsSet claims}.
|
||||
* @return the {@link JwtClaimsSet}
|
||||
*/
|
||||
public JwtClaimsSet getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
}
|
|
@ -14,39 +14,22 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
/**
|
||||
* This exception is thrown when an error occurs while attempting to encode a JSON Web
|
||||
* Token (JWT).
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.5
|
||||
* @since 5.6
|
||||
*/
|
||||
class JwtEncodingException extends JwtException {
|
||||
public class JwtEncodingException extends JwtException {
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtEncodingException} using the provided parameters.
|
||||
* @param message the detail message
|
||||
*/
|
||||
JwtEncodingException(String message) {
|
||||
public JwtEncodingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
|
@ -55,7 +38,7 @@ class JwtEncodingException extends JwtException {
|
|||
* @param message the detail message
|
||||
* @param cause the root cause
|
||||
*/
|
||||
JwtEncodingException(String message, Throwable cause) {
|
||||
public JwtEncodingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
@ -46,38 +46,23 @@ import com.nimbusds.jose.util.Base64URL;
|
|||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* A JWT encoder that encodes a JSON Web Token (JWT) using the JSON Web Signature (JWS)
|
||||
* Compact Serialization format. The private/secret key used for signing the JWS is
|
||||
* supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource} provided via the
|
||||
* constructor.
|
||||
* An implementation of a {@link JwtEncoder} that encodes a JSON Web Token (JWT) using the
|
||||
* JSON Web Signature (JWS) Compact Serialization format. The private/secret key used for
|
||||
* signing the JWS is supplied by the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
* provided via the constructor.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.5
|
||||
* @since 5.6
|
||||
* @see JwtEncoder
|
||||
* @see com.nimbusds.jose.jwk.source.JWKSource
|
||||
* @see com.nimbusds.jose.jwk.JWK
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token
|
||||
|
@ -89,10 +74,12 @@ import org.springframework.util.StringUtils;
|
|||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus
|
||||
* JOSE + JWT SDK</a>
|
||||
*/
|
||||
final class NimbusJwsEncoder {
|
||||
public final class NimbusJwtEncoder implements JwtEncoder {
|
||||
|
||||
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s";
|
||||
|
||||
private static final JwsHeader DEFAULT_JWS_HEADER = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
|
||||
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory();
|
||||
|
||||
private final Map<JWK, JWSSigner> jwsSigners = new ConcurrentHashMap<>();
|
||||
|
@ -100,17 +87,23 @@ final class NimbusJwsEncoder {
|
|||
private final JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
/**
|
||||
* Constructs a {@code NimbusJwsEncoder} using the provided parameters.
|
||||
* Constructs a {@code NimbusJwtEncoder} using the provided parameters.
|
||||
* @param jwkSource the {@code com.nimbusds.jose.jwk.source.JWKSource}
|
||||
*/
|
||||
NimbusJwsEncoder(JWKSource<SecurityContext> jwkSource) {
|
||||
public NimbusJwtEncoder(JWKSource<SecurityContext> jwkSource) {
|
||||
Assert.notNull(jwkSource, "jwkSource cannot be null");
|
||||
this.jwkSource = jwkSource;
|
||||
}
|
||||
|
||||
Jwt encode(JoseHeader headers, JwtClaimsSet claims) throws JwtEncodingException {
|
||||
Assert.notNull(headers, "headers cannot be null");
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
@Override
|
||||
public Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException {
|
||||
Assert.notNull(parameters, "parameters cannot be null");
|
||||
|
||||
JwsHeader headers = parameters.getJwsHeader();
|
||||
if (headers == null) {
|
||||
headers = DEFAULT_JWS_HEADER;
|
||||
}
|
||||
JwtClaimsSet claims = parameters.getClaims();
|
||||
|
||||
JWK jwk = selectJwk(headers);
|
||||
headers = addKeyIdentifierHeadersIfNecessary(headers, jwk);
|
||||
|
@ -120,7 +113,7 @@ final class NimbusJwsEncoder {
|
|||
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
|
||||
}
|
||||
|
||||
private JWK selectJwk(JoseHeader headers) {
|
||||
private JWK selectJwk(JwsHeader headers) {
|
||||
List<JWK> jwks;
|
||||
try {
|
||||
JWKSelector jwkSelector = new JWKSelector(createJwkMatcher(headers));
|
||||
|
@ -144,11 +137,11 @@ final class NimbusJwsEncoder {
|
|||
return jwks.get(0);
|
||||
}
|
||||
|
||||
private String serialize(JoseHeader headers, JwtClaimsSet claims, JWK jwk) {
|
||||
private String serialize(JwsHeader headers, JwtClaimsSet claims, JWK jwk) {
|
||||
JWSHeader jwsHeader = convert(headers);
|
||||
JWTClaimsSet jwtClaimsSet = convert(claims);
|
||||
|
||||
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, NimbusJwsEncoder::createSigner);
|
||||
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, NimbusJwtEncoder::createSigner);
|
||||
|
||||
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
|
||||
try {
|
||||
|
@ -161,7 +154,7 @@ final class NimbusJwsEncoder {
|
|||
return signedJwt.serialize();
|
||||
}
|
||||
|
||||
private static JWKMatcher createJwkMatcher(JoseHeader headers) {
|
||||
private static JWKMatcher createJwkMatcher(JwsHeader headers) {
|
||||
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(headers.getAlgorithm().getName());
|
||||
|
||||
if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm) || JWSAlgorithm.Family.EC.contains(jwsAlgorithm)) {
|
||||
|
@ -189,7 +182,7 @@ final class NimbusJwsEncoder {
|
|||
return null;
|
||||
}
|
||||
|
||||
private static JoseHeader addKeyIdentifierHeadersIfNecessary(JoseHeader headers, JWK jwk) {
|
||||
private static JwsHeader addKeyIdentifierHeadersIfNecessary(JwsHeader headers, JWK jwk) {
|
||||
// Check if headers have already been added
|
||||
if (StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(headers.getX509SHA256Thumbprint())) {
|
||||
return headers;
|
||||
|
@ -199,7 +192,7 @@ final class NimbusJwsEncoder {
|
|||
return headers;
|
||||
}
|
||||
|
||||
JoseHeader.Builder headersBuilder = JoseHeader.from(headers);
|
||||
JwsHeader.Builder headersBuilder = JwsHeader.from(headers);
|
||||
if (!StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(jwk.getKeyID())) {
|
||||
headersBuilder.keyId(jwk.getKeyID());
|
||||
}
|
||||
|
@ -220,7 +213,7 @@ final class NimbusJwsEncoder {
|
|||
}
|
||||
}
|
||||
|
||||
private static JWSHeader convert(JoseHeader headers) {
|
||||
private static JWSHeader convert(JwsHeader headers) {
|
||||
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getAlgorithm().getName()));
|
||||
|
||||
if (headers.getJwkSetUrl() != null) {
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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 org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwsHeader}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class JwsHeaderTests {
|
||||
|
||||
@Test
|
||||
public void withWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JwsHeader.with(null))
|
||||
.withMessage("jwsAlgorithm cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JwsHeader.from(null))
|
||||
.withMessage("headers cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenHeadersProvidedThenCopied() {
|
||||
JwsHeader expectedJwsHeader = TestJwsHeaders.jwsHeader().build();
|
||||
JwsHeader jwsHeader = JwsHeader.from(expectedJwsHeader).build();
|
||||
assertThat(jwsHeader.getHeaders()).isEqualTo(expectedJwsHeader.getHeaders());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllHeadersProvidedThenAllHeadersAreSet() {
|
||||
JwsHeader expectedJwsHeader = TestJwsHeaders.jwsHeader().build();
|
||||
|
||||
// @formatter:off
|
||||
JwsHeader jwsHeader = JwsHeader.with(expectedJwsHeader.getAlgorithm())
|
||||
.jwkSetUrl(expectedJwsHeader.getJwkSetUrl().toExternalForm())
|
||||
.jwk(expectedJwsHeader.getJwk())
|
||||
.keyId(expectedJwsHeader.getKeyId())
|
||||
.x509Url(expectedJwsHeader.getX509Url().toExternalForm())
|
||||
.x509CertificateChain(expectedJwsHeader.getX509CertificateChain())
|
||||
.x509SHA1Thumbprint(expectedJwsHeader.getX509SHA1Thumbprint())
|
||||
.x509SHA256Thumbprint(expectedJwsHeader.getX509SHA256Thumbprint())
|
||||
.type(expectedJwsHeader.getType())
|
||||
.contentType(expectedJwsHeader.getContentType())
|
||||
.criticalHeader("critical-header1-name", "critical-header1-value")
|
||||
.criticalHeader("critical-header2-name", "critical-header2-value")
|
||||
.headers((headers) -> headers.put("custom-header-name", "custom-header-value"))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
assertThat(jwsHeader.getAlgorithm()).isEqualTo(expectedJwsHeader.getAlgorithm());
|
||||
assertThat(jwsHeader.getJwkSetUrl()).isEqualTo(expectedJwsHeader.getJwkSetUrl());
|
||||
assertThat(jwsHeader.getJwk()).isEqualTo(expectedJwsHeader.getJwk());
|
||||
assertThat(jwsHeader.getKeyId()).isEqualTo(expectedJwsHeader.getKeyId());
|
||||
assertThat(jwsHeader.getX509Url()).isEqualTo(expectedJwsHeader.getX509Url());
|
||||
assertThat(jwsHeader.getX509CertificateChain()).isEqualTo(expectedJwsHeader.getX509CertificateChain());
|
||||
assertThat(jwsHeader.getX509SHA1Thumbprint()).isEqualTo(expectedJwsHeader.getX509SHA1Thumbprint());
|
||||
assertThat(jwsHeader.getX509SHA256Thumbprint()).isEqualTo(expectedJwsHeader.getX509SHA256Thumbprint());
|
||||
assertThat(jwsHeader.getType()).isEqualTo(expectedJwsHeader.getType());
|
||||
assertThat(jwsHeader.getContentType()).isEqualTo(expectedJwsHeader.getContentType());
|
||||
assertThat(jwsHeader.getCritical()).containsExactlyInAnyOrder("critical-header1-name", "critical-header2-name");
|
||||
assertThat(jwsHeader.<String>getHeader("critical-header1-name")).isEqualTo("critical-header1-value");
|
||||
assertThat(jwsHeader.<String>getHeader("critical-header2-name")).isEqualTo("critical-header2-value");
|
||||
assertThat(jwsHeader.<String>getHeader("custom-header-name")).isEqualTo("custom-header-value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerWhenNameNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> JwsHeader.with(SignatureAlgorithm.RS256).header(null, "value"))
|
||||
.withMessage("name cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerWhenValueNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> JwsHeader.with(SignatureAlgorithm.RS256).header("name", null))
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHeaderWhenNullThenThrowIllegalArgumentException() {
|
||||
JwsHeader jwsHeader = TestJwsHeaders.jwsHeader().build();
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> jwsHeader.getHeader(null))
|
||||
.withMessage("name cannot be empty");
|
||||
}
|
||||
|
||||
}
|
|
@ -14,28 +14,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtClaimsSet}.
|
||||
*
|
||||
|
@ -46,7 +31,7 @@ public class JwtClaimsSetTests {
|
|||
@Test
|
||||
public void buildWhenClaimsEmptyThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JwtClaimsSet.builder().build())
|
||||
.isInstanceOf(IllegalArgumentException.class).withMessage("claims cannot be empty");
|
||||
.withMessage("claims cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -80,7 +65,7 @@ public class JwtClaimsSetTests {
|
|||
@Test
|
||||
public void fromWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JwtClaimsSet.from(null))
|
||||
.isInstanceOf(IllegalArgumentException.class).withMessage("claims cannot be null");
|
||||
.withMessage("claims cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
|
@ -0,0 +1,518 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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 org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.jose.EncryptionMethod;
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.JWEAlgorithm;
|
||||
import com.nimbusds.jose.JWEHeader;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import com.nimbusds.jose.Payload;
|
||||
import com.nimbusds.jose.crypto.RSAEncrypter;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKMatcher;
|
||||
import com.nimbusds.jose.jwk.JWKSelector;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.KeyType;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.jose.util.Base64;
|
||||
import com.nimbusds.jose.util.Base64URL;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.jose.JwaAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for proofing out future support of JWE.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class NimbusJweEncoderTests {
|
||||
|
||||
// @formatter:off
|
||||
private static final JweHeader DEFAULT_JWE_HEADER =
|
||||
JweHeader.with(JweAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM.getName()).build();
|
||||
// @formatter:on
|
||||
|
||||
private List<JWK> jwkList;
|
||||
|
||||
private JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
private NimbusJweEncoder jweEncoder;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.jwkList = new ArrayList<>();
|
||||
this.jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(new JWKSet(this.jwkList));
|
||||
this.jweEncoder = new NimbusJweEncoder(this.jwkSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwtClaimsSetThenEncodes() {
|
||||
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
|
||||
this.jwkList.add(rsaJwk);
|
||||
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
// @formatter:off
|
||||
// **********************
|
||||
// Assume future API:
|
||||
// JwtEncoderParameters.with(JweHeader jweHeader, JwtClaimsSet claims)
|
||||
// **********************
|
||||
// @formatter:on
|
||||
Jwt encodedJwe = this.jweEncoder.encode(JwtEncoderParameters.from(jwtClaimsSet));
|
||||
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(DEFAULT_JWE_HEADER.getAlgorithm());
|
||||
assertThat(encodedJwe.getHeaders().get("enc")).isEqualTo(DEFAULT_JWE_HEADER.<String>getHeader("enc"));
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.JKU)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.JWK)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID());
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.X5U)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.X5C)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.X5T)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.X5T_S256)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.TYP)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.CTY)).isNull();
|
||||
assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.CRIT)).isNull();
|
||||
|
||||
assertThat(encodedJwe.getIssuer()).isEqualTo(jwtClaimsSet.getIssuer());
|
||||
assertThat(encodedJwe.getSubject()).isEqualTo(jwtClaimsSet.getSubject());
|
||||
assertThat(encodedJwe.getAudience()).isEqualTo(jwtClaimsSet.getAudience());
|
||||
assertThat(encodedJwe.getExpiresAt()).isEqualTo(jwtClaimsSet.getExpiresAt());
|
||||
assertThat(encodedJwe.getNotBefore()).isEqualTo(jwtClaimsSet.getNotBefore());
|
||||
assertThat(encodedJwe.getIssuedAt()).isEqualTo(jwtClaimsSet.getIssuedAt());
|
||||
assertThat(encodedJwe.getId()).isEqualTo(jwtClaimsSet.getId());
|
||||
assertThat(encodedJwe.<String>getClaim("custom-claim-name")).isEqualTo("custom-claim-value");
|
||||
|
||||
assertThat(encodedJwe.getTokenValue()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenNestedJwsThenEncodes() {
|
||||
// See Nimbus example -> Nested signed and encrypted JWT
|
||||
// https://connect2id.com/products/nimbus-jose-jwt/examples/signed-and-encrypted-jwt
|
||||
|
||||
RSAKey rsaJwk = TestJwks.DEFAULT_RSA_JWK;
|
||||
this.jwkList.add(rsaJwk);
|
||||
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
// @formatter:off
|
||||
// **********************
|
||||
// Assume future API:
|
||||
// JwtEncoderParameters.with(JwsHeader jwsHeader, JweHeader jweHeader, JwtClaimsSet claims)
|
||||
// **********************
|
||||
// @formatter:on
|
||||
Jwt encodedJweNestedJws = this.jweEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
|
||||
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.ALG))
|
||||
.isEqualTo(DEFAULT_JWE_HEADER.getAlgorithm());
|
||||
assertThat(encodedJweNestedJws.getHeaders().get("enc")).isEqualTo(DEFAULT_JWE_HEADER.<String>getHeader("enc"));
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.JKU)).isNull();
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.JWK)).isNull();
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID());
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5U)).isNull();
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5C)).isNull();
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5T)).isNull();
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5T_S256)).isNull();
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.TYP)).isNull();
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.CTY)).isEqualTo("JWT");
|
||||
assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.CRIT)).isNull();
|
||||
|
||||
assertThat(encodedJweNestedJws.getIssuer()).isEqualTo(jwtClaimsSet.getIssuer());
|
||||
assertThat(encodedJweNestedJws.getSubject()).isEqualTo(jwtClaimsSet.getSubject());
|
||||
assertThat(encodedJweNestedJws.getAudience()).isEqualTo(jwtClaimsSet.getAudience());
|
||||
assertThat(encodedJweNestedJws.getExpiresAt()).isEqualTo(jwtClaimsSet.getExpiresAt());
|
||||
assertThat(encodedJweNestedJws.getNotBefore()).isEqualTo(jwtClaimsSet.getNotBefore());
|
||||
assertThat(encodedJweNestedJws.getIssuedAt()).isEqualTo(jwtClaimsSet.getIssuedAt());
|
||||
assertThat(encodedJweNestedJws.getId()).isEqualTo(jwtClaimsSet.getId());
|
||||
assertThat(encodedJweNestedJws.<String>getClaim("custom-claim-name")).isEqualTo("custom-claim-value");
|
||||
|
||||
assertThat(encodedJweNestedJws.getTokenValue()).isNotNull();
|
||||
}
|
||||
|
||||
enum JweAlgorithm implements JwaAlgorithm {
|
||||
|
||||
RSA_OAEP_256("RSA-OAEP-256");
|
||||
|
||||
private final String name;
|
||||
|
||||
JweAlgorithm(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class JweHeader extends JoseHeader {
|
||||
|
||||
private JweHeader(Map<String, Object> headers) {
|
||||
super(headers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public JweAlgorithm getAlgorithm() {
|
||||
return super.getAlgorithm();
|
||||
}
|
||||
|
||||
private static Builder with(JweAlgorithm jweAlgorithm, String enc) {
|
||||
return new Builder(jweAlgorithm, enc);
|
||||
}
|
||||
|
||||
private static Builder from(JweHeader headers) {
|
||||
return new Builder(headers);
|
||||
}
|
||||
|
||||
private static final class Builder extends AbstractBuilder<JweHeader, Builder> {
|
||||
|
||||
private Builder(JweAlgorithm jweAlgorithm, String enc) {
|
||||
Assert.notNull(jweAlgorithm, "jweAlgorithm cannot be null");
|
||||
Assert.hasText(enc, "enc cannot be empty");
|
||||
algorithm(jweAlgorithm);
|
||||
header("enc", enc);
|
||||
}
|
||||
|
||||
private Builder(JweHeader headers) {
|
||||
Assert.notNull(headers, "headers cannot be null");
|
||||
Consumer<Map<String, Object>> headersConsumer = (h) -> h.putAll(headers.getHeaders());
|
||||
headers(headersConsumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JweHeader build() {
|
||||
return new JweHeader(getHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class NimbusJweEncoder implements JwtEncoder {
|
||||
|
||||
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s";
|
||||
|
||||
private static final Converter<JweHeader, JWEHeader> JWE_HEADER_CONVERTER = new JweHeaderConverter();
|
||||
|
||||
private static final Converter<JwtClaimsSet, JWTClaimsSet> JWT_CLAIMS_SET_CONVERTER = new JwtClaimsSetConverter();
|
||||
|
||||
private final JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
private final JwtEncoder jwsEncoder;
|
||||
|
||||
private NimbusJweEncoder(JWKSource<SecurityContext> jwkSource) {
|
||||
Assert.notNull(jwkSource, "jwkSource cannot be null");
|
||||
this.jwkSource = jwkSource;
|
||||
this.jwsEncoder = new NimbusJwtEncoder(jwkSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Jwt encode(JwtEncoderParameters parameters) throws JwtEncodingException {
|
||||
Assert.notNull(parameters, "parameters cannot be null");
|
||||
|
||||
// @formatter:off
|
||||
// **********************
|
||||
// Assume future API:
|
||||
// JwtEncoderParameters.getJweHeader()
|
||||
// **********************
|
||||
// @formatter:on
|
||||
JweHeader jweHeader = DEFAULT_JWE_HEADER; // Assume this is accessed via
|
||||
// JwtEncoderParameters.getJweHeader()
|
||||
|
||||
JwsHeader jwsHeader = parameters.getJwsHeader();
|
||||
JwtClaimsSet claims = parameters.getClaims();
|
||||
|
||||
JWK jwk = selectJwk(jweHeader);
|
||||
jweHeader = addKeyIdentifierHeadersIfNecessary(jweHeader, jwk);
|
||||
|
||||
JWEHeader jweHeader2 = JWE_HEADER_CONVERTER.convert(jweHeader);
|
||||
JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims);
|
||||
|
||||
String payload;
|
||||
if (jwsHeader != null) {
|
||||
// Sign then encrypt
|
||||
Jwt jws = this.jwsEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
payload = jws.getTokenValue();
|
||||
|
||||
// @formatter:off
|
||||
jweHeader = JweHeader.from(jweHeader)
|
||||
.contentType("JWT") // Indicates Nested JWT (REQUIRED)
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
else {
|
||||
// Encrypt only
|
||||
payload = jwtClaimsSet.toString();
|
||||
}
|
||||
|
||||
JWEObject jweObject = new JWEObject(jweHeader2, new Payload(payload));
|
||||
try {
|
||||
// FIXME
|
||||
// Resolve type of JWEEncrypter using the JWK key type
|
||||
// For now, assuming RSA key type
|
||||
jweObject.encrypt(new RSAEncrypter(jwk.toRSAKey()));
|
||||
}
|
||||
catch (JOSEException ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to encrypt the JWT -> " + ex.getMessage()), ex);
|
||||
}
|
||||
String jwe = jweObject.serialize();
|
||||
|
||||
// NOTE:
|
||||
// For the Nested JWS use case, we lose access to the JWS Header in the
|
||||
// returned JWT.
|
||||
// If this is needed, we can simply add the new method Jwt.getNestedHeaders().
|
||||
return new Jwt(jwe, claims.getIssuedAt(), claims.getExpiresAt(), jweHeader.getHeaders(),
|
||||
claims.getClaims());
|
||||
}
|
||||
|
||||
private JWK selectJwk(JweHeader headers) {
|
||||
List<JWK> jwks;
|
||||
try {
|
||||
JWKSelector jwkSelector = new JWKSelector(createJwkMatcher(headers));
|
||||
jwks = this.jwkSource.get(jwkSelector, null);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to select a JWK encryption key -> " + ex.getMessage()), ex);
|
||||
}
|
||||
|
||||
if (jwks.size() > 1) {
|
||||
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Found multiple JWK encryption keys for algorithm '" + headers.getAlgorithm().getName() + "'"));
|
||||
}
|
||||
|
||||
if (jwks.isEmpty()) {
|
||||
throw new JwtEncodingException(
|
||||
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK encryption key"));
|
||||
}
|
||||
|
||||
return jwks.get(0);
|
||||
}
|
||||
|
||||
private static JWKMatcher createJwkMatcher(JweHeader headers) {
|
||||
JWEAlgorithm jweAlgorithm = JWEAlgorithm.parse(headers.getAlgorithm().getName());
|
||||
|
||||
// @formatter:off
|
||||
return new JWKMatcher.Builder()
|
||||
.keyType(KeyType.forAlgorithm(jweAlgorithm))
|
||||
.keyID(headers.getKeyId())
|
||||
.keyUses(KeyUse.ENCRYPTION, null)
|
||||
.algorithms(jweAlgorithm, null)
|
||||
.x509CertSHA256Thumbprint(Base64URL.from(headers.getX509SHA256Thumbprint()))
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static JweHeader addKeyIdentifierHeadersIfNecessary(JweHeader headers, JWK jwk) {
|
||||
// Check if headers have already been added
|
||||
if (StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(headers.getX509SHA256Thumbprint())) {
|
||||
return headers;
|
||||
}
|
||||
// Check if headers can be added from JWK
|
||||
if (!StringUtils.hasText(jwk.getKeyID()) && jwk.getX509CertSHA256Thumbprint() == null) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
JweHeader.Builder headersBuilder = JweHeader.from(headers);
|
||||
if (!StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(jwk.getKeyID())) {
|
||||
headersBuilder.keyId(jwk.getKeyID());
|
||||
}
|
||||
if (!StringUtils.hasText(headers.getX509SHA256Thumbprint()) && jwk.getX509CertSHA256Thumbprint() != null) {
|
||||
headersBuilder.x509SHA256Thumbprint(jwk.getX509CertSHA256Thumbprint().toString());
|
||||
}
|
||||
|
||||
return headersBuilder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class JweHeaderConverter implements Converter<JweHeader, JWEHeader> {
|
||||
|
||||
@Override
|
||||
public JWEHeader convert(JweHeader headers) {
|
||||
JWEAlgorithm jweAlgorithm = JWEAlgorithm.parse(headers.getAlgorithm().getName());
|
||||
EncryptionMethod encryptionMethod = EncryptionMethod.parse(headers.getHeader("enc"));
|
||||
JWEHeader.Builder builder = new JWEHeader.Builder(jweAlgorithm, encryptionMethod);
|
||||
|
||||
URL jwkSetUri = headers.getJwkSetUrl();
|
||||
if (jwkSetUri != null) {
|
||||
try {
|
||||
builder.jwkURL(jwkSetUri.toURI());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to convert '" + JoseHeaderNames.JKU + "' JOSE header to a URI", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> jwk = headers.getJwk();
|
||||
if (!CollectionUtils.isEmpty(jwk)) {
|
||||
try {
|
||||
builder.jwk(JWK.parse(jwk));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalArgumentException("Unable to convert '" + JoseHeaderNames.JWK + "' JOSE header",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
String keyId = headers.getKeyId();
|
||||
if (StringUtils.hasText(keyId)) {
|
||||
builder.keyID(keyId);
|
||||
}
|
||||
|
||||
URL x509Uri = headers.getX509Url();
|
||||
if (x509Uri != null) {
|
||||
try {
|
||||
builder.x509CertURL(x509Uri.toURI());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to convert '" + JoseHeaderNames.X5U + "' JOSE header to a URI", ex);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> x509CertificateChain = headers.getX509CertificateChain();
|
||||
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
|
||||
builder.x509CertChain(x509CertificateChain.stream().map(Base64::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
|
||||
if (StringUtils.hasText(x509SHA1Thumbprint)) {
|
||||
builder.x509CertThumbprint(new Base64URL(x509SHA1Thumbprint));
|
||||
}
|
||||
|
||||
String x509SHA256Thumbprint = headers.getX509SHA256Thumbprint();
|
||||
if (StringUtils.hasText(x509SHA256Thumbprint)) {
|
||||
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
|
||||
}
|
||||
|
||||
String type = headers.getType();
|
||||
if (StringUtils.hasText(type)) {
|
||||
builder.type(new JOSEObjectType(type));
|
||||
}
|
||||
|
||||
String contentType = headers.getContentType();
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
builder.contentType(contentType);
|
||||
}
|
||||
|
||||
Set<String> critical = headers.getCritical();
|
||||
if (!CollectionUtils.isEmpty(critical)) {
|
||||
builder.criticalParams(critical);
|
||||
}
|
||||
|
||||
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream()
|
||||
.filter((header) -> !JWEHeader.getRegisteredParameterNames().contains(header.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (!CollectionUtils.isEmpty(customHeaders)) {
|
||||
builder.customParams(customHeaders);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> {
|
||||
|
||||
@Override
|
||||
public JWTClaimsSet convert(JwtClaimsSet claims) {
|
||||
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
|
||||
|
||||
// NOTE: The value of the 'iss' claim is a String or URL (StringOrURI).
|
||||
Object issuer = claims.getClaim(JwtClaimNames.ISS);
|
||||
if (issuer != null) {
|
||||
builder.issuer(issuer.toString());
|
||||
}
|
||||
|
||||
String subject = claims.getSubject();
|
||||
if (StringUtils.hasText(subject)) {
|
||||
builder.subject(subject);
|
||||
}
|
||||
|
||||
List<String> audience = claims.getAudience();
|
||||
if (!CollectionUtils.isEmpty(audience)) {
|
||||
builder.audience(audience);
|
||||
}
|
||||
|
||||
Instant expiresAt = claims.getExpiresAt();
|
||||
if (expiresAt != null) {
|
||||
builder.expirationTime(Date.from(expiresAt));
|
||||
}
|
||||
|
||||
Instant notBefore = claims.getNotBefore();
|
||||
if (notBefore != null) {
|
||||
builder.notBeforeTime(Date.from(notBefore));
|
||||
}
|
||||
|
||||
Instant issuedAt = claims.getIssuedAt();
|
||||
if (issuedAt != null) {
|
||||
builder.issueTime(Date.from(issuedAt));
|
||||
}
|
||||
|
||||
String jwtId = claims.getId();
|
||||
if (StringUtils.hasText(jwtId)) {
|
||||
builder.jwtID(jwtId);
|
||||
}
|
||||
|
||||
Map<String, Object> customClaims = new HashMap<>();
|
||||
claims.getClaims().forEach((name, value) -> {
|
||||
if (!JWTClaimsSet.getRegisteredNames().contains(name)) {
|
||||
customClaims.put(name, value);
|
||||
}
|
||||
});
|
||||
if (!customClaims.isEmpty()) {
|
||||
customClaims.forEach(builder::claim);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
|
@ -42,8 +42,6 @@ import org.mockito.stubbing.Answer;
|
|||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
@ -54,74 +52,58 @@ import static org.mockito.BDDMockito.willAnswer;
|
|||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for {@link NimbusJwsEncoder}.
|
||||
* Tests for {@link NimbusJwtEncoder}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class NimbusJwsEncoderTests {
|
||||
public class NimbusJwtEncoderTests {
|
||||
|
||||
private List<JWK> jwkList;
|
||||
|
||||
private JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
private NimbusJwsEncoder jwsEncoder;
|
||||
private NimbusJwtEncoder jwtEncoder;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.jwkList = new ArrayList<>();
|
||||
this.jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(new JWKSet(this.jwkList));
|
||||
this.jwsEncoder = new NimbusJwsEncoder(this.jwkSource);
|
||||
this.jwtEncoder = new NimbusJwtEncoder(this.jwkSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenJwkSourceNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new NimbusJwsEncoder(null))
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new NimbusJwtEncoder(null))
|
||||
.withMessage("jwkSource cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenHeadersNullThenThrowIllegalArgumentException() {
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(null, jwtClaimsSet))
|
||||
.withMessage("headers cannot be null");
|
||||
public void encodeWhenParametersNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.jwtEncoder.encode(null))
|
||||
.withMessage("parameters cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenClaimsNullThenThrowIllegalArgumentException() {
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.jwsEncoder.encode(joseHeader, null))
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, null)))
|
||||
.withMessage("claims cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkSelectFailedThenThrowJwtEncodingException() throws Exception {
|
||||
this.jwkSource = mock(JWKSource.class);
|
||||
this.jwsEncoder = new NimbusJwsEncoder(this.jwkSource);
|
||||
this.jwtEncoder = new NimbusJwtEncoder(this.jwkSource);
|
||||
given(this.jwkSource.get(any(), any())).willThrow(new KeySourceException("key source error"));
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isThrownBy(() -> this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)))
|
||||
.withMessageContaining("Failed to select a JWK signing key -> key source error");
|
||||
}
|
||||
|
||||
|
@ -131,24 +113,40 @@ public class NimbusJwsEncoderTests {
|
|||
this.jwkList.add(rsaJwk);
|
||||
this.jwkList.add(rsaJwk);
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isThrownBy(() -> this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)))
|
||||
.withMessageContaining("Found multiple JWK signing keys for algorithm 'RS256'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkSelectEmptyThenThrowJwtEncodingException() {
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet))
|
||||
.isThrownBy(() -> this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)))
|
||||
.withMessageContaining("Failed to select a JWK signing key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenHeadersNotProvidedThenDefaulted() {
|
||||
// @formatter:off
|
||||
RSAKey rsaJwk = TestJwks.jwk(TestKeys.DEFAULT_PUBLIC_KEY, TestKeys.DEFAULT_PRIVATE_KEY)
|
||||
.keyID("rsa-jwk-1")
|
||||
.build();
|
||||
this.jwkList.add(rsaJwk);
|
||||
// @formatter:on
|
||||
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwtClaimsSet));
|
||||
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(SignatureAlgorithm.RS256);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeWhenJwkSelectWithProvidedKidThenSelected() {
|
||||
// @formatter:off
|
||||
|
@ -162,10 +160,10 @@ public class NimbusJwsEncoderTests {
|
|||
this.jwkList.add(rsaJwk2);
|
||||
// @formatter:on
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).keyId(rsaJwk2.getKeyID()).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).keyId(rsaJwk2.getKeyID()).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt encodedJws = this.jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
|
||||
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk2.getKeyID());
|
||||
}
|
||||
|
@ -185,11 +183,11 @@ public class NimbusJwsEncoderTests {
|
|||
this.jwkList.add(rsaJwk2);
|
||||
// @formatter:on
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256)
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
|
||||
.x509SHA256Thumbprint(rsaJwk1.getX509CertSHA256Thumbprint().toString()).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt encodedJws = this.jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
|
||||
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.X5T_S256))
|
||||
.isEqualTo(rsaJwk1.getX509CertSHA256Thumbprint().toString());
|
||||
|
@ -205,14 +203,15 @@ public class NimbusJwsEncoderTests {
|
|||
// @formatter:on
|
||||
|
||||
this.jwkSource = mock(JWKSource.class);
|
||||
this.jwsEncoder = new NimbusJwsEncoder(this.jwkSource);
|
||||
this.jwtEncoder = new NimbusJwtEncoder(this.jwkSource);
|
||||
given(this.jwkSource.get(any(), any())).willReturn(Collections.singletonList(rsaJwk));
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
assertThatExceptionOfType(JwtEncodingException.class)
|
||||
.isThrownBy(() -> this.jwsEncoder.encode(joseHeader, jwtClaimsSet)).withMessageContaining(
|
||||
.isThrownBy(() -> this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)))
|
||||
.withMessageContaining(
|
||||
"Failed to create a JWS Signer -> The JWK use must be sig (signature) or unspecified");
|
||||
}
|
||||
|
||||
|
@ -226,12 +225,12 @@ public class NimbusJwsEncoderTests {
|
|||
this.jwkList.add(rsaJwk);
|
||||
// @formatter:on
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt encodedJws = this.jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
|
||||
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(joseHeader.getAlgorithm());
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(jwsHeader.getAlgorithm());
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.JKU)).isNull();
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.JWK)).isNull();
|
||||
assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID());
|
||||
|
@ -266,15 +265,15 @@ public class NimbusJwsEncoderTests {
|
|||
return jwkSource.get(jwkSelector, context);
|
||||
}
|
||||
});
|
||||
NimbusJwsEncoder jwsEncoder = new NimbusJwsEncoder(jwkSourceDelegate);
|
||||
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSourceDelegate);
|
||||
|
||||
JwkListResultCaptor jwkListResultCaptor = new JwkListResultCaptor();
|
||||
willAnswer(jwkListResultCaptor).given(jwkSourceDelegate).get(any(), any());
|
||||
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet().build();
|
||||
|
||||
Jwt encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
Jwt encodedJws = jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
|
||||
|
||||
JWK jwk1 = jwkListResultCaptor.getResult().get(0);
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk1).toRSAPublicKey()).build();
|
||||
|
@ -282,7 +281,7 @@ public class NimbusJwsEncoderTests {
|
|||
|
||||
jwkSource.rotate(); // Simulate key rotation
|
||||
|
||||
encodedJws = jwsEncoder.encode(joseHeader, jwtClaimsSet);
|
||||
encodedJws = jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
|
||||
|
||||
JWK jwk2 = jwkListResultCaptor.getResult().get(0);
|
||||
jwtDecoder = NimbusJwtDecoder.withPublicKey(((RSAKey) jwk2).toRSAPublicKey()).build();
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
@ -22,36 +22,21 @@ import java.util.Map;
|
|||
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
final class TestJoseHeaders {
|
||||
public final class TestJwsHeaders {
|
||||
|
||||
private TestJoseHeaders() {
|
||||
private TestJwsHeaders() {
|
||||
}
|
||||
|
||||
static JoseHeader.Builder joseHeader() {
|
||||
return joseHeader(SignatureAlgorithm.RS256);
|
||||
public static JwsHeader.Builder jwsHeader() {
|
||||
return jwsHeader(SignatureAlgorithm.RS256);
|
||||
}
|
||||
|
||||
static JoseHeader.Builder joseHeader(SignatureAlgorithm signatureAlgorithm) {
|
||||
public static JwsHeader.Builder jwsHeader(SignatureAlgorithm signatureAlgorithm) {
|
||||
// @formatter:off
|
||||
return JoseHeader.withAlgorithm(signatureAlgorithm)
|
||||
return JwsHeader.with(signatureAlgorithm)
|
||||
.jwkSetUrl("https://provider.com/oauth2/jwks")
|
||||
.jwk(rsaJwk())
|
||||
.keyId("keyId")
|
|
@ -14,36 +14,21 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.oauth2.client.endpoint;
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* This originated in gh-9208 (JwtEncoder),
|
||||
* which is required to realize the feature in gh-8175 (JWT Client Authentication).
|
||||
* However, we decided not to merge gh-9208 as part of the 5.5.0 release
|
||||
* and instead packaged it up privately with the gh-8175 feature.
|
||||
* We MAY merge gh-9208 in a later release but that is yet to be determined.
|
||||
*
|
||||
* gh-9208 Introduce JwtEncoder
|
||||
* https://github.com/spring-projects/spring-security/pull/9208
|
||||
*
|
||||
* gh-8175 Support JWT for Client Authentication
|
||||
* https://github.com/spring-projects/spring-security/issues/8175
|
||||
*/
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
final class TestJwtClaimsSets {
|
||||
public final class TestJwtClaimsSets {
|
||||
|
||||
private TestJwtClaimsSets() {
|
||||
}
|
||||
|
||||
static JwtClaimsSet.Builder jwtClaimsSet() {
|
||||
public static JwtClaimsSet.Builder jwtClaimsSet() {
|
||||
String issuer = "https://provider.com";
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
|
Loading…
Reference in New Issue