18 changed files with 1069 additions and 457 deletions
@ -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"); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -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()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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"); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue