Added endorsement inspector for OB 2.0
This commit is contained in:
parent
55ef973efe
commit
cb60fd11ed
@ -0,0 +1,138 @@
|
||||
package org.oneedtech.inspect.vc;
|
||||
|
||||
import static org.oneedtech.inspect.core.probe.RunContext.Key.GENERATED_OBJECT_BUILDER;
|
||||
import static org.oneedtech.inspect.core.probe.RunContext.Key.JACKSON_OBJECTMAPPER;
|
||||
import static org.oneedtech.inspect.core.probe.RunContext.Key.JSONPATH_EVALUATOR;
|
||||
import static org.oneedtech.inspect.core.probe.RunContext.Key.JSON_DOCUMENT_LOADER;
|
||||
import static org.oneedtech.inspect.core.probe.RunContext.Key.JWT_CREDENTIAL_NODE_NAME;
|
||||
import static org.oneedtech.inspect.core.probe.RunContext.Key.PNG_CREDENTIAL_KEY;
|
||||
import static org.oneedtech.inspect.core.probe.RunContext.Key.SVG_CREDENTIAL_QNAME;
|
||||
import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
|
||||
import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
|
||||
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
|
||||
import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.oneedtech.inspect.core.SubInspector;
|
||||
import org.oneedtech.inspect.core.probe.GeneratedObject;
|
||||
import org.oneedtech.inspect.core.probe.Probe;
|
||||
import org.oneedtech.inspect.core.probe.RunContext;
|
||||
import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
|
||||
import org.oneedtech.inspect.core.report.Report;
|
||||
import org.oneedtech.inspect.core.report.ReportItems;
|
||||
import org.oneedtech.inspect.util.json.ObjectMapperCache;
|
||||
import org.oneedtech.inspect.util.resource.Resource;
|
||||
import org.oneedtech.inspect.vc.Assertion.Type;
|
||||
import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
|
||||
import org.oneedtech.inspect.vc.payload.PngParser;
|
||||
import org.oneedtech.inspect.vc.payload.SvgParser;
|
||||
import org.oneedtech.inspect.vc.probe.AssertionRevocationListProbe;
|
||||
import org.oneedtech.inspect.vc.probe.ExpirationProbe;
|
||||
import org.oneedtech.inspect.vc.probe.IssuanceProbe;
|
||||
import org.oneedtech.inspect.vc.probe.VerificationDependenciesProbe;
|
||||
|
||||
import com.apicatalog.jsonld.loader.DocumentLoader;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* An inspector for EndorsementCredential objects.
|
||||
* @author mgylling
|
||||
*/
|
||||
public class OB20EndorsementInspector extends VCInspector implements SubInspector {
|
||||
|
||||
private DocumentLoader documentLoader;
|
||||
|
||||
protected OB20EndorsementInspector(OB20EndorsementInspector.Builder builder) {
|
||||
super(builder);
|
||||
this.documentLoader = builder.documentLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Report run(Resource resource, Map<String, GeneratedObject> parentObjects) {
|
||||
|
||||
/*
|
||||
* The resource param is the top-level credential that embeds the endorsement, we
|
||||
* expect parentObjects to provide a pointer to the JsonNode we should check.
|
||||
*
|
||||
* The parent inspector is responsible to decode away possible jwt-ness, so that
|
||||
* what we get here is a verbatim json node.
|
||||
*
|
||||
*/
|
||||
|
||||
Assertion endorsement = (Assertion) checkNotNull(parentObjects.get(CREDENTIAL_KEY));
|
||||
|
||||
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
|
||||
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
|
||||
|
||||
RunContext ctx = new RunContext.Builder()
|
||||
.put(this)
|
||||
.put(resource)
|
||||
.put(JACKSON_OBJECTMAPPER, mapper)
|
||||
.put(JSONPATH_EVALUATOR, jsonPath)
|
||||
.put(GENERATED_OBJECT_BUILDER, new Assertion.Builder())
|
||||
.put(PNG_CREDENTIAL_KEY, PngParser.Keys.OB20)
|
||||
.put(SVG_CREDENTIAL_QNAME, SvgParser.QNames.OB20)
|
||||
.put(JSON_DOCUMENT_LOADER, documentLoader)
|
||||
.put(JWT_CREDENTIAL_NODE_NAME, Assertion.JWT_NODE_NAME)
|
||||
.build();
|
||||
|
||||
parentObjects.entrySet().stream().forEach(entry -> {
|
||||
if (!entry.getKey().equals(CREDENTIAL_KEY)) {
|
||||
ctx.addGeneratedObject(entry.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
List<ReportItems> accumulator = new ArrayList<>();
|
||||
int probeCount = 0;
|
||||
try {
|
||||
|
||||
JsonNode endorsementNode = endorsement.getJson();
|
||||
// verification and revocation
|
||||
if (endorsement.getCredentialType() == Type.Endorsement) {
|
||||
for(Probe<JsonLdGeneratedObject> probe : List.of(new VerificationDependenciesProbe(endorsementNode.get("id").asText(), "claim"),
|
||||
new AssertionRevocationListProbe(endorsementNode.get("id").asText(), "claim"))) {
|
||||
probeCount++;
|
||||
accumulator.add(probe.run(new JsonLdGeneratedObject(endorsementNode.toString()), ctx));
|
||||
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
|
||||
}
|
||||
}
|
||||
|
||||
// expiration and issuance
|
||||
for(Probe<Credential> probe : List.of(
|
||||
new ExpirationProbe(), new IssuanceProbe())) {
|
||||
probeCount++;
|
||||
accumulator.add(probe.run(endorsement, ctx));
|
||||
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
|
||||
}
|
||||
|
||||
return new Report(ctx, new ReportItems(accumulator), probeCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R extends Resource> Report run(R resource) {
|
||||
throw new IllegalStateException("must use #run(resource, map)");
|
||||
}
|
||||
|
||||
public static class Builder extends VCInspector.Builder<OB20EndorsementInspector.Builder> {
|
||||
private DocumentLoader documentLoader;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public OB20EndorsementInspector build() {
|
||||
return new OB20EndorsementInspector(this);
|
||||
}
|
||||
|
||||
public Builder documentLoader(DocumentLoader documentLoader) {
|
||||
this.documentLoader = documentLoader;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -4,12 +4,20 @@ import static java.lang.Boolean.TRUE;
|
||||
import static org.oneedtech.inspect.core.Inspector.Behavior.RESET_CACHES_ON_RUN;
|
||||
import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
|
||||
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
|
||||
import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY;
|
||||
import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.oneedtech.inspect.core.Inspector;
|
||||
import org.oneedtech.inspect.core.probe.Outcome;
|
||||
import org.oneedtech.inspect.core.probe.GeneratedObject;
|
||||
import org.oneedtech.inspect.core.probe.Probe;
|
||||
import org.oneedtech.inspect.core.probe.RunContext;
|
||||
import org.oneedtech.inspect.core.probe.RunContext.Key;
|
||||
@ -20,6 +28,7 @@ import org.oneedtech.inspect.schema.JsonSchemaCache;
|
||||
import org.oneedtech.inspect.util.json.ObjectMapperCache;
|
||||
import org.oneedtech.inspect.util.resource.Resource;
|
||||
import org.oneedtech.inspect.util.resource.ResourceType;
|
||||
import org.oneedtech.inspect.util.resource.UriResource;
|
||||
import org.oneedtech.inspect.util.spec.Specification;
|
||||
import org.oneedtech.inspect.vc.Assertion.Type;
|
||||
import org.oneedtech.inspect.vc.Credential.CredentialEnum;
|
||||
@ -41,38 +50,25 @@ import org.oneedtech.inspect.vc.probe.validation.ValidationPropertyProbeFactory;
|
||||
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
|
||||
|
||||
import com.apicatalog.jsonld.loader.DocumentLoader;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import foundation.identity.jsonld.ConfigurableDocumentLoader;
|
||||
|
||||
/**
|
||||
* A verifier for Open Badges 2.0.
|
||||
* @author xaracil
|
||||
*/
|
||||
public class OB20Inspector extends Inspector {
|
||||
public class OB20Inspector extends VCInspector {
|
||||
|
||||
protected OB20Inspector(OB20Inspector.Builder builder) {
|
||||
protected <B extends VCInspector.Builder<?>> OB20Inspector(B builder) {
|
||||
super(builder);
|
||||
}
|
||||
|
||||
protected Report abort(RunContext ctx, List<ReportItems> accumulator, int probeCount) {
|
||||
return new Report(ctx, new ReportItems(accumulator), probeCount);
|
||||
}
|
||||
|
||||
protected boolean broken(List<ReportItems> accumulator) {
|
||||
return broken(accumulator, false);
|
||||
}
|
||||
|
||||
protected boolean broken(List<ReportItems> accumulator, boolean force) {
|
||||
if(!force && getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) {
|
||||
return false;
|
||||
}
|
||||
for(ReportItems items : accumulator) {
|
||||
if(items.contains(Outcome.FATAL, Outcome.EXCEPTION)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.oneedtech.inspect.core.Inspector#run(org.oneedtech.inspect.util.resource.Resource)
|
||||
*/
|
||||
@Override
|
||||
public Report run(Resource resource) {
|
||||
super.check(resource);
|
||||
@ -143,14 +139,6 @@ public class OB20Inspector extends Inspector {
|
||||
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
|
||||
}
|
||||
|
||||
// expiration and issuance
|
||||
for(Probe<Credential> probe : List.of(
|
||||
new ExpirationProbe(), new IssuanceProbe())) {
|
||||
probeCount++;
|
||||
accumulator.add(probe.run(assertion, ctx));
|
||||
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
|
||||
}
|
||||
|
||||
// verification and revocation
|
||||
if (assertion.getCredentialType() == Type.Assertion) {
|
||||
for(Probe<JsonLdGeneratedObject> probe : List.of(new VerificationDependenciesProbe(assertionNode.get("id").asText()),
|
||||
@ -168,6 +156,47 @@ public class OB20Inspector extends Inspector {
|
||||
}
|
||||
}
|
||||
|
||||
// expiration and issuance
|
||||
for(Probe<Credential> probe : List.of(
|
||||
new ExpirationProbe(), new IssuanceProbe())) {
|
||||
probeCount++;
|
||||
accumulator.add(probe.run(assertion, ctx));
|
||||
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
|
||||
}
|
||||
|
||||
// Embedded endorsements. Pass document loader because it has already cached documents, and it has localdomains for testing
|
||||
OB20EndorsementInspector endorsementInspector = new OB20EndorsementInspector.Builder().documentLoader(documentLoader).build();
|
||||
|
||||
// get endorsements for all JSON_LD objects in the graph
|
||||
List<JsonNode> endorsements = ctx.getGeneratedObjects().values().stream()
|
||||
.filter(generatedObject -> generatedObject instanceof JsonLdGeneratedObject)
|
||||
.flatMap(obj -> {
|
||||
JsonNode node;
|
||||
try {
|
||||
node = mapper.readTree(((JsonLdGeneratedObject) obj).getJson());
|
||||
// return endorsement node, filtering out the on inside @context
|
||||
return asNodeList(node, "$..endorsement", jsonPath).stream().filter(endorsementNode -> !endorsementNode.isObject());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Couldn't not parse " + obj.getId() + ": contains invalid JSON");
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for(JsonNode node : endorsements) {
|
||||
probeCount++;
|
||||
// get endorsement json from context
|
||||
UriResource uriResource = resolveUriResource(ctx, node.asText());
|
||||
JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
|
||||
if (resolved == null) {
|
||||
throw new IllegalArgumentException("endorsement " + node.toString() + " not found in graph");
|
||||
}
|
||||
|
||||
Assertion endorsement = new Assertion.Builder().resource(resource).jsonData(mapper.readTree(resolved.getJson())).build();
|
||||
// pass graph to subinspector
|
||||
Map<String, GeneratedObject> parentObjects = new HashMap<>(ctx.getGeneratedObjects());
|
||||
parentObjects.put(CREDENTIAL_KEY, endorsement);
|
||||
accumulator.add(endorsementInspector.run(resource, parentObjects));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
|
||||
@ -176,11 +205,7 @@ public class OB20Inspector extends Inspector {
|
||||
return new Report(ctx, new ReportItems(accumulator), probeCount);
|
||||
}
|
||||
|
||||
protected DocumentLoader getDocumentLoader() {
|
||||
return new CachingDocumentLoader();
|
||||
}
|
||||
|
||||
public static class Builder extends Inspector.Builder<OB20Inspector.Builder> {
|
||||
public static class Builder extends VCInspector.Builder<OB20Inspector.Builder> {
|
||||
|
||||
public Builder() {
|
||||
super();
|
||||
@ -204,4 +229,19 @@ public class OB20Inspector extends Inspector {
|
||||
public static final String ALLOW_LOCAL_REDIRECTION = "ALLOW_LOCAL_REDIRECTION";
|
||||
}
|
||||
|
||||
protected UriResource resolveUriResource(RunContext ctx, String url) throws URISyntaxException {
|
||||
URI uri = new URI(url);
|
||||
UriResource initialUriResource = new UriResource(uri);
|
||||
UriResource uriResource = initialUriResource;
|
||||
|
||||
// check if uri points to a local resource
|
||||
if (ctx.get(Key.JSON_DOCUMENT_LOADER) instanceof ConfigurableDocumentLoader) {
|
||||
if (ConfigurableDocumentLoader.getDefaultHttpLoader() instanceof CachingDocumentLoader.HttpLoader) {
|
||||
URI resolvedUri = ((CachingDocumentLoader.HttpLoader) ConfigurableDocumentLoader.getDefaultHttpLoader()).resolve(uri);
|
||||
uriResource = new UriResource(resolvedUri);
|
||||
}
|
||||
}
|
||||
return uriResource;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ import org.oneedtech.inspect.core.probe.Probe;
|
||||
import org.oneedtech.inspect.core.probe.RunContext;
|
||||
import org.oneedtech.inspect.core.report.Report;
|
||||
import org.oneedtech.inspect.core.report.ReportItems;
|
||||
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
|
||||
|
||||
import com.apicatalog.jsonld.loader.DocumentLoader;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
@ -61,6 +63,14 @@ public abstract class VCInspector extends Inspector {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a caching document loader for loading json resources
|
||||
* @return document loader for loading json resources
|
||||
*/
|
||||
protected DocumentLoader getDocumentLoader() {
|
||||
return new CachingDocumentLoader();
|
||||
}
|
||||
|
||||
protected static final String REFRESHED = "is.refreshed.credential";
|
||||
|
||||
public abstract static class Builder<B extends VCInspector.Builder<B>> extends Inspector.Builder<B> {
|
||||
|
@ -25,10 +25,16 @@ import foundation.identity.jsonld.ConfigurableDocumentLoader;
|
||||
|
||||
public class AssertionRevocationListProbe extends Probe<JsonLdGeneratedObject> {
|
||||
private final String assertionId;
|
||||
private final String propertyName;
|
||||
|
||||
public AssertionRevocationListProbe(String assertionId) {
|
||||
this(assertionId, "badge");
|
||||
}
|
||||
|
||||
public AssertionRevocationListProbe(String assertionId, String propertyName) {
|
||||
super(ID);
|
||||
this.assertionId = assertionId;
|
||||
this.propertyName = propertyName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -37,7 +43,7 @@ public class AssertionRevocationListProbe extends Probe<JsonLdGeneratedObject> {
|
||||
JsonNode jsonNode = (mapper).readTree(jsonLdGeneratedObject.getJson());
|
||||
|
||||
// get badge
|
||||
UriResource badgeUriResource = resolveUriResource(ctx, jsonNode.get("badge").asText().strip());
|
||||
UriResource badgeUriResource = resolveUriResource(ctx, getBadgeClaimId(jsonNode));
|
||||
JsonLdGeneratedObject badgeObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
|
||||
JsonLDCompactionProve.getId(badgeUriResource));
|
||||
|
||||
@ -97,5 +103,18 @@ public class AssertionRevocationListProbe extends Probe<JsonLdGeneratedObject> {
|
||||
return uriResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the node with name propertyName
|
||||
* @param jsonNode node
|
||||
* @return ID of the node. If node is textual, the text is returned. If node is an object, its "ID" attribute is returned
|
||||
*/
|
||||
protected String getBadgeClaimId(JsonNode jsonNode) {
|
||||
JsonNode propertyNode = jsonNode.get(propertyName);
|
||||
if (propertyNode.isTextual()) {
|
||||
return propertyNode.asText().strip();
|
||||
}
|
||||
return propertyNode.get("id").asText().strip();
|
||||
}
|
||||
|
||||
public static final String ID = AssertionRevocationListProbe.class.getSimpleName();
|
||||
}
|
||||
|
@ -28,12 +28,19 @@ import foundation.identity.jsonld.ConfigurableDocumentLoader;
|
||||
*/
|
||||
public class VerificationDependenciesProbe extends Probe<JsonLdGeneratedObject> {
|
||||
private final String assertionId;
|
||||
private final String propertyName;
|
||||
|
||||
public VerificationDependenciesProbe(String assertionId) {
|
||||
this(assertionId, "badge");
|
||||
}
|
||||
|
||||
public VerificationDependenciesProbe(String assertionId, String propertyName) {
|
||||
super(ID);
|
||||
this.assertionId = assertionId;
|
||||
this.propertyName = propertyName;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ReportItems run(JsonLdGeneratedObject jsonLdGeneratedObject, RunContext ctx) throws Exception {
|
||||
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
|
||||
@ -56,7 +63,7 @@ public class VerificationDependenciesProbe extends Probe<JsonLdGeneratedObject>
|
||||
|
||||
if ("HostedBadge".equals(type)) {
|
||||
// get badge
|
||||
UriResource badgeUriResource = resolveUriResource(ctx, jsonNode.get("badge").asText().strip());
|
||||
UriResource badgeUriResource = resolveUriResource(ctx, getBadgeClaimId(jsonNode));
|
||||
JsonLdGeneratedObject badgeObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
|
||||
JsonLDCompactionProve.getId(badgeUriResource));
|
||||
JsonNode badgeNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
|
||||
@ -104,7 +111,7 @@ public class VerificationDependenciesProbe extends Probe<JsonLdGeneratedObject>
|
||||
allowedOrigins = List.of(defaultAllowedOrigins);
|
||||
}
|
||||
} else {
|
||||
JsonNodeUtil.asStringList(allowedOriginsNode);
|
||||
allowedOrigins = JsonNodeUtil.asStringList(allowedOriginsNode);
|
||||
}
|
||||
|
||||
if (allowedOrigins == null || allowedOrigins.isEmpty() || !issuerId.startsWith("http")) {
|
||||
@ -147,6 +154,20 @@ public class VerificationDependenciesProbe extends Probe<JsonLdGeneratedObject>
|
||||
return uriResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the node with name propertyName
|
||||
* @param jsonNode node
|
||||
* @return ID of the node. If node is textual, the text is returned. If node is an object, its "ID" attribute is returned
|
||||
*/
|
||||
protected String getBadgeClaimId(JsonNode jsonNode) {
|
||||
JsonNode propertyNode = jsonNode.get(propertyName);
|
||||
if (propertyNode.isTextual()) {
|
||||
return propertyNode.asText().strip();
|
||||
}
|
||||
return propertyNode.get("id").asText().strip();
|
||||
}
|
||||
|
||||
|
||||
public static final String ID = VerificationDependenciesProbe.class.getSimpleName();
|
||||
|
||||
}
|
||||
|
@ -11,7 +11,16 @@
|
||||
"image": "https://example.org/beths-robot-badge.png",
|
||||
"evidence": "https://example.org/beths-robot-work.html",
|
||||
"issuedOn": "2016-12-31T23:59:59Z",
|
||||
"badge": "https://example.org/robotics-badge.json",
|
||||
"badge": {
|
||||
"type": "BadgeClass",
|
||||
"id": "https://example.org/badgeclass-with-endorsements.json",
|
||||
"name": "Awesome Robotics Badge",
|
||||
"description": "For doing awesome things with robots that people think is pretty great.",
|
||||
"image": "https://example.org/robotics-badge.png",
|
||||
"criteria": "https://example.org/badgecriteria.json",
|
||||
"issuer": "https://example.org/organization.json",
|
||||
"endorsement": ["https://example.org/endorsement-3.json", "https://example.org/endorsement-4.json"]
|
||||
},
|
||||
"verification": {
|
||||
"type": "hosted"
|
||||
},
|
||||
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"@context": "https://w3id.org/openbadges/v2",
|
||||
"type": "BadgeClass",
|
||||
"id": "https://example.org/badgeclass-with-endorsements.json",
|
||||
"name": "Awesome Robotics Badge",
|
||||
"description": "For doing awesome things with robots that people think is pretty great.",
|
||||
"image": "https://example.org/robotics-badge.png",
|
||||
"criteria": "https://example.org/badgecriteria.json",
|
||||
"issuer": "https://example.org/organization.json",
|
||||
"endorsement": ["https://example.org/endorsement-3.json", "https://example.org/endorsement-4.json"]
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
"id": "http://example.org/endorsement-1.json",
|
||||
"type": "Endorsement",
|
||||
"claim": {
|
||||
"id": "https://example.org/robotics-badge.json",
|
||||
"id": "https://example.org/badgeclass-with-endorsements.json",
|
||||
"endorsementComment": "Pretty good"
|
||||
},
|
||||
"issuedOn": "2017-10-01T00:00Z",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"id": "http://example.org/endorsement-2.json",
|
||||
"type": "Endorsement",
|
||||
"claim": {
|
||||
"id": "https://example.org/robotics-badge.json",
|
||||
"id": "https://example.org/badgeclass-with-endorsements.json",
|
||||
"endorsementComment": "Pretty good"
|
||||
},
|
||||
"issuedOn": "2017-10-01T00:00Z",
|
||||
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"@context": "https://w3id.org/openbadges/v2",
|
||||
"id": "http://example.org/endorsement-3.json",
|
||||
"type": "Endorsement",
|
||||
"claim": {
|
||||
"id": "https://example.org/badgeclass-with-endorsements.json",
|
||||
"endorsementComment": "Pretty good"
|
||||
},
|
||||
"issuedOn": "2017-10-01T00:00Z",
|
||||
"issuer": "http://example.org/issuer1.json",
|
||||
"verification": {
|
||||
"type": "HostedBadge"
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"@context": "https://w3id.org/openbadges/v2",
|
||||
"id": "http://example.org/endorsement-4.json",
|
||||
"type": "Endorsement",
|
||||
"claim": {
|
||||
"id": "https://example.org/badgeclass-with-endorsements.json",
|
||||
"endorsementComment": "Pretty good"
|
||||
},
|
||||
"issuedOn": "2017-10-01T00:00Z",
|
||||
"issuer": "http://example.org/issuer1.json",
|
||||
"verification": {
|
||||
"type": "HostedBadge"
|
||||
}
|
||||
}
|
@ -204,6 +204,6 @@
|
||||
"email": "me@example.org",
|
||||
"url": "http://example.org",
|
||||
"verification": {
|
||||
"allowedOrigins": ["example.com"]
|
||||
"allowedOrigins": ["example.com", "example.org"]
|
||||
}
|
||||
}
|
@ -198,7 +198,7 @@
|
||||
},
|
||||
"verify": "verification"
|
||||
},
|
||||
"id": "http://example.org/issuer1",
|
||||
"id": "http://example.org/issuer1.json",
|
||||
"type": "Issuer",
|
||||
"name": "Example Issuer",
|
||||
"email": "me@example.org",
|
||||
|
Loading…
Reference in New Issue
Block a user