Skip to content

Commit

Permalink
feat: add Credential Request API + issuer service skeleton
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Jan 16, 2025
1 parent bca2455 commit f73a831
Show file tree
Hide file tree
Showing 34 changed files with 3,246 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package org.eclipse.edc.identityhub.participantcontext;

import org.eclipse.edc.identityhub.spi.did.store.DidResourceStore;
import org.eclipse.edc.identityhub.spi.keypair.KeyPairService;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.participantcontext.StsAccountProvisioner;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextObservable;
Expand Down Expand Up @@ -43,8 +42,6 @@ public class ParticipantContextExtension implements ServiceExtension {
@Inject
private TransactionContext transactionContext;
@Inject
private KeyPairService keyPairService;
@Inject
private Clock clock;
@Inject
private EventRouter eventRouter;
Expand Down
42 changes: 42 additions & 0 deletions dist/bom/issuerservice-base-bom/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

plugins {
`java-library`
}

dependencies {
runtimeOnly(project(":core:identity-hub-did"))
runtimeOnly(project(":core:identity-hub-core"))
runtimeOnly(project(":core:identity-hub-participants"))
runtimeOnly(project(":core:identity-hub-keypairs"))
runtimeOnly(project(":extensions:did:local-did-publisher"))
// API modules
runtimeOnly(project(":extensions:protocols:dcp:credential-request-api"))
runtimeOnly(project(":extensions:protocols:dcp:credential-request-status-api"))
runtimeOnly(project(":extensions:protocols:dcp:issuer-metadata-api"))

runtimeOnly(project(":extensions:sts:sts-account-provisioner"))
runtimeOnly(libs.edc.identity.did.core)
runtimeOnly(libs.edc.core.token)
runtimeOnly(libs.edc.api.version)
runtimeOnly(libs.edc.transaction.local) // needed by the PresentationCreatorRegistry

runtimeOnly(libs.edc.identity.did.web)
runtimeOnly(libs.bundles.connector)
}

edcBuild {

}
26 changes: 26 additions & 0 deletions dist/bom/issuerservice-bom/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

plugins {
`java-library`
}

dependencies {
runtimeOnly(project(":dist:bom:issuerservice-base-bom"))
runtimeOnly(project(":extensions:sts:sts-account-service-remote"))
}

edcBuild {

}
38 changes: 38 additions & 0 deletions dist/bom/issuerservice-feature-sql-bom/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

plugins {
`java-library`
}

dependencies {
// sql modules
api(project(":extensions:store:sql:identity-hub-credentials-store-sql"))
api(project(":extensions:store:sql:identity-hub-did-store-sql"))
api(project(":extensions:store:sql:identity-hub-keypair-store-sql"))
api(project(":extensions:store:sql:identity-hub-participantcontext-store-sql"))

api(libs.edc.sql.core)
api(libs.edc.transaction.local)
api(libs.edc.sql.pool)
api(libs.edc.sql.bootstrapper)
api(libs.edc.sql.jtivdalidation)

// third-party deps
api(libs.postgres)
}

edcBuild {

}
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,28 @@ class IdentityHubWithSts extends SmokeTest {
":dist:bom:identityhub-with-sts-bom"
));
}

@Nested
@EndToEndTest
class IssuerService extends SmokeTest {
@RegisterExtension
protected RuntimeExtension runtime =
new RuntimePerMethodExtension(new EmbeddedRuntime("issuer-service-bom",
new HashMap<>() {
{
put("web.http.port", DEFAULT_PORT);
put("web.http.path", DEFAULT_PATH);
put("web.http.version.port", valueOf(getFreePort()));
put("web.http.version.path", "/api/version");
put("web.http.did.port", valueOf(getFreePort()));
put("web.http.did.path", "/api/did");
put("web.http.credential-request.port", valueOf(getFreePort()));
put("web.http.credential-request.path", "/api/issuance");
put("edc.sts.account.api.url", "https://sts.com/accounts");
put("edc.sts.accounts.api.auth.header.value", "password");
}
},
":dist:bom:issuerservice-bom"
));
}
}
44 changes: 44 additions & 0 deletions extensions/protocols/dcp/credential-request-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

plugins {
`java-library`
`maven-publish`
id("io.swagger.core.v3.swagger-gradle-plugin")
}

dependencies {
api(project(":spi:identity-hub-spi"))
api(project(":spi:verifiable-credential-spi"))
api(libs.edc.spi.jsonld)
api(libs.edc.spi.jwt)
api(libs.edc.spi.core)
implementation(libs.edc.spi.web)
implementation(libs.edc.spi.dcp)
implementation(libs.edc.lib.jerseyproviders)
implementation(libs.edc.lib.transform)
implementation(libs.edc.dcp.transform)
implementation(libs.jakarta.rsApi)
testImplementation(libs.edc.junit)
testImplementation(libs.edc.jsonld)
testImplementation(testFixtures(libs.edc.core.jersey))
testImplementation(testFixtures(project(":spi:verifiable-credential-spi")))
testImplementation(libs.nimbus.jwt)
}

edcBuild {
swagger {
apiGroup.set("credential-request-api")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2025 Cofinity-X
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Cofinity-X - initial API and implementation
*
*/

package org.eclipse.edc.protocols.dcp.credentialrequest;

import com.fasterxml.jackson.databind.DeserializationFeature;
import org.eclipse.edc.protocols.dcp.credentialrequest.v1alpha.api.CredentialRequestApiController;
import org.eclipse.edc.runtime.metamodel.annotation.Configuration;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.runtime.metamodel.annotation.Settings;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.apiversion.ApiVersionService;
import org.eclipse.edc.spi.system.apiversion.VersionRecord;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.web.spi.WebService;
import org.eclipse.edc.web.spi.configuration.PortMapping;
import org.eclipse.edc.web.spi.configuration.PortMappingRegistry;

import java.io.IOException;
import java.util.stream.Stream;

import static org.eclipse.edc.identityhub.spi.webcontext.IdentityHubApiContext.CREDENTIAL_REQUEST;
import static org.eclipse.edc.protocols.dcp.credentialrequest.CredentialRequestApiExtension.NAME;

@Extension(value = NAME)
public class CredentialRequestApiExtension implements ServiceExtension {
public static final String NAME = "CredentialRequestApiExtension";

private static final String API_VERSION_JSON_FILE = "credential-request-api-version.json";

@Inject
private TypeManager typeManager;
@Inject
private ApiVersionService apiVersionService;
@Inject
private WebService webService;
@Inject
private PortMappingRegistry portMappingRegistry;

@Configuration
private CredentialRequestApiConfiguration apiConfiguration;

@Override
public void initialize(ServiceExtensionContext context) {

portMappingRegistry.register(new PortMapping(CREDENTIAL_REQUEST, apiConfiguration.port(), apiConfiguration.path()));

var controller = new CredentialRequestApiController();
webService.registerResource(CREDENTIAL_REQUEST, controller);

registerVersionInfo(getClass().getClassLoader());
}

private void registerVersionInfo(ClassLoader resourceClassLoader) {
try (var versionContent = resourceClassLoader.getResourceAsStream(API_VERSION_JSON_FILE)) {
if (versionContent == null) {
throw new EdcException("Version file '%s' not found or not readable.".formatted(API_VERSION_JSON_FILE));
}
Stream.of(typeManager.getMapper()
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.readValue(versionContent, VersionRecord[].class))
.forEach(vr -> apiVersionService.addRecord("credential-request", vr));
} catch (IOException e) {
throw new EdcException(e);
}
}

@Settings
record CredentialRequestApiConfiguration(
@Setting(key = "web.http." + CREDENTIAL_REQUEST + ".port", description = "Port for " + CREDENTIAL_REQUEST + " api context", defaultValue = 13132 + "")
int port,
@Setting(key = "web.http." + CREDENTIAL_REQUEST + ".path", description = "Path for " + CREDENTIAL_REQUEST + " api context", defaultValue = "/api/issuance")
String path
) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Cofinity-X
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Cofinity-X - initial API and implementation
*
*/

package org.eclipse.edc.protocols.dcp.credentialrequest.v1alpha.api;

import io.swagger.v3.oas.annotations.media.Schema;

public interface ApiSchema {
@Schema(name = "ApiErrorDetail", example = ApiErrorDetailSchema.API_ERROR_EXAMPLE)
record ApiErrorDetailSchema(
String message,
String type,
String path,
String invalidValue
) {
public static final String API_ERROR_EXAMPLE = """
{
"message": "error message",
"type": "ErrorType",
"path": "object.error.path",
"invalidValue": "this value is not valid"
}
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2025 Cofinity-X
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Cofinity-X - initial API and implementation
*
*/

package org.eclipse.edc.protocols.dcp.credentialrequest.v1alpha.api;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.protocols.dcp.credentialrequest.v1alpha.model.CredentialRequestMessage;

@OpenAPIDefinition(
info = @Info(description = "This represents the Credential Request API as per DCP specification. It serves endpoints to request the issuance of Verifiable Credentials from an issuer.", title = "Credential Request API",
version = "v1alpha"))
@SecurityScheme(name = "Authentication",
description = "Self-Issued ID token containing an access_token",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT")
public interface CredentialRequestApi {

@Tag(name = "Credential Request API")
@Operation(description = "Requests the issuance of one or several verifiable credentials from an issuer",
operationId = "requestCredentials",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = CredentialRequestMessage.class))),
responses = {
@ApiResponse(responseCode = "201", description = "The request was successfully received and is being processed.", headers = {@Header(name = "Location",
description = "contains the relative URL where the status of the request can be queried (Credential Request Status API)")}),
@ApiResponse(responseCode = "400", description = "Request body was malformed, e.g. required parameter or properties were missing",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "401", description = "No Authorization header was provided.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "403", description = "The given authentication token could not be validated or the client is not authorized to call this endpoint.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class))))

}
)
Response requestCredential(CredentialRequestMessage message);
}
Loading

0 comments on commit f73a831

Please sign in to comment.