18 changed files with 1069 additions and 457 deletions
@ -1,123 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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