parent
d8a678df6f
commit
c986b6f4b5
|
@ -0,0 +1,8 @@
|
|||
apply plugin: 'io.spring.convention.spring-module'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-core')
|
||||
compile project(':spring-security-oauth2-core')
|
||||
compile springCoreDependency
|
||||
compile 'com.nimbusds:nimbus-jose-jwt'
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.jose.jws;
|
||||
|
||||
/**
|
||||
* The cryptographic algorithms defined by the <i>JSON Web Algorithms (JWA)</i> specification
|
||||
* and used by <i>JSON Web Signature (JWS)</i> to digitally sign or create a MAC
|
||||
* of the contents of the JWS Protected Header and JWS Payload.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
|
||||
*/
|
||||
public interface JwsAlgorithm {
|
||||
|
||||
/**
|
||||
* HMAC using SHA-256 (Required)
|
||||
*/
|
||||
String HS256 = "HS256";
|
||||
|
||||
/**
|
||||
* HMAC using SHA-384 (Optional)
|
||||
*/
|
||||
String HS384 = "HS384";
|
||||
|
||||
/**
|
||||
* HMAC using SHA-512 (Optional)
|
||||
*/
|
||||
String HS512 = "HS512";
|
||||
|
||||
/**
|
||||
* RSASSA-PKCS1-v1_5 using SHA-256 (Recommended)
|
||||
*/
|
||||
String RS256 = "RS256";
|
||||
|
||||
/**
|
||||
* RSASSA-PKCS1-v1_5 using SHA-384 (Optional)
|
||||
*/
|
||||
String RS384 = "RS384";
|
||||
|
||||
/**
|
||||
* RSASSA-PKCS1-v1_5 using SHA-512 (Optional)
|
||||
*/
|
||||
String RS512 = "RS512";
|
||||
|
||||
/**
|
||||
* ECDSA using P-256 and SHA-256 (Recommended+)
|
||||
*/
|
||||
String ES256 = "ES256";
|
||||
|
||||
/**
|
||||
* ECDSA using P-384 and SHA-384 (Optional)
|
||||
*/
|
||||
String ES384 = "ES384";
|
||||
|
||||
/**
|
||||
* ECDSA using P-521 and SHA-512 (Optional)
|
||||
*/
|
||||
String ES512 = "ES512";
|
||||
|
||||
/**
|
||||
* RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (Optional)
|
||||
*/
|
||||
String PS256 = "PS256";
|
||||
|
||||
/**
|
||||
* RSASSA-PSS using SHA-384 and MGF1 with SHA-384 (Optional)
|
||||
*/
|
||||
String PS384 = "PS384";
|
||||
|
||||
/**
|
||||
* RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (Optional)
|
||||
*/
|
||||
String PS512 = "PS512";
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.jwt;
|
||||
|
||||
import org.springframework.security.oauth2.core.AbstractToken;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractToken} representing a <i>JSON Web Token (JWT)</i>.
|
||||
*
|
||||
* <p>
|
||||
* JWTs represent a set of "Claims" as a JSON object that is encoded in a
|
||||
* <i>JSON Web Signature (JWS)</i> and/or <i>JSON Web Encryption (JWE)</i> structure.
|
||||
* The JSON object, also known as the <i>JWT Claims Set</i>, consists of one or more Claim Name/Claim Value pairs.
|
||||
* The Claim Name is a <code>String</code> and the Claim Value is an arbitrary JSON object.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AbstractToken
|
||||
* @see JwtClaimAccessor
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
|
||||
*/
|
||||
public class Jwt extends AbstractToken implements JwtClaimAccessor {
|
||||
private final Map<String, Object> headers;
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
public Jwt(String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||
Map<String, Object> headers, Map<String, Object> claims) {
|
||||
super(tokenValue, issuedAt, expiresAt);
|
||||
Assert.notEmpty(headers, "headers cannot be empty");
|
||||
Assert.notEmpty(claims, "claims cannot be empty");
|
||||
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
|
||||
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
|
||||
}
|
||||
|
||||
public Map<String, Object> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.jwt;
|
||||
|
||||
/**
|
||||
* The "Registered Claim Names" defined by the <i>JSON Web Token (JWT)</i> specification
|
||||
* that may be contained in the JSON object <i>JWT Claims Set</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see JwtClaimAccessor
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims</a>
|
||||
*/
|
||||
public interface JwtClaim {
|
||||
|
||||
String ISS = "iss";
|
||||
|
||||
String SUB = "sub";
|
||||
|
||||
String AUD = "aud";
|
||||
|
||||
String EXP = "exp";
|
||||
|
||||
String NBF = "nbf";
|
||||
|
||||
String IAT = "iat";
|
||||
|
||||
String JTI = "jti";
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.jwt;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* A {@link ClaimAccessor} for the "Registered Claim Names"
|
||||
* that may be contained in the JSON object <i>JWT Claims Set</i> of a <i>JSON Web Token (JWT)</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see ClaimAccessor
|
||||
* @see JwtClaim
|
||||
* @see Jwt
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4.1">Registered Claim Names</a>
|
||||
*/
|
||||
public interface JwtClaimAccessor extends ClaimAccessor {
|
||||
|
||||
default URI getIssuer() {
|
||||
return this.getClaimAsURI(JwtClaim.ISS);
|
||||
}
|
||||
|
||||
default String getSubject() {
|
||||
return this.getClaimAsString(JwtClaim.SUB);
|
||||
}
|
||||
|
||||
default String getAudience() {
|
||||
// FIXME Should return String[]
|
||||
return this.getClaimAsString(JwtClaim.AUD);
|
||||
}
|
||||
|
||||
default Instant getExpiresAt() {
|
||||
return this.getClaimAsInstant(JwtClaim.EXP);
|
||||
}
|
||||
|
||||
default Instant getNotBefore() {
|
||||
return this.getClaimAsInstant(JwtClaim.NBF);
|
||||
}
|
||||
|
||||
default Instant getIssuedAt() {
|
||||
return this.getClaimAsInstant(JwtClaim.IAT);
|
||||
}
|
||||
|
||||
default String getId() {
|
||||
return this.getClaimAsString(JwtClaim.JTI);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.jwt;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for "decoding"
|
||||
* a <i>JSON Web Token (JWT)</i> from it's compact claims representation format to a {@link Jwt}.
|
||||
*
|
||||
* <p>
|
||||
* JWTs may be represented using the JWS Compact Serialization format for a
|
||||
* <i>JSON Web Signature (JWS)</i> structure or JWE Compact Serialization format for a
|
||||
* <i>JSON Web Encryption (JWE)</i> structure. Therefore, implementors are responsible
|
||||
* for verifying a JWS and/or decrypting a JWE.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see Jwt
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-3.1">JWE Compact Serialization</a>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface JwtDecoder {
|
||||
|
||||
Jwt decode(String token) throws JwtException;
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.jwt;
|
||||
|
||||
/**
|
||||
* Base exception for all <i>JSON Web Token (JWT)</i> related errors.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public class JwtException extends RuntimeException {
|
||||
|
||||
public JwtException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JwtException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.jwt.nimbus;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
|
||||
import com.nimbusds.jose.proc.JWSKeySelector;
|
||||
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.JWTParser;
|
||||
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
|
||||
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
|
||||
import org.springframework.security.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.jwt.Jwt;
|
||||
import org.springframework.security.jwt.JwtDecoder;
|
||||
import org.springframework.security.jwt.JwtException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An implementation of a {@link JwtDecoder} that "decodes" a
|
||||
* <i>JSON Web Token (JWT)</i> and additionally verifies it's digital signature if the JWT is a
|
||||
* <i>JSON Web Signature (JWS)</i>. The public key used for verification is obtained from the
|
||||
* <i>JSON Web Key (JWK)</i> Set <code>URL</code> which is supplied via the constructor.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation uses the <b>Nimbus JOSE + JWT SDK</b> internally.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see JwtDecoder
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a>
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
|
||||
*/
|
||||
public class NimbusJwtDecoderJwkSupport implements JwtDecoder {
|
||||
private final URL jwkSetUrl;
|
||||
private final JWSAlgorithm jwsAlgorithm;
|
||||
private final ConfigurableJWTProcessor<SecurityContext> jwtProcessor;
|
||||
|
||||
public NimbusJwtDecoderJwkSupport(String jwkSetUrl) {
|
||||
this(jwkSetUrl, JwsAlgorithm.RS256);
|
||||
}
|
||||
|
||||
public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) {
|
||||
Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
|
||||
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
|
||||
try {
|
||||
this.jwkSetUrl = new URL(jwkSetUrl);
|
||||
} catch (MalformedURLException ex) {
|
||||
throw new IllegalArgumentException("Invalid JWK Set URL: " + ex.getMessage(), ex);
|
||||
}
|
||||
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
|
||||
|
||||
this.jwtProcessor = new DefaultJWTProcessor<>();
|
||||
JWKSource jwkSource = new RemoteJWKSet(this.jwkSetUrl);
|
||||
JWSKeySelector<SecurityContext> jwsKeySelector =
|
||||
new JWSVerificationKeySelector<SecurityContext>(this.jwsAlgorithm, jwkSource);
|
||||
this.jwtProcessor.setJWSKeySelector(jwsKeySelector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Jwt decode(String token) throws JwtException {
|
||||
Jwt jwt;
|
||||
|
||||
try {
|
||||
JWT parsedJwt = JWTParser.parse(token);
|
||||
|
||||
// Verify the signature
|
||||
JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
|
||||
|
||||
Instant expiresAt = jwtClaimsSet.getExpirationTime().toInstant();
|
||||
Instant issuedAt;
|
||||
if (jwtClaimsSet.getIssueTime() != null) {
|
||||
issuedAt = jwtClaimsSet.getIssueTime().toInstant();
|
||||
} else {
|
||||
// issuedAt is required in AbstractToken so let's default to expiresAt - 1 second
|
||||
issuedAt = Instant.from(expiresAt).minusSeconds(1);
|
||||
}
|
||||
|
||||
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
|
||||
|
||||
jwt = new Jwt(token, issuedAt, expiresAt, headers, jwtClaimsSet.getClaims());
|
||||
|
||||
} catch (Exception ex) {
|
||||
throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
return jwt;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An "accessor" for a set of claims that may be used for assertions.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ClaimAccessor {
|
||||
|
||||
Map<String, Object> getClaims();
|
||||
|
||||
default Boolean containsClaim(String claim) {
|
||||
Assert.notNull(claim, "claim cannot be null");
|
||||
return this.getClaims().containsKey(claim);
|
||||
}
|
||||
|
||||
default String getClaimAsString(String claim) {
|
||||
return (this.containsClaim(claim) ? this.getClaims().get(claim).toString() : null);
|
||||
}
|
||||
|
||||
default Boolean getClaimAsBoolean(String claim) {
|
||||
return (this.containsClaim(claim) ? Boolean.valueOf(this.getClaimAsString(claim)) : null);
|
||||
}
|
||||
|
||||
default Instant getClaimAsInstant(String claim) {
|
||||
if (!this.containsClaim(claim)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Instant.ofEpochSecond(Long.valueOf(this.getClaimAsString(claim)));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to Instant: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
default URI getClaimAsURI(String claim) {
|
||||
if (!this.containsClaim(claim)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URI(this.getClaimAsString(claim));
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to URI: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue