diff --git a/Cargo.lock b/Cargo.lock index a4ff67e2..be285608 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,8 @@ dependencies = [ "tracing-subscriber", "tracing-test", "url", + "utoipa", + "utoipa-scalar", "uuid", "wiremock", ] @@ -1408,7 +1410,7 @@ checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "consumer" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did_iota", "did_jwk", @@ -1974,7 +1976,7 @@ dependencies = [ [[package]] name = "did_iota" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "bls12_381_plus 0.8.15", "identity_iota", @@ -1988,7 +1990,7 @@ dependencies = [ [[package]] name = "did_jwk" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-jwk", "identity_iota", @@ -2005,7 +2007,7 @@ dependencies = [ [[package]] name = "did_key" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-method-key", "identity_iota", @@ -2023,7 +2025,7 @@ dependencies = [ [[package]] name = "did_manager" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "consumer", "producer", @@ -2051,7 +2053,7 @@ dependencies = [ [[package]] name = "did_web" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did-web", "identity_iota", @@ -3454,7 +3456,7 @@ dependencies = [ [[package]] name = "identity_stronghold_ext" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "async-trait", "elliptic-curve 0.13.8", @@ -5431,7 +5433,7 @@ dependencies = [ [[package]] name = "producer" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "did_iota", "did_jwk", @@ -6593,7 +6595,7 @@ dependencies = [ [[package]] name = "shared" version = "0.1.0" -source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.2#6880b7f81b23e6cf1bbdc76d1dea8d2c1f6ef559" +source = "git+https://git@github.com/impierce/did-manager.git?tag=v1.0.0-beta.3#3ad5e3dba7bc76df8d6cb4a4fd2df2238d88710b" dependencies = [ "identity_iota", "identity_storage", @@ -8107,6 +8109,43 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.0.0-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fac56d240b49c629b9083c932ac20a23d926937e67c21ba209f836e2983d4f" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_json", + "serde_yaml", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.0.0-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d88270777931b8133b119c953062bd41665bb8507841f7d433f46d2765e9d4" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.67", +] + +[[package]] +name = "utoipa-scalar" +version = "0.2.0-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc86065a210b8540e46d15e0844765d1d14eec7fd6221c2b0de8f6edde990648" +dependencies = [ + "axum 0.7.5", + "serde", + "serde_json", + "utoipa", +] + [[package]] name = "uuid" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index c913ede1..b90dd68f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ edition = "2021" rust-version = "1.76.0" [workspace.dependencies] -did_manager = { git = "https://git@github.com/impierce/did-manager.git", tag = "v1.0.0-beta.2" } +did_manager = { git = "https://git@github.com/impierce/did-manager.git", tag = "v1.0.0-beta.3" } siopv2 = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } oid4vci = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } oid4vc-core = { git = "https://git@github.com/impierce/openid4vc.git", rev = "23facd4" } diff --git a/agent_api_rest/Cargo.toml b/agent_api_rest/Cargo.toml index 73997c65..399f238d 100644 --- a/agent_api_rest/Cargo.toml +++ b/agent_api_rest/Cargo.toml @@ -27,6 +27,17 @@ tracing.workspace = true tracing-subscriber.workspace = true url.workspace = true uuid.workspace = true +utoipa = { version = "=5.0.0-beta.0", features = ["axum_extras", "yaml"] } +utoipa-scalar = { version = "=0.2.0-beta.0", features = ["axum"] } +# TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) +# utoipa = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ +# "axum_extras", +# "yaml", +# ] } +# # TODO: wait for new release that contains PR juhaku/utoipa#1002 (current version `=5.0.0-alpha.1`) +# utoipa-scalar = { git = "https://github.com/juhaku/utoipa.git", rev = "f2a7143", features = [ +# "axum", +# ] } [dev-dependencies] agent_event_publisher_http = { path = "../agent_event_publisher_http", features = ["test_utils"] } @@ -35,7 +46,9 @@ agent_issuance = { path = "../agent_issuance", features = ["test_utils"] } agent_secret_manager = { path = "../agent_secret_manager", features = ["test_utils"] } agent_shared = { path = "../agent_shared", features = ["test_utils"] } agent_store = { path = "../agent_store" } -agent_verification = { path = "../agent_verification", features = ["test_utils"] } +agent_verification = { path = "../agent_verification", features = [ + "test_utils", +] } futures.workspace = true jsonwebtoken.workspace = true diff --git a/agent_api_rest/README.md b/agent_api_rest/README.md index 4e55c2bf..d70cdadc 100644 --- a/agent_api_rest/README.md +++ b/agent_api_rest/README.md @@ -7,13 +7,20 @@ Breaking changes may occur before the API reaches a stable version. The current version of the REST API is `v0`. -### OpenAPI specification (Swagger UI) +### OpenAPI specification + +> [!NOTE] +> UniCore uses [Scalar](https://scalar.com) to make its OpenAPI specification interactive. It is served under `///api-reference` (for example: `/v0/api-reference`). The `openapi.yaml` file can be downloaded there as well. The latest version of the `openapi.yaml` file is also deployed as part of the documentation at https://docs.impierce.com/unicore/api-reference. + +#### Swagger UI + +You can also run a local Swagger UI container to inspect the OpenAPI specification. ```bash docker run --rm -p 9090:8080 -e SWAGGER_JSON=/tmp/openapi.yaml -v $(pwd):/tmp swaggerapi/swagger-ui ``` -Browse to http://localhost:9090 +Browse to http://localhost:9090. ### CORS diff --git a/agent_api_rest/bak.openapi.yaml b/agent_api_rest/bak.openapi.yaml new file mode 100644 index 00000000..b2f96e73 --- /dev/null +++ b/agent_api_rest/bak.openapi.yaml @@ -0,0 +1,468 @@ +openapi: 3.1.0 +info: + title: SSI Agent - REST API + description: A lightweight REST API for the SSI Agent + version: 0.1.0 + +servers: + - url: http://localhost:3033 + description: Development + +paths: + /v0/configurations/credential_configurations: + post: + tags: + - Configurations + summary: Create a new Credential Configuration + # description: n/a + requestBody: + content: + application/json: + schema: + type: object + properties: + credentialConfigurationId: + type: string + example: w3c_vc_credential + format: + type: string + example: jwt_vc_json + credential_definition: + type: object + properties: + type: + type: array + items: + type: string + example: VerifiableCredential + example: + - VerifiableCredential + display: + type: array + items: + type: object + properties: + locale: + type: string + example: en + logo: + type: object + properties: + alt_text: + type: string + example: UniCore Logo + url: + type: string + example: https://impierce.com/images/logo-blue.png + name: + type: string + example: Identity Credential + example: + - locale: en + logo: + alt_text: UniCore Logo + url: https://impierce.com/images/logo-blue.png + name: Identity Credential + required: + - credentialConfigurationId + - format + - credential_definition + examples: + openbadgesv3_credential_configurations: + summary: Open Badges 3.0 + value: + credentialConfigurationId: openbadge_credential + credential_definition: + type: + - VerifiableCredential + - OpenBadgeCredential + display: + - locale: en + logo: + alt_text: UniCore Logo + url: https://impierce.com/images/logo-blue.png + name: Identity Credential + format: jwt_vc_json + w3c_vc_credential_configurations: + summary: W3C VC Data Model + value: + credentialConfigurationId: w3c_vc_credential + credential_definition: + type: + - VerifiableCredential + display: + - locale: en + logo: + alt_text: UniCore Logo + url: https://impierce.com/images/logo-blue.png + name: Identity Credential + format: jwt_vc_json + responses: + "200": + description: A Credential Configuration has been successfully added to the Credential Issuer Metadata + + /v0/credentials: + post: + summary: Create a new Credential for a given Subject + # description: n/a + tags: + - Creation + requestBody: + # description: n/a + required: true + content: + application/json: + schema: + type: object + properties: + # $meta: + # type: object + # properties: + # credentialTemplate: + # type: string + # description: The template to be used to create the credential + offerId: + type: string + credentialConfigurationId: + type: string + credential: + oneOf: + - type: object + properties: + credentialSubject: + type: object + - type: string + isSigned: + type: boolean + required: + - offerId + - credentialConfigurationId + - credential + examples: + open-badges-3: + summary: Open Badges 3.0 + value: + credential: + credentialSubject: + achievement: + criteria: + narrative: Team members are nominated for this badge by their peers and recognized upon review by Example Corp management. + description: This badge recognizes the development of the capacity to collaborate within a group environment. + id: https://example.com/achievements/21st-century-skills/teamwork + name: Teamwork + type: Achievement + type: + - AchievementSubject + credentialConfigurationId: openbadge_credential + offerId: "001" + w3c-vc-dm: + summary: W3C VC Data Model + value: + credential: + credentialSubject: + dob: 1982-01-01 + first_name: Ferris + last_name: Crabman + credentialConfigurationId: w3c_vc_credential + offerId: "001" + responses: + "201": + description: An Open Badge 3.0 has successfully been created for the provided credentialSubject + headers: + Location: + schema: + type: string + example: "/v0/credentials/c0c97176-44c3-4f22-ab11-6bb782e29cb9" + description: URL of the created resource + content: + application/json: + schema: + type: object + examples: + open-badges-3: + summary: Open Badges 3.0 + value: + "@context": + - https://www.w3.org/2018/credentials/v1 + - https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json + id: http://example.com/credentials/3527 + type: + - VerifiableCredential + - OpenBadgeCredential + name: Identity Credential + credentialSubject: + type: + - AchievementSubject + achievement: + id: https://example.com/achievements/21st-century-skills/teamwork + type: Achievement + criteria: + narrative: + Team members are nominated for this badge by their peers and recognized + upon review by Example Corp management. + description: + This badge recognizes the development of the capacity to collaborate + within a group environment. + name: Teamwork + issuer: + id: http://192.168.1.127:3033 + type: Profile + name: UniCore + issuanceDate: "2024-06-21T12:34:54Z" + w3c-vc-dm: + summary: W3C VC Data Model + value: + "@context": https://www.w3.org/2018/credentials/v1 + type: + - VerifiableCredential + credentialSubject: + dob: "1982-01-01" + first_name: Ferris + last_name: Crabman + issuer: + id: http://192.168.1.127:3033/ + name: UniCore + issuanceDate: "2024-06-21T12:43:20Z" + + /v0/credentials/{credential_id}: + get: + summary: Get the Credential with the given Credential ID + tags: + - Retrieval + # description: n/a + parameters: + - in: path + name: credential_id + required: true + schema: + type: string + minimum: 1 + description: The Credential ID + responses: + "200": + description: A Credential with the given Credential ID has been successfully retrieved + content: + application/json: + schema: + type: object + examples: + open-badges-3: + summary: Open Badges 3.0 + externalValue: res/open-badge-response.json + + /v0/offers: + post: + summary: Create a new Offer for one or more Credentials + tags: + - Distribution + requestBody: + description: The id of the Subject + required: true + content: + application/json: + schema: + type: object + properties: + offerId: + type: string + preAuthorizedCode: + type: string + required: + - offerId + example: + offerId: "c86289fa-b105-4ec3-9326-a02436788f11" + responses: + "200": + description: Offer created successfully. Response value should be displayed to the user in the form of a QR code. + content: + application/x-www-form-urlencoded: + schema: + type: string + example: openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%2F%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D%7D + + # (proxied) + /.well-known/oauth-authorization-server: + get: + summary: Standard OpenID Connect discovery endpoint for authorization metadata + description: Standard OpenID Connect discovery endpoint for authorization metadata + tags: + - (proxied) + /.well-known/openid-credential-issuer: + get: + summary: Standard OpenID Connect discovery endpoint for issuer metadata + tags: + - (proxied) + /auth/token: + post: + summary: Standard OAuth 2.0 endpoint for fetching a token + tags: + - (proxied) + /openid4vci/credential: + post: + summary: Standard OpenID Connect endpoint for redeeming a token for a credential + tags: + - (proxied) + + /v0/authorization_requests: + post: + summary: Create a new Authorization Request + # description: n/a + tags: + - Creation + requestBody: + # description: n/a + required: true + content: + application/json: + schema: + type: object + properties: + nonce: + type: string + example: "0d520cbe176ab9e1f7888c70888020d84a69672a4baabd3ce1c6aaad8f6420c0" + state: + type: string + example: "84266fdbd31d4c2c6d0665f7e8380fa3" + presentation_definition: + type: object + required: + - nonce + examples: + siopv2: + summary: SIOPv2 Authorization Request + value: + nonce: this is a nonce + oid4vp-open-badges-3: + summary: OID4VP Open Badges 3.0 + value: + nonce: this is a nonce + presentation_definition: + id: Verifiable Presentation request for sign-on + input_descriptors: + - id: Request for Verifiable Credential + constraints: + fields: + - path: + - "$.vc.type" + filter: + type: array + contains: + const: OpenBadgeCredential + oid4vp-w3c-vc-dm: + summary: OID4VP W3C VC Data Model + value: + nonce: this is a nonce + presentation_definition: + id: Verifiable Presentation request for sign-on + input_descriptors: + - id: Request for Verifiable Credential + constraints: + fields: + - path: + - "$.vc.type" + filter: + type: array + contains: + const: VerifiableCredential + responses: + "201": + description: An Authorization Request has successfully been created + headers: + Location: + schema: + type: string + example: "/request/43482b98aa2e071231082fc29db7a59f342a643b0c590f71083af3c7ae83f3c3" + description: URL of the created resource + content: + application/x-www-form-urlencoded: + schema: + type: string + example: "openid://?client_id=did%3Ajwk%3AeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJiUUtRUnphb3A3Q2dFdnFWcThVbGdMR3NkRi1SLWhuTEZrS0ZacVcyVk4wIiwia3R5IjoiT0tQIiwieCI6Ikdsbks5ZVBzODAyWHhBZ2xST1F6b0d1cm05UXB2MElGUEViZE1DSUxOX1UifQ&request_uri=http%3A%2F%2F192.168.1.127%3A3033%2Frequest%2F0fc2af709c435975ab5ebbc6dd2d5508c7f4a1cc3a59145a73a39e532bcbfdc7" + /v0/authorization_requests/{authorization_requests_id}: + get: + summary: Get the Authorization Request with the given Authorization Request ID + tags: + - Retrieval + # description: n/a + parameters: + - in: path + name: authorization_requests_id + required: true + schema: + type: string + minimum: 1 + description: The Authorization Request ID + responses: + "200": + description: An Authorization Request with the given Authorization Request ID has been successfully retrieved + content: + application/json: + schema: + type: object + examples: + open-badges-3: + summary: SIOPv2 Authorization Request + externalValue: res/siopv2-authorization-request.json + + # (proxied) + /request/{state}: + get: + summary: Standard request endpoint for fetching the Authorization Request object + tags: + - (proxied) + /redirect: + post: + summary: Standard OAuth 2.0 redirection endpoint + tags: + - (proxied) + + /v0/identity/offer: + post: + summary: Receive offers from third-parties + tags: + - Identity + requestBody: + content: + application/json: + schema: + type: object + examples: + receive-0: + summary: Example invitation + externalValue: "requests/payload.json" + + /v0/identity/offers: + get: + summary: List all current offers + tags: + - Identity + /v0/identity/offer/{offerId}/accept: + post: + summary: Accept an offer. UniCore will then make a request and accept the offer. + tags: + - Identity + parameters: + - in: path + name: offerId + schema: + type: integer + required: true + # description: Numeric ID of the user to get + /v0/identity/offer/{offerId}/decline: + post: + summary: Refuses an offer. UniCore will not make a request. + tags: + - Identity + parameters: + - in: path + name: offerId + schema: + type: integer + required: true + # description: Numeric ID of the user to get + +tags: + - name: Creation + description: Creating credentials + externalDocs: + url: https://docs.impierce.com/issuance diff --git a/agent_api_rest/docs/openapi-description.md b/agent_api_rest/docs/openapi-description.md new file mode 100644 index 00000000..46d8416b --- /dev/null +++ b/agent_api_rest/docs/openapi-description.md @@ -0,0 +1,58 @@ +![Banner](https://images.placeholders.dev/?width=1280&height=720) + +Full HTTP API reference for the UniCore SSI Agent. + +## Overview + +### Management endpoints + +### Standardized endpoints + +Some endpoints that UniCore offers follow a specification (such as the [OpenID4VC](https://openid.net/sg/openid4vc/specifications) protocol family). These endpoints have the **`(standardized)`** tag. + +### Public endpoints + +Some endpoints should always be publicly accessible to allow identity wallets to interact with UniCore and follow standard protocol flows. These endpoints have the **`(public)`** tag. + +> [!NOTE] +> Endpoints that should not sit behind some form of authentication are grouped under the `(public)` tag. + +```json +{ + "foo": "bar" +} +``` + +## Authentication & Authorization + +UniCore does not have any user management or authentication built-in (yet). It does not know of any roles or scopes. It is expected that the application which calls UniCore only performs calls which have been checked in the consumer business logic. If you want to deploy UniCore publicly, you should restrict access to the API by running it behind a reverse proxy or some API gateway. In most cases, only the endpoints behind `/v0` need to be protected, but all other endpoints should stay publicly accessible. + +### Example reverse proxy configuration + +Here is an example Nginx configuration that restricts access to the `/v0` endpoints by checking for a valid API key in the headers: + +
+ nginx.conf + +``` +http { + server { + listen 8080; + gzip on; + + location /v0 { + if ($http_x_api_key != "A041FE585C6F45CF841D20D47D329FA5") { + return 403; + } + + proxy_pass http://127.0.0.1:3033/v0; + } + + location / { + proxy_pass http://127.0.0.1:3033; + } + } +} +``` + +
diff --git a/agent_api_rest/openapi.yaml b/agent_api_rest/openapi.yaml index 37b3fe35..1a66112c 100644 --- a/agent_api_rest/openapi.yaml +++ b/agent_api_rest/openapi.yaml @@ -1,415 +1,509 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: - title: SSI Agent - REST API - description: A lightweight REST API for the SSI Agent - version: 0.1.0 + title: UniCore HTTP API + description: | + ![Banner](https://images.placeholders.dev/?width=1280&height=720) -servers: - - url: http://localhost:3033 - description: Development + Full HTTP API reference for the UniCore SSI Agent. + ## Overview + + ### Management endpoints + + ### Standardized endpoints + + Some endpoints that UniCore offers follow a specification (such as the [OpenID4VC](https://openid.net/sg/openid4vc/specifications) protocol family). These endpoints have the **`(standardized)`** tag. + + ### Public endpoints + + Some endpoints should always be publicly accessible to allow identity wallets to interact with UniCore and follow standard protocol flows. These endpoints have the **`(public)`** tag. + + > [!NOTE] + > Endpoints that should not sit behind some form of authentication are grouped under the `(public)` tag. + + ```json + { + "foo": "bar" + } + ``` + + ## Authentication & Authorization + + UniCore does not have any user management or authentication built-in (yet). It does not know of any roles or scopes. It is expected that the application which calls UniCore only performs calls which have been checked in the consumer business logic. If you want to deploy UniCore publicly, you should restrict access to the API by running it behind a reverse proxy or some API gateway. In most cases, only the endpoints behind `/v0` need to be protected, but all other endpoints should stay publicly accessible. + + ### Example reverse proxy configuration + + Here is an example Nginx configuration that restricts access to the `/v0` endpoints by checking for a valid API key in the headers: + +
+ nginx.conf + + ``` + http { + server { + listen 8080; + gzip on; + + location /v0 { + if ($http_x_api_key != "A041FE585C6F45CF841D20D47D329FA5") { + return 403; + } + + proxy_pass http://127.0.0.1:3033/v0; + } + + location / { + proxy_pass http://127.0.0.1:3033; + } + } + } + ``` + +
+ license: + name: '' + version: '' paths: - /v0/configurations/credential_configurations: + /.well-known/did-configuration.json: + get: + tags: + - (.well-known) + - (public) + summary: DID Configuration Resource for Domain Linkage + description: Standard .well-known endpoint for DID Configuration Resources. + operationId: did_configuration_json + responses: + '200': + description: DID Configuration Resource + content: + application/json: + schema: + $ref: '#/components/schemas/DomainLinkageConfiguration' + '404': + description: Domain Linkage inactive. + /.well-known/did.json: + get: + tags: + - (.well-known) + - (public) + summary: DID Document for `did:web` method + description: Standard .well-known endpoint for self-hosted DID Document. + operationId: did_json + responses: + '200': + description: DID Document for `did:web` method + content: + application/json: + schema: + $ref: '#/components/schemas/CoreDocument' + '404': + description: DID method `did:web` inactive. + /.well-known/oauth-authorization-server: + get: + tags: + - (.well-known) + - (public) + summary: Authorization Server Metadata + description: Standard OpenID Connect discovery endpoint for authorization metadata. + operationId: oauth_authorization_server + responses: + '200': + description: Successfully returns the Authorization Server Metadata + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AuthorizationServerMetadata' + /.well-known/openid-credential-issuer: + get: + tags: + - (.well-known) + - (public) + summary: Credential Issuer Metadata + description: Standard OpenID Connect discovery endpoint for issuer metadata. + operationId: openid_credential_issuer + responses: + '200': + description: Successfully returns the Credential Issuer Metadata + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CredentialIssuerMetadata' + /auth/token: + post: + tags: + - (public) + summary: Token Endpoint + description: Standard OAuth 2.0 endpoint that returns an access_token after successful authorization. + operationId: token + requestBody: + description: '' + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenRequest' + required: true + responses: + '200': + description: Returns an access token. + /openid4vci/credential: post: tags: - - Configurations - summary: Create a new Credential Configuration - # description: n/a + - Issuance + - (public) + summary: Credential Endpoint + description: Standard OpenID4VCI endpoint for redeeming a token for a credential. + operationId: credential requestBody: content: application/json: schema: - type: object - properties: - credentialConfigurationId: - type: string - example: w3c_vc_credential - format: - type: string - example: jwt_vc_json - credential_definition: - type: object - properties: - type: - type: array - items: - type: string - example: VerifiableCredential - example: - - VerifiableCredential - display: - type: array - items: - type: object - properties: - locale: - type: string - example: en - logo: - type: object - properties: - alt_text: - type: string - example: UniCore Logo - url: - type: string - example: https://impierce.com/images/logo-blue.png - name: - type: string - example: Identity Credential - example: - - locale: en - logo: - alt_text: UniCore Logo - url: https://impierce.com/images/logo-blue.png - name: Identity Credential - required: - - credentialConfigurationId - - format - - credential_definition - examples: - openbadgesv3_credential_configurations: - summary: Open Badges 3.0 - value: - credentialConfigurationId: openbadge_credential - credential_definition: - type: - - VerifiableCredential - - OpenBadgeCredential - display: - - locale: en - logo: - alt_text: UniCore Logo - url: https://impierce.com/images/logo-blue.png - name: Identity Credential - format: jwt_vc_json - w3c_vc_credential_configurations: - summary: W3C VC Data Model - value: - credentialConfigurationId: w3c_vc_credential - credential_definition: - type: - - VerifiableCredential - display: - - locale: en - logo: - alt_text: UniCore Logo - url: https://impierce.com/images/logo-blue.png - name: Identity Credential - format: jwt_vc_json + $ref: '#/components/schemas/CredentialRequest' + required: true responses: '200': - description: A Credential Configuration has been successfully added to the Credential Issuer Metadata + description: Successfully returns the credential + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CredentialResponse' + /openid4vci/offers: + get: + tags: + - Holder + - (public) + summary: Credential Offer Endpoint + description: |- + Standard OpenID4VCI endpoint that allows the Issuer to pass information about the credential offer to the Holder's wallet. - /v0/credentials: + [Specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint) + operationId: offers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Oid4vciOfferEndpointRequest' + required: true + responses: + '200': + description: Successfully received offer metadata. + /redirect: + post: + tags: + - (public) + summary: Redirect Endpoint + description: Standard OAuth 2.0 endpoint. + operationId: redirect + requestBody: + description: '' + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/GenericAuthorizationResponse' + required: true + responses: + '200': + description: '' + /request/{id}: + get: + tags: + - (public) + summary: Authorization Request + description: |- + Standard OAuth 2.0 endpoint. + + Instead of directly embedding the Authorization Request into a QR-code or deeplink, the `Relying Party` can embed a + `request_uri` that points to this endpoint from where the Authorization Request Object can be retrieved. + As described here: https://www.rfc-editor.org/rfc/rfc9101.html#name-passing-a-request-object-by- + operationId: request + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: '' + /v0/authorization_requests: post: - summary: Create a new Credential for a given Subject - # description: n/a tags: - - Creation + - Verification + summary: Create a new Authorization Request + description: UniCore will ask a holder for certain information based on the Presentation Definition specified. + operationId: authorization_requests requestBody: - # description: n/a + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizationRequestsEndpointRequest' + required: true + responses: + '201': + description: Authorization Request successfully created. + /v0/authorization_requests/{id}: + get: + tags: + - Verification + summary: Get an Authorization Request + description: Retrieve an existing Authorization Request. + operationId: get_authorization_requests + parameters: + - name: id + in: path + description: The ID of the Authorization Request to retrieve. required: true + schema: + type: string + responses: + '200': + description: Successfully returns an existing Authorization Request. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GenericAuthorizationRequest' + /v0/credentials: + post: + tags: + - Issuance + summary: Create a new credential + description: Create a new credential for a given subject. + operationId: credentials + requestBody: content: application/json: schema: - type: object - properties: - # $meta: - # type: object - # properties: - # credentialTemplate: - # type: string - # description: The template to be used to create the credential - offerId: - type: string - credentialConfigurationId: - type: string - credential: - oneOf: - - type: object - properties: - credentialSubject: - type: object - - type: string - isSigned: - type: boolean - required: - - offerId - - credentialConfigurationId - - credential + $ref: '#/components/schemas/CredentialsEndpointRequest' examples: - open-badges-3: + openbadges: summary: Open Badges 3.0 + description: s0me descr1pti0n + externalValue: res/open-badge-request.json + w3c-vc: + summary: W3C v1.1 + description: s0me descr1pti0n value: + offerId: '123' + credentialConfigurationId: w3c_vc_credential credential: credentialSubject: - achievement: - criteria: - narrative: Team members are nominated for this badge by their peers and recognized upon review by Example Corp management. - description: This badge recognizes the development of the capacity to collaborate within a group environment. - id: https://example.com/achievements/21st-century-skills/teamwork - name: Teamwork - type: Achievement - type: - - AchievementSubject - credentialConfigurationId: openbadge_credential - offerId: '001' - w3c-vc-dm: - summary: W3C VC Data Model - value: - credential: - credentialSubject: - dob: 1982-01-01 first_name: Ferris - last_name: Crabman - credentialConfigurationId: w3c_vc_credential - offerId: '001' + last_name: Rustacean + required: true responses: - "201": - description: An Open Badge 3.0 has successfully been created for the provided credentialSubject + '201': + description: Successfully created a new credential. headers: Location: schema: type: string - example: "/v0/credentials/c0c97176-44c3-4f22-ab11-6bb782e29cb9" description: URL of the created resource content: application/json: schema: - type: object + $ref: '#/components/schemas/CredentialView' examples: - open-badges-3: + openbadges-3-0: summary: Open Badges 3.0 + description: An badge following the Open Badges Specification 3.0 value: - "@context": - - https://www.w3.org/2018/credentials/v1 - - https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json - id: http://example.com/credentials/3527 - type: - - VerifiableCredential - - OpenBadgeCredential - name: Identity Credential - credentialSubject: - type: - - AchievementSubject - achievement: - id: https://example.com/achievements/21st-century-skills/teamwork - type: Achievement - criteria: - narrative: Team members are nominated for this badge by their peers and recognized - upon review by Example Corp management. - description: This badge recognizes the development of the capacity to collaborate - within a group environment. - name: Teamwork - issuer: - id: http://192.168.1.127:3033 - type: Profile - name: UniCore - issuanceDate: '2024-06-21T12:34:54Z' - w3c-vc-dm: - summary: W3C VC Data Model + foo: bar + w3c-vc-1-1: + summary: W3C VC Data Model v1.1 + description: A credential following the W3C Verifiable Credentials Data Model v1.1 value: - "@context": https://www.w3.org/2018/credentials/v1 - type: - - VerifiableCredential - credentialSubject: - dob: '1982-01-01' - first_name: Ferris - last_name: Crabman - issuer: - id: http://192.168.1.127:3033/ - name: UniCore - issuanceDate: '2024-06-21T12:43:20Z' - - /v0/credentials/{credential_id}: + offerId: '0001' + '400': + description: Invalid payload. + /v0/credentials/{id}: get: - summary: Get the Credential with the given Credential ID tags: - - Retrieval - # description: n/a + - Issuance + summary: Retrieve a credential + description: Retrieves an existing credential by its ID. + operationId: get_credentials parameters: - - in: path - name: credential_id - required: true - schema: - type: string - minimum: 1 - description: The Credential ID - responses: - "200": - description: A Credential with the given Credential ID has been successfully retrieved - content: - application/json: - schema: - type: object - examples: - open-badges-3: - summary: Open Badges 3.0 - externalValue: res/open-badge-response.json - - /v0/offers: - post: - summary: Create a new Offer for one or more Credentials - tags: - - Distribution - requestBody: - description: The id of the Subject + - name: id + in: path + description: Unique identifier of the Credential required: true - content: - application/json: - schema: - type: object - properties: - offerId: - type: string - preAuthorizedCode: - type: string - required: - - offerId - example: - offerId: "c86289fa-b105-4ec3-9326-a02436788f11" + schema: + type: string + example: '0001' responses: - "200": - description: Offer created successfully. Response value should be displayed to the user in the form of a QR code. + '200': + description: Credential found content: - application/x-www-form-urlencoded: + application/json: schema: - type: string - example: openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%2F%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D%7D - - # (proxied) - /.well-known/oauth-authorization-server: + type: array + items: + $ref: '#/components/schemas/CredentialView' + /v0/holder/credentials: get: - summary: Standard OpenID Connect discovery endpoint for authorization metadata - description: Standard OpenID Connect discovery endpoint for authorization metadata tags: - - (proxied) - /.well-known/openid-credential-issuer: + - Holder + summary: Get all credentials + description: Retrieve all credentials that this UniCore instance currently holds. + operationId: credentials + responses: + '200': + description: Successfully retrieved all credentials. + /v0/holder/offers: get: - summary: Standard OpenID Connect discovery endpoint for issuer metadata tags: - - (proxied) - /auth/token: + - Holder + summary: Get all offers + description: Retrieve all pending credential offers. + operationId: offers + responses: + '200': + description: Successfully retrieved all pending offers. + /v0/holder/offers/{offer_id}/accept: post: - summary: Standard OAuth 2.0 endpoint for fetching a token tags: - - (proxied) - /openid4vci/credential: + - Holder + summary: Accept an offer + description: Accept a pending credential offer. UniCore will then make a request to the Issuer to receive the offer. + operationId: accept + parameters: + - name: offer_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Successfully accepted a pending offer. + /v0/holder/offers/{offer_id}/reject: post: - summary: Standard OpenID Connect endpoint for redeeming a token for a credential tags: - - (proxied) - - /v0/authorization_requests: + - Holder + summary: Reject an offer + description: Reject a pending credential offer. UniCore will not make any further requests to the Issuer. + operationId: reject + parameters: + - name: offer_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Successfully rejected a pending offer. + /v0/offers: post: - summary: Create a new Authorization Request - # description: n/a tags: - - Creation + - Issuance + summary: Create a new credential offer + description: Create a new offer for one or more credentials. + operationId: offers requestBody: - # description: n/a - required: true content: application/json: schema: - type: object - properties: - nonce: - type: string - example: "0d520cbe176ab9e1f7888c70888020d84a69672a4baabd3ce1c6aaad8f6420c0" - state: - type: string - example: "84266fdbd31d4c2c6d0665f7e8380fa3" - presentation_definition: - type: object - required: - - nonce - examples: - siopv2: - summary: SIOPv2 Authorization Request - value: - nonce: this is a nonce - oid4vp-open-badges-3: - summary: OID4VP Open Badges 3.0 - value: - nonce: this is a nonce - presentation_definition: - id: Verifiable Presentation request for sign-on - input_descriptors: - - id: Request for Verifiable Credential - constraints: - fields: - - path: - - "$.vc.type" - filter: - type: array - contains: - const: OpenBadgeCredential - oid4vp-w3c-vc-dm: - summary: OID4VP W3C VC Data Model - value: - nonce: this is a nonce - presentation_definition: - id: Verifiable Presentation request for sign-on - input_descriptors: - - id: Request for Verifiable Credential - constraints: - fields: - - path: - - "$.vc.type" - filter: - type: array - contains: - const: VerifiableCredential + $ref: '#/components/schemas/OffersEndpointRequest' + required: true responses: - "201": - description: An Authorization Request has successfully been created - headers: - Location: - schema: - type: string - example: "/request/43482b98aa2e071231082fc29db7a59f342a643b0c590f71083af3c7ae83f3c3" - description: URL of the created resource + '200': + description: Successfully created a new credential offer. Response value is standard-compliant and can be used by identity wallet. content: application/x-www-form-urlencoded: schema: type: string - example: "openid://?client_id=did%3Ajwk%3AeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJiUUtRUnphb3A3Q2dFdnFWcThVbGdMR3NkRi1SLWhuTEZrS0ZacVcyVk4wIiwia3R5IjoiT0tQIiwieCI6Ikdsbks5ZVBzODAyWHhBZ2xST1F6b0d1cm05UXB2MElGUEViZE1DSUxOX1UifQ&request_uri=http%3A%2F%2F192.168.1.127%3A3033%2Frequest%2F0fc2af709c435975ab5ebbc6dd2d5508c7f4a1cc3a59145a73a39e532bcbfdc7" - /v0/authorization_requests/{authorization_requests_id}: - get: - summary: Get the Authorization Request with the given Authorization Request ID - tags: - - Retrieval - # description: n/a - parameters: - - in: path - name: authorization_requests_id - required: true - schema: - type: string - minimum: 1 - description: The Authorization Request ID - responses: - "200": - description: An Authorization Request with the given Authorization Request ID has been successfully retrieved - content: - application/json: - schema: - type: object - examples: - open-badges-3: - summary: SIOPv2 Authorization Request - externalValue: res/siopv2-authorization-request.json - - # (proxied) - /request/{state}: - get: - summary: Standard request endpoint for fetching the Authorization Request object - tags: - - (proxied) - /redirect: + example: openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%2F%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D%7D + /v0/offers/send: post: - summary: Standard OAuth 2.0 redirection endpoint tags: - - (proxied) + - Issuance + summary: Send offer to Holder + description: |- + Manually send a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. + This is **not** required if the wallet initiates the flow (usually an end-user mobile wallet), but rather when the Holder that has no prior knowledge of the offer (most often another cloud-based wallet, such as another UniCore instance). + operationId: send + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SendOfferEndpointRequest' + example: + offerId: '0001' + targetUrl: https://wallet.example.com/openid4vci/offers + required: true + responses: + '200': + description: Successfully sent credential offer to Holder. + '400': + description: Invalid payload. +components: + schemas: + AuthorizationRequestsEndpointRequest: + allOf: + - allOf: + - type: 'null' + - $ref: '#/components/schemas/PresentationDefinitionResource' + - type: object + required: + - nonce + properties: + nonce: + type: string + state: + type: + - string + - 'null' + CredentialsEndpointRequest: + type: object + required: + - offerId + - credential + - credentialConfigurationId + properties: + credential: {} + credentialConfigurationId: + type: string + isSigned: + type: boolean + offerId: + type: string + OffersEndpointRequest: + type: object + required: + - offerId + properties: + offerId: + type: string + Oid4vciOfferEndpointRequest: + allOf: + - $ref: '#/components/schemas/CredentialOffer' + - type: object + SendOfferEndpointRequest: + type: object + required: + - offerId + - targetUrl + properties: + offerId: + type: string + targetUrl: + $ref: '#/components/schemas/Url' +tags: +- name: (public) + description: A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets. +- name: (.well-known) + description: Well-known endpoints provide metadata about the server. +- name: Issuance + description: Issue credentials to holders that will store them in their wallets. + externalDocs: + url: https://docs.impierce.com + description: Issuance API Documentation +externalDocs: + url: https://docs.impierce.com + description: Official Documentation diff --git a/agent_api_rest/src/holder/holder/credentials/mod.rs b/agent_api_rest/src/holder/holder/credentials/mod.rs index c18e9f71..4398df18 100644 --- a/agent_api_rest/src/holder/holder/credentials/mod.rs +++ b/agent_api_rest/src/holder/holder/credentials/mod.rs @@ -8,6 +8,17 @@ use axum::{ use hyper::StatusCode; use serde_json::json; +/// Get all credentials +/// +/// Retrieve all credentials that this UniCore instance currently holds. +#[utoipa::path( + get, + path = "/holder/credentials", + tag = "Holder", + responses( + (status = 200, description = "Successfully retrieved all credentials."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn credentials(State(state): State) -> Response { match query_handler("all_credentials", &state.query.all_credentials).await { diff --git a/agent_api_rest/src/holder/holder/offers/accept.rs b/agent_api_rest/src/holder/holder/offers/accept.rs index ee3f819a..d7fa2e90 100644 --- a/agent_api_rest/src/holder/holder/offers/accept.rs +++ b/agent_api_rest/src/holder/holder/offers/accept.rs @@ -10,6 +10,18 @@ use axum::{ }; use hyper::StatusCode; +/// Accept an offer +/// +/// Accept a pending credential offer. UniCore will then make a request to the Issuer to receive the offer. +#[utoipa::path( + post, + path = "/holder/offers/{offer_id}/accept", + // request_body = ?, + tag = "Holder", + responses( + (status = 200, description = "Successfully accepted a pending offer."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn accept(State(state): State, Path(offer_id): Path) -> Response { // TODO: General note that also applies to other endpoints: currently we are using Application Layer logic in the diff --git a/agent_api_rest/src/holder/holder/offers/mod.rs b/agent_api_rest/src/holder/holder/offers/mod.rs index a5130017..dda4f67e 100644 --- a/agent_api_rest/src/holder/holder/offers/mod.rs +++ b/agent_api_rest/src/holder/holder/offers/mod.rs @@ -11,6 +11,17 @@ use axum::{ use hyper::StatusCode; use serde_json::json; +/// Get all offers +/// +/// Retrieve all pending credential offers. +#[utoipa::path( + get, + path = "/holder/offers", + tag = "Holder", + responses( + (status = 200, description = "Successfully retrieved all pending offers."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State) -> Response { match query_handler("all_offers", &state.query.all_offers).await { diff --git a/agent_api_rest/src/holder/holder/offers/reject.rs b/agent_api_rest/src/holder/holder/offers/reject.rs index eb0ffe17..fb1ebd58 100644 --- a/agent_api_rest/src/holder/holder/offers/reject.rs +++ b/agent_api_rest/src/holder/holder/offers/reject.rs @@ -6,6 +6,18 @@ use axum::{ }; use hyper::StatusCode; +/// Reject an offer +/// +/// Reject a pending credential offer. UniCore will not make any further requests to the Issuer. +#[utoipa::path( + post, + path = "/holder/offers/{offer_id}/reject", + // request_body = ?, + tag = "Holder", + responses( + (status = 200, description = "Successfully rejected a pending offer."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn reject(State(state): State, Path(offer_id): Path) -> Response { let command = OfferCommand::RejectCredentialOffer { diff --git a/agent_api_rest/src/holder/openid4vci/mod.rs b/agent_api_rest/src/holder/openid4vci/mod.rs index 95145b61..310db955 100644 --- a/agent_api_rest/src/holder/openid4vci/mod.rs +++ b/agent_api_rest/src/holder/openid4vci/mod.rs @@ -9,13 +9,29 @@ use hyper::StatusCode; use oid4vci::credential_offer::CredentialOffer; use serde::{Deserialize, Serialize}; use tracing::info; +use utoipa::ToSchema; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct Oid4vciOfferEndpointRequest { #[serde(flatten)] pub credential_offer: CredentialOffer, } +/// Credential Offer Endpoint +/// +/// Standard OpenID4VCI endpoint that allows the Issuer to pass information about the credential offer to the Holder's wallet. +/// +/// [Specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint) +#[utoipa::path( + get, + path = "/openid4vci/offers", + request_body = Oid4vciOfferEndpointRequest, + tag = "Holder", + tags = ["(public)"], + responses( + (status = 200, description = "Successfully received offer metadata."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index c0d43200..157526a8 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -27,6 +27,21 @@ use tracing::{error, info}; const DEFAULT_EXTERNAL_SERVER_RESPONSE_TIMEOUT_MS: u64 = 1000; const POLLING_INTERVAL_MS: u64 = 100; +/// Credential Endpoint +/// +/// Standard OpenID4VCI endpoint for redeeming a token for a credential. +#[utoipa::path( + post, + path = "/openid4vci/credential", + // TODO: doesn't work since (external) `CredentialRequest` doesn't implement `ToSchema`? + // See: https://github.com/juhaku/utoipa?tab=readme-ov-file#how-to-implement-toschema-for-external-type + request_body = CredentialRequest, + tag = "Issuance", + tags = ["(public)"], + responses( + (status = 200, description = "Successfully returns the credential", body = [CredentialResponse]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn credential( State(state): State, diff --git a/agent_api_rest/src/issuance/credential_issuer/token.rs b/agent_api_rest/src/issuance/credential_issuer/token.rs index 72728ad5..08ebfac7 100644 --- a/agent_api_rest/src/issuance/credential_issuer/token.rs +++ b/agent_api_rest/src/issuance/credential_issuer/token.rs @@ -16,6 +16,17 @@ use oid4vci::token_request::TokenRequest; use serde_json::json; use tracing::info; +/// Token Endpoint +/// +/// Standard OAuth 2.0 endpoint that returns an access_token after successful authorization. +#[utoipa::path( + post, + path = "/auth/token", + tags = ["(public)"], + responses( + (status = 200, description = "Returns an access token."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn token( State(state): State, diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs index 73ab36c3..d99ba393 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/oauth_authorization_server.rs @@ -9,6 +9,18 @@ use axum::{ response::{IntoResponse, Response}, }; +/// Authorization Server Metadata +/// +/// Standard OpenID Connect discovery endpoint for authorization metadata. +#[utoipa::path( + get, + path = "/.well-known/oauth-authorization-server", + tag = "(.well-known)", + tags = ["(public)"], + responses( + (status = 200, description = "Successfully returns the Authorization Server Metadata", body = [AuthorizationServerMetadata]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn oauth_authorization_server(State(state): State) -> Response { match query_handler(SERVER_CONFIG_ID, &state.query.server_config).await { diff --git a/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs b/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs index 92a9c3dd..c95bcd9b 100644 --- a/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs +++ b/agent_api_rest/src/issuance/credential_issuer/well_known/openid_credential_issuer.rs @@ -9,6 +9,18 @@ use axum::{ response::{IntoResponse, Response}, }; +/// Credential Issuer Metadata +/// +/// Standard OpenID Connect discovery endpoint for issuer metadata. +#[utoipa::path( + get, + path = "/.well-known/openid-credential-issuer", + tag = "(.well-known)", + tags = ["(public)"], + responses( + (status = 200, description = "Successfully returns the Credential Issuer Metadata", body = [CredentialIssuerMetadata]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn openid_credential_issuer(State(state): State) -> Response { match query_handler(SERVER_CONFIG_ID, &state.query.server_config).await { diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index f8ea8a11..9c39bd25 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -16,7 +16,22 @@ use oid4vci::credential_issuer::credential_issuer_metadata::CredentialIssuerMeta use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; +use utoipa::ToSchema; +/// Retrieve a credential +/// +/// Retrieves an existing credential by its ID. +#[utoipa::path( + get, + path = "/credentials/{id}", + tag = "Issuance", + params( + ("id" = String, Path, description = "Unique identifier of the Credential", example = "0001"), + ), + responses( + (status = 200, description = "Credential found", body = [CredentialView]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn get_credentials(State(state): State, Path(credential_id): Path) -> Response { // Get the credential if it exists. @@ -30,7 +45,7 @@ pub(crate) async fn get_credentials(State(state): State, Path(cre } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CredentialsEndpointRequest { pub offer_id: String, @@ -40,6 +55,30 @@ pub struct CredentialsEndpointRequest { pub credential_configuration_id: String, } +/// Create a new credential +/// +/// Create a new credential for a given subject. +#[utoipa::path( + post, + path = "/credentials", + request_body(content = CredentialsEndpointRequest, + examples( + ("w3c-vc" = (summary = "W3C v1.1", description = "s0me descr1pti0n", value = json!({"offerId": "123", "credentialConfigurationId": "w3c_vc_credential", "credential": {"credentialSubject": {"first_name": "Ferris", "last_name": "Rustacean"}}}))), + ("openbadges" = (summary = "Open Badges 3.0", description = "s0me descr1pti0n", external_value = "res/open-badge-request.json")) + ) + ), + tag = "Issuance", + responses( + (status = 201, description = "Successfully created a new credential.", body = CredentialView, + headers(("Location" = String, description = "URL of the created resource")), + examples( + ("w3c-vc-1-1" = (summary = "W3C VC Data Model v1.1", description = "A credential following the W3C Verifiable Credentials Data Model v1.1", value = json!({"offerId": "0001"}))), + ("openbadges-3-0" = (summary = "Open Badges 3.0", description = "An badge following the Open Badges Specification 3.0", value = json!({"foo": "bar"}))) + ) + ), + (status = 400, description = "Invalid payload.") + ), +)] #[axum_macros::debug_handler] pub(crate) async fn credentials( State(state): State, diff --git a/agent_api_rest/src/issuance/offers/mod.rs b/agent_api_rest/src/issuance/offers/mod.rs index 06900dfb..2d0abb8c 100644 --- a/agent_api_rest/src/issuance/offers/mod.rs +++ b/agent_api_rest/src/issuance/offers/mod.rs @@ -15,13 +15,26 @@ use hyper::header; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; +use utoipa::ToSchema; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct OffersEndpointRequest { pub offer_id: String, } +/// Create a new credential offer +/// +/// Create a new offer for one or more credentials. +#[utoipa::path( + post, + path = "/offers", + request_body = OffersEndpointRequest, + tag = "Issuance", + responses( + (status = 200, description = "Successfully created a new credential offer. Response value is standard-compliant and can be used by identity wallet.", body = String, content_type = "application/x-www-form-urlencoded", example = json!("openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%2F%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D%7D")) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn offers(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); diff --git a/agent_api_rest/src/issuance/offers/send.rs b/agent_api_rest/src/issuance/offers/send.rs index 2e9a973a..10f9b0a4 100644 --- a/agent_api_rest/src/issuance/offers/send.rs +++ b/agent_api_rest/src/issuance/offers/send.rs @@ -9,14 +9,29 @@ use hyper::StatusCode; use serde::{Deserialize, Serialize}; use tracing::info; use url::Url; +use utoipa::ToSchema; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SendOfferEndpointRequest { pub offer_id: String, pub target_url: Url, } +/// Send offer to Holder +/// +/// Manually send a prepared credential offer to a Holder's [Credential Offer Endpoint](#tag/holder/GET/openid4vci/offers) via a `GET` request. +/// This is **not** required if the wallet initiates the flow (usually an end-user mobile wallet), but rather when the Holder that has no prior knowledge of the offer (most often another cloud-based wallet, such as another UniCore instance). +#[utoipa::path( + post, + path = "/offers/send", + request_body(content = SendOfferEndpointRequest, example = json!({"offerId": "0001", "targetUrl": "https://wallet.example.com/openid4vci/offers"})), + tag = "Issuance", + responses( + (status = 200, description = "Successfully sent credential offer to Holder."), + (status = 400, description = "Invalid payload."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn send(State(state): State, Json(payload): Json) -> Response { info!("Request Body: {}", payload); diff --git a/agent_api_rest/src/lib.rs b/agent_api_rest/src/lib.rs index b282b706..631ac60a 100644 --- a/agent_api_rest/src/lib.rs +++ b/agent_api_rest/src/lib.rs @@ -1,5 +1,6 @@ pub mod holder; pub mod issuance; +pub mod openapi; pub mod verification; use agent_holder::state::HolderState; @@ -9,6 +10,10 @@ use agent_verification::state::VerificationState; use axum::{body::Bytes, extract::MatchedPath, http::Request, response::Response, Router}; use tower_http::trace::TraceLayer; use tracing::{info_span, Span}; +use utoipa::OpenApi; +use utoipa_scalar::{Scalar, Servable}; + +use crate::openapi::{did_configuration, did_web, HolderApi, IssuanceApi, VerificationApi}; pub const API_VERSION: &str = "/v0"; @@ -32,7 +37,12 @@ pub fn app( Router::new() .merge(issuance_state.map(issuance::router).unwrap_or_default()) .merge(holder_state.map(holder::router).unwrap_or_default()) - .merge(verification_state.map(verification::router).unwrap_or_default()), + .merge(verification_state.map(verification::router).unwrap_or_default()) + // API Docs + .merge(Scalar::with_url( + format!("{}/api-reference", API_VERSION), + patch_generated_openapi(ApiDoc::openapi()), + )), ) // Trace layer .layer( @@ -83,6 +93,55 @@ fn get_base_path() -> Result { }) } +#[derive(utoipa::OpenApi)] +#[openapi( + // modifiers(), + paths( + // Standard endpoints as defined in the protocol specifications. + // OAuth 2.0 + crate::verification::relying_party::redirect::redirect, + crate::verification::relying_party::request::request, + crate::issuance::credential_issuer::token::token, + // OpenID4VCI + crate::holder::openid4vci::offers, + crate::issuance::credential_issuer::credential::credential, + // .well-known + crate::issuance::credential_issuer::well_known::oauth_authorization_server::oauth_authorization_server, + crate::issuance::credential_issuer::well_known::openid_credential_issuer::openid_credential_issuer, + ), + nest( + (path = "/v0", api = IssuanceApi), + (path = "/v0", api = VerificationApi), + (path = "/v0", api = HolderApi) + ), + tags( + (name = "(public)", description = "A collection of endpoints that should be publicly accessible without authentication. They are used to resolve metadata or allow communication with wallets."), + (name = "(.well-known)", description = "Well-known endpoints provide metadata about the server."), + (name = "Issuance", description = "Issue credentials to holders that will store them in their wallets.", external_docs(description="Issuance API Documentation", url="https://docs.impierce.com")), + ), + external_docs(description="Official Documentation", url="https://docs.impierce.com"), + )] +pub struct ApiDoc; + +pub fn patch_generated_openapi(mut openapi: utoipa::openapi::OpenApi) -> utoipa::openapi::OpenApi { + openapi.info.title = "UniCore HTTP API".into(); + openapi.info.description = Some(include_str!("../docs/openapi-description.md").into()); + // openapi.info.version = "1.0.0-alpha.1".into(); // can UniCore even be aware of its current version or does it need to be removed from the openapi.yaml? + openapi.info.version = "".into(); + // TODO: required to use `UNICORE__URL` as the "self" server? + // openapi.servers = vec![ServerBuilder::new() + // .url("https://playground.agent-dev.impierce.com") + // .description(Some("UniCore development server hosted by Impierce Technologies")) + // .build()] + // .into(); + // Append endpoints defined outside of `agent_api_rest`. + openapi.paths.add_path("/.well-known/did.json", did_web()); + openapi + .paths + .add_path("/.well-known/did-configuration.json", did_configuration()); + openapi +} + #[cfg(test)] mod tests { use super::*; @@ -94,6 +153,9 @@ mod tests { credential_issuer_metadata::CredentialIssuerMetadata, }; use serde_json::json; + use utoipa::OpenApi; + + use crate::{app, ApiDoc}; use std::collections::HashMap; pub const CREDENTIAL_CONFIGURATION_ID: &str = "badge"; @@ -138,6 +200,14 @@ mod tests { async fn handler() {} + #[tokio::test] + async fn generate_openapi_file() { + let yaml_value = patch_generated_openapi(ApiDoc::openapi()); + let yaml_string = serde_yaml::to_string(&yaml_value).unwrap(); + println!("{}", yaml_string); + std::fs::write("openapi.yaml", yaml_string).unwrap(); + } + #[tokio::test] #[should_panic] async fn test_base_path_routes() { diff --git a/agent_api_rest/src/openapi.rs b/agent_api_rest/src/openapi.rs new file mode 100644 index 00000000..36a9dcf7 --- /dev/null +++ b/agent_api_rest/src/openapi.rs @@ -0,0 +1,97 @@ +use utoipa::openapi::{ + path::OperationBuilder, Content, HttpMethod, PathItem, Ref, Response, ResponseBuilder, ResponsesBuilder, +}; +use utoipa::OpenApi; + +use crate::holder::{holder, openid4vci}; +use crate::issuance::credentials; +use crate::issuance::offers; +use crate::verification::authorization_requests; + +#[derive(OpenApi)] +#[openapi( + paths( + credentials::credentials, + credentials::get_credentials, + offers::offers, + offers::send::send + ), + components(schemas( + credentials::CredentialsEndpointRequest, + offers::OffersEndpointRequest, + offers::send::SendOfferEndpointRequest + )) +)] +pub(crate) struct IssuanceApi; + +#[derive(OpenApi)] +#[openapi( + paths( + authorization_requests::authorization_requests, + authorization_requests::get_authorization_requests + ), + components(schemas(authorization_requests::AuthorizationRequestsEndpointRequest)) +)] +pub(crate) struct VerificationApi; + +#[derive(OpenApi)] +#[openapi( + paths( + holder::credentials::credentials, + holder::offers::offers, + holder::offers::accept::accept, + holder::offers::reject::reject + ), + components(schemas(openid4vci::Oid4vciOfferEndpointRequest)) +)] +pub(crate) struct HolderApi; + +pub(crate) fn did_web() -> PathItem { + PathItem::new( + HttpMethod::Get, + OperationBuilder::new() + .responses( + ResponsesBuilder::new() + .response( + "200", + ResponseBuilder::new() + .description("DID Document for `did:web` method") + .content("application/json", Content::new(Ref::from_schema_name("CoreDocument"))), + ) + .response("404", Response::new("DID method `did:web` inactive.")), + ) + .operation_id(Some("did_json")) + .summary(Some("DID Document for `did:web` method")) + .description(Some("Standard .well-known endpoint for self-hosted DID Document.")) + .tags(Some(vec!["(.well-known)", "(public)"])), + ) +} + +pub(crate) fn did_configuration() -> PathItem { + PathItem::new( + HttpMethod::Get, + OperationBuilder::new() + .responses( + ResponsesBuilder::new() + .response( + "200", + ResponseBuilder::new() + .description("DID Configuration Resource") + .content( + "application/json", + Content::new(Ref::from_schema_name("DomainLinkageConfiguration")), + // Content::new( + // ObjectBuilder::new() + // .schema_type(SchemaType::Type(schema::Type::Object)) + // .format(Some(schema::SchemaFormat::KnownFormat(schema::KnownFormat::Int64))), + // ), + ), + ) + .response("404", Response::new("Domain Linkage inactive.")), + ) + .operation_id(Some("did_configuration_json")) + .summary(Some("DID Configuration Resource for Domain Linkage")) + .description(Some("Standard .well-known endpoint for DID Configuration Resources.")) + .tags(Some(vec!["(.well-known)", "(public)"])), + ) +} diff --git a/agent_api_rest/src/verification/authorization_requests.rs b/agent_api_rest/src/verification/authorization_requests.rs index e934bc3f..fa0804a6 100644 --- a/agent_api_rest/src/verification/authorization_requests.rs +++ b/agent_api_rest/src/verification/authorization_requests.rs @@ -18,7 +18,22 @@ use oid4vp::PresentationDefinition; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::info; +use utoipa::ToSchema; +/// Get an Authorization Request +/// +/// Retrieve an existing Authorization Request. +#[utoipa::path( + get, + path = "/authorization_requests/{id}", + tag = "Verification", + params( + ("id" = String, Path, description = "The ID of the Authorization Request to retrieve.") + ), + responses( + (status = 200, description = "Successfully returns an existing Authorization Request.", body = [GenericAuthorizationRequest]) + ) +)] #[axum_macros::debug_handler] pub(crate) async fn get_authorization_requests( State(state): State, @@ -35,7 +50,7 @@ pub(crate) async fn get_authorization_requests( } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct AuthorizationRequestsEndpointRequest { pub nonce: String, pub state: Option, @@ -50,6 +65,18 @@ pub enum PresentationDefinitionResource { PresentationDefinition(PresentationDefinition), } +/// Create a new Authorization Request +/// +/// UniCore will ask a holder for certain information based on the Presentation Definition specified. +#[utoipa::path( + post, + path = "/authorization_requests", + request_body = AuthorizationRequestsEndpointRequest, + tag = "Verification", + responses( + (status = 201, description = "Authorization Request successfully created."), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn authorization_requests( State(verification_state): State, diff --git a/agent_api_rest/src/verification/relying_party/redirect.rs b/agent_api_rest/src/verification/relying_party/redirect.rs index 7e315071..a61de50b 100644 --- a/agent_api_rest/src/verification/relying_party/redirect.rs +++ b/agent_api_rest/src/verification/relying_party/redirect.rs @@ -10,6 +10,17 @@ use axum::{ Form, }; +/// Redirect Endpoint +/// +/// Standard OAuth 2.0 endpoint. +#[utoipa::path( + post, + path = "/redirect", + tags = ["(public)"], + responses( + (status = 200, description = ""), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn redirect( State(verification_state): State, diff --git a/agent_api_rest/src/verification/relying_party/request.rs b/agent_api_rest/src/verification/relying_party/request.rs index ff73e918..a9aa319b 100644 --- a/agent_api_rest/src/verification/relying_party/request.rs +++ b/agent_api_rest/src/verification/relying_party/request.rs @@ -7,9 +7,21 @@ use axum::{ }; use hyper::header; +/// Authorization Request +/// +/// Standard OAuth 2.0 endpoint. +/// /// Instead of directly embedding the Authorization Request into a QR-code or deeplink, the `Relying Party` can embed a /// `request_uri` that points to this endpoint from where the Authorization Request Object can be retrieved. /// As described here: https://www.rfc-editor.org/rfc/rfc9101.html#name-passing-a-request-object-by- +#[utoipa::path( + get, + path = "/request/{id}", + tags = ["(public)"], + responses( + (status = 200, description = ""), + ) +)] #[axum_macros::debug_handler] pub(crate) async fn request( State(verification_state): State, diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index f333905c..c0ce5c61 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -74,7 +74,6 @@ CREATE TABLE holder_credential PRIMARY KEY (view_id) ); - CREATE TABLE all_credentials ( view_id text NOT NULL, diff --git a/agent_application/src/main.rs b/agent_application/src/main.rs index 23933869..20ed55bc 100644 --- a/agent_application/src/main.rs +++ b/agent_application/src/main.rs @@ -15,7 +15,7 @@ use agent_verification::services::VerificationServices; use axum::{routing::get, Json}; use identity_document::service::{Service, ServiceEndpoint}; use std::sync::Arc; -use tokio::{fs, io}; +use tokio::io; use tower_http::cors::CorsLayer; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -155,12 +155,6 @@ async fn main() -> io::Result<()> { app = app.route(path, get(Json(did_document))); } - // This is used to indicate that the server accepts requests. - // In a docker container this file can be searched to see if its ready. - // A better solution can be made later (needed for impierce-demo) - fs::create_dir_all("/tmp/unicore/").await?; - fs::write("/tmp/unicore/accept_requests", []).await?; - let listener = tokio::net::TcpListener::bind("0.0.0.0:3033").await?; info!("listening on {}", listener.local_addr()?); axum::serve(listener, app).await?;