Issuer id must be a URL according to specification (#30961)
fixes #30960 Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de> Signed-off-by: Captain-P-Goldfish <captain.p.goldfish@gmx.de>
This commit is contained in:
parent
e750b44e9d
commit
f3341390f4
2 changed files with 167 additions and 9 deletions
|
@ -22,13 +22,24 @@ import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.TextNode;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pojo to represent a VerifiableCredential for internal handling
|
* Pojo to represent a VerifiableCredential for internal handling
|
||||||
|
@ -42,14 +53,23 @@ public class VerifiableCredential {
|
||||||
public static final String VC_CONTEXT_V2 = "https://www.w3.org/ns/credentials/v2";
|
public static final String VC_CONTEXT_V2 = "https://www.w3.org/ns/credentials/v2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @context: The value of the @context property MUST be an ordered set where the first item is a URL with the
|
* @context: The value of the @context property MUST be an ordered set where the first item is a URL with the value
|
||||||
* value https://www.w3.org/ns/credentials/v2. Subsequent items in the ordered set MUST be composed of any
|
* https://www.w3.org/ns/credentials/v2. Subsequent items in the ordered set MUST be composed of any combination of
|
||||||
* combination of URLs and/or objects, where each is processable as a JSON-LD Context.
|
* URLs and/or objects, where each is processable as a JSON-LD Context.
|
||||||
*/
|
*/
|
||||||
@JsonProperty("@context")
|
@JsonProperty("@context")
|
||||||
private List<String> context = new ArrayList<>(List.of(VC_CONTEXT_V1));
|
private List<String> context = new ArrayList<>(List.of(VC_CONTEXT_V1));
|
||||||
private List<String> type = new ArrayList<>();
|
private List<String> type = new ArrayList<>();
|
||||||
private URI issuer;
|
|
||||||
|
/**
|
||||||
|
* The value of the issuer property MUST be either a URL, or an object containing an id property whose value is a
|
||||||
|
* URL; in either case, the issuer selects this URL to identify itself in a globally unambiguous way. It is
|
||||||
|
* RECOMMENDED that the URL be one which, if dereferenced, results in a controller document, as defined in
|
||||||
|
* [VC-DATA-INTEGRITY] or [VC-JOSE-COSE], about the issuer that can be used to verify the information expressed in
|
||||||
|
* the credential.
|
||||||
|
*/
|
||||||
|
@JsonDeserialize(using = IssuerDeserializer.class)
|
||||||
|
private Object issuer;
|
||||||
private Instant issuanceDate;
|
private Instant issuanceDate;
|
||||||
private URI id;
|
private URI id;
|
||||||
private Instant expirationDate;
|
private Instant expirationDate;
|
||||||
|
@ -62,6 +82,11 @@ public class VerifiableCredential {
|
||||||
return additionalProperties;
|
return additionalProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public VerifiableCredential setAdditionalProperties(Map<String, Object> additionalProperties) {
|
||||||
|
this.additionalProperties = additionalProperties;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@JsonAnySetter
|
@JsonAnySetter
|
||||||
public VerifiableCredential setAdditionalProperties(String name, Object property) {
|
public VerifiableCredential setAdditionalProperties(String name, Object property) {
|
||||||
additionalProperties.put(name, property);
|
additionalProperties.put(name, property);
|
||||||
|
@ -86,11 +111,37 @@ public class VerifiableCredential {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public URI getIssuer() {
|
public Object getIssuer() {
|
||||||
return issuer;
|
return issuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VerifiableCredential setIssuer(URI issuer) {
|
public VerifiableCredential setIssuer(Object issuer) {
|
||||||
|
if (issuer instanceof Map<?, ?> issuerMap) {
|
||||||
|
|
||||||
|
Optional.ofNullable(issuerMap).ifPresent(map -> {
|
||||||
|
String id = (String) Optional.ofNullable(map.get("id"))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException(
|
||||||
|
"id is a required field for issuer"));
|
||||||
|
try {
|
||||||
|
// id must be a URL: https://www.w3.org/TR/vc-data-model-2.0/#issuer
|
||||||
|
new URI(id);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new IllegalStateException("id must be a valid URI", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.issuer = issuerMap;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
this.issuer = new URI(String.valueOf(issuer));
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new IllegalStateException("id must be a valid URI", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VerifiableCredential setIssuerMap(Map<String, String> issuer) {
|
||||||
this.issuer = issuer;
|
this.issuer = issuer;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -131,8 +182,24 @@ public class VerifiableCredential {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VerifiableCredential setAdditionalProperties(Map<String, Object> additionalProperties) {
|
public static class IssuerDeserializer extends JsonDeserializer<Object> {
|
||||||
this.additionalProperties = additionalProperties;
|
|
||||||
return this;
|
@Override
|
||||||
|
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
JsonNode node = p.readValueAsTree();
|
||||||
|
if (node instanceof TextNode) {
|
||||||
|
try {
|
||||||
|
return new URI(node.textValue());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (node instanceof ObjectNode objectNode) {
|
||||||
|
return JsonSerialization.mapper.convertValue(objectNode, Map.class);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalArgumentException("Issuer must be a valid URI or a JSON object");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* 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.keycloak.protocol.oid4vc.model;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Pascal Knueppel
|
||||||
|
* @since 02.07.2024
|
||||||
|
*/
|
||||||
|
public class VerifiableCredentialTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIssuerIsDeserializedAsUri() throws IOException {
|
||||||
|
final String verifiableCredentialJson = """
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/credentials/v2",
|
||||||
|
"https://www.w3.org/ns/credentials/examples/v2"
|
||||||
|
],
|
||||||
|
"id": "http://university.example/credentials/3732",
|
||||||
|
"type": ["VerifiableCredential", "ExampleDegreeCredential"],
|
||||||
|
"issuer": "https://university.example/issuers/565049",
|
||||||
|
"validFrom": "2010-01-01T00:00:00Z",
|
||||||
|
"credentialSubject": {
|
||||||
|
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
|
||||||
|
"degree": {
|
||||||
|
"type": "ExampleBachelorDegree",
|
||||||
|
"name": "Bachelor of Science and Arts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
VerifiableCredential verifiableCredential = JsonSerialization.readValue(verifiableCredentialJson,
|
||||||
|
VerifiableCredential.class);
|
||||||
|
Assert.assertEquals(URI.class, verifiableCredential.getIssuer().getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeserializeIssuerAsMap() throws IOException {
|
||||||
|
final String verifiableCredentialJson = """
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/credentials/v2",
|
||||||
|
"https://www.w3.org/ns/credentials/examples/v2"
|
||||||
|
],
|
||||||
|
"id": "http://university.example/credentials/3732",
|
||||||
|
"type": ["VerifiableCredential", "ExampleDegreeCredential"],
|
||||||
|
"issuer": {
|
||||||
|
"id": "https://university.example/issuers/565049",
|
||||||
|
"name": "Example University",
|
||||||
|
"description": "A public university focusing on teaching examples."
|
||||||
|
},
|
||||||
|
"validFrom": "2015-05-10T12:30:00Z",
|
||||||
|
"name": "Example University Degree",
|
||||||
|
"description": "2015 Bachelor of Science and Arts Degree",
|
||||||
|
"credentialSubject": {
|
||||||
|
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
|
||||||
|
"degree": {
|
||||||
|
"type": "ExampleBachelorDegree",
|
||||||
|
"name": "Bachelor of Science and Arts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
VerifiableCredential verifiableCredential = JsonSerialization.readValue(verifiableCredentialJson,
|
||||||
|
VerifiableCredential.class);
|
||||||
|
Assert.assertTrue(Map.class.isAssignableFrom(verifiableCredential.getIssuer().getClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue