Skip to content

Commit 2e59950

Browse files
authored
feat: align with draft13 and enable auth server integration (#2262)
* feat: align with draft13 and enable auth server integration Signed-off-by: Ivan Wei <ivan.wei@ontario.ca> * fix: lint and unit tests Signed-off-by: Ivan Wei <ivan.wei@ontario.ca> * fix: integration test Signed-off-by: Ivan Wei <ivan.wei@ontario.ca> * fix: code review Signed-off-by: Ivan Wei <ivan.wei@ontario.ca> --------- Signed-off-by: Ivan Wei <ivan.wei@ontario.ca>
1 parent cbc8205 commit 2e59950

24 files changed

+1437
-200
lines changed

oid4vc/README.md

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,17 @@ DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build -f ../docker/Dockerfile --tag o
3434

3535
### Demo Flow
3636

37-
Navigate to `http://localhost:3002` in your browser. You will start at the landing page. The sidebar has buttons to take you to the issuance and presentation pages.
37+
Navigate to `http://localhost:3002` in your browser. You will start at the landing page. The sidebar has buttons to take you to the issuance and presentation pages.
3838

3939
1. Issue Credential
4040

4141
- This page generates a simple `UniversityCredential` for issuance
42-
- The demo obscures and automates the necessary `credential-supported/create` call, which is what defines the type and values of a credential that can be issued
42+
43+
- The demo obscures and automates the necessary `credential-supported/create` call, which is what defines the type and values of a credential that can be issued
4344

4445
- Preparing a credential offer is simple:
45-
- Enter your name and email, or use the test value provided, and hit `Register`
46-
- Once you hit `Register`, you'll be automatically taken to the Credential Offer Page
46+
- Enter your name and email, or use the test value provided, and hit `Register`
47+
- Once you hit `Register`, you'll be automatically taken to the Credential Offer Page
4748

4849
2. Credential Offer Page
4950
- Presents a credential offer in the form of a QR code.
@@ -145,7 +146,9 @@ controller ->> alice: redirect to success page
145146
end
146147
end
147148
```
149+
148150
### Credential Presentation
151+
149152
```mermaid
150153
sequenceDiagram
151154
autonumber
@@ -164,13 +167,13 @@ admin ->> acapy: store presentation definition
164167
admin -->> controller: created presentation definition
165168
alice ->> controller: Hits web page initiating presentation
166169
controller ->> admin: POST /oid4vp/request
167-
admin ->> acapy: save request record associated <br/>with a particular pres def
170+
admin ->> acapy: save request record associated <br/>with a particular pres def
168171
admin -->> controller: request URI
169172
controller ->> alice: QR Code
170173
alice ->> holder: Scan QR Code
171174
holder ->> public: GET /oid4vp/request/{request_id} (request uri in QR code)
172175
public -> acapy: retrieve stored request
173-
public -->> holder: request
176+
public -->> holder: request
174177
holder ->> public: POST /oid4vp/response/{presentation_id}
175178
acapy ->> controller: POST /topic/oid4vp <br/>(state: presentation-valid/invalid)
176179
controller ->> holder: result
@@ -183,26 +186,26 @@ controller ->> holder: result
183186
The Plugin expects the following configuration options. These options can either be set by environment variable (`OID4VCI_*`) or by plugin config value (`-o oid4vci.*`).
184187

185188
- `OID4VCI_HOST` or `oid4vci.host`
186-
- Host used for the OpenID4VCI public server
189+
- Host used for the OpenID4VCI public server
187190
- `OID4VCI_PORT` or `oid4vci.port`
188-
- Port used for the OpenID4VCI public server
191+
- Port used for the OpenID4VCI public server
189192
- `OID4VCI_ENDPOINT` or `oid4vci.endpoint`
190-
- `credential_issuer` endpoint, seen in the Credential Offer
193+
- `credential_issuer` endpoint, seen in the Credential Offer
191194
- `OID4VCI_CRED_HANDLER` or `oid4vci.cred_handler`
192-
- Dict of credential handlers. e.g. `{"jwt_vc_json": "jwt_vc_json"}`
195+
- Dict of credential handlers. e.g. `{"jwt_vc_json": "jwt_vc_json"}`
196+
- `OID4VCI_AUTH_SERVER_URL` or `oid4vci.auth_server_url`
197+
- Optional authorization server URL
198+
- `OID4VCI_AUTH_SERVER_CLIENT` or `oid4vci.auth_server_client`
199+
- Optional authorization server client credential, e.g. `{"auth_type": "client_secret_basic", "client_id": "client_id", "client_secret": "client_secret"}`
193200

194201
### Creating Supported Credential Records
195202

196203
To issue a credential using OpenID4VCI, the Issuer must first prepare credential issuer metadata including which credentials the Issuer can issue. Below is an example payload to the `POST /oid4vci/credential-supported/create/jwt` endpoint:
197204

198205
```json
199206
{
200-
"cryptographic_binding_methods_supported": [
201-
"did"
202-
],
203-
"cryptographic_suites_supported": [
204-
"ES256K"
205-
],
207+
"cryptographic_binding_methods_supported": ["did"],
208+
"cryptographic_suites_supported": ["ES256K"],
206209
"display": [
207210
{
208211
"name": "University Credential",
@@ -242,15 +245,12 @@ To issue a credential using OpenID4VCI, the Issuer must first prepare credential
242245
]
243246
}
244247
},
245-
"type": [
246-
"VerifiableCredential",
247-
"UniversityDegreeCredential"
248-
],
248+
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
249249
"id": "UniversityDegreeCredential",
250250
"@context": [
251251
"https://www.w3.org/2018/credentials/v1",
252252
"https://www.w3.org/2018/credentials/examples/v1"
253-
],
253+
]
254254
}
255255
```
256256

@@ -268,12 +268,8 @@ When the Controller sets up a Supported Credential record using the Admin API, t
268268
"credentials_supported": [
269269
{
270270
"format": "jwt_vc_json",
271-
"cryptographic_binding_methods_supported": [
272-
"did"
273-
],
274-
"cryptographic_suites_supported": [
275-
"ES256K"
276-
],
271+
"cryptographic_binding_methods_supported": ["did"],
272+
"cryptographic_suites_supported": ["ES256K"],
277273
"display": [
278274
{
279275
"name": "University Credential",
@@ -313,10 +309,7 @@ When the Controller sets up a Supported Credential record using the Admin API, t
313309
]
314310
}
315311
},
316-
"types": [
317-
"VerifiableCredential",
318-
"UniversityDegreeCredential"
319-
]
312+
"types": ["VerifiableCredential", "UniversityDegreeCredential"]
320313
}
321314
]
322315
}

oid4vc/integration/oid4vci_client/client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44
from dataclasses import dataclass
5-
from typing import Dict, List, Literal, Optional, Union
5+
from typing import Dict, List, Literal, Optional, Union, Any
66
from urllib.parse import parse_qsl, urlparse
77

88
from aiohttp import ClientSession
@@ -61,7 +61,7 @@ class IssuerMetadata:
6161

6262
credential_endpoint: str
6363
token_endpoint: str
64-
credentials_supported: List[dict]
64+
credential_configurations_supported: dict[str, Any]
6565

6666

6767
@dataclass
@@ -101,7 +101,7 @@ async def get_issuer_metadata(self, issuer_url: str):
101101
return IssuerMetadata(
102102
metadata["credential_endpoint"],
103103
token_endpoint,
104-
metadata["credentials_supported"],
104+
metadata["credential_configurations_supported"],
105105
)
106106

107107
async def request_token(self, offer: CredentialOffer, metadata: IssuerMetadata):

oid4vc/jwt_vc_json/cred_processor.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from pydid import DIDUrl
1111

1212
from oid4vc.cred_processor import (
13-
CredProcessorError,
1413
CredVerifier,
1514
Issuer,
1615
PresVerifier,
@@ -21,7 +20,6 @@
2120
from oid4vc.models.presentation import OID4VPPresentation
2221
from oid4vc.models.supported_cred import SupportedCredential
2322
from oid4vc.pop_result import PopResult
24-
from oid4vc.public_routes import types_are_subset
2523
from oid4vc.status_handler import StatusHandler
2624

2725
LOGGER = logging.getLogger(__name__)
@@ -40,8 +38,6 @@ async def issue(
4038
) -> Any:
4139
"""Return signed credential in JWT format."""
4240
assert supported.format_data
43-
if not types_are_subset(body.get("types"), supported.format_data.get("types")):
44-
raise CredProcessorError("Requested types does not match offer.")
4541

4642
current_time = datetime.datetime.now(datetime.timezone.utc)
4743
current_time_unix_timestamp = int(current_time.timestamp())

oid4vc/oid4vc/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212

1313
from jwt_vc_json.cred_processor import JwtVcJsonCredProcessor
1414
from oid4vc.cred_processor import CredProcessors
15-
from .status_handler import StatusHandler
1615

16+
from .app_resources import AppResources
1717
from .config import Config
1818
from .jwk import DID_JWK, P256
1919
from .jwk_resolver import JwkResolver
2020
from .oid4vci_server import Oid4vciServer
21+
from .status_handler import StatusHandler
2122

2223
LOGGER = logging.getLogger(__name__)
2324

@@ -64,6 +65,7 @@ async def startup(profile: Profile, event: Event):
6465
profile,
6566
)
6667
profile.context.injector.bind_instance(Oid4vciServer, oid4vci)
68+
await AppResources.startup(config)
6769
except Exception:
6870
LOGGER.exception("Unable to register admin server")
6971
raise
@@ -76,3 +78,4 @@ async def shutdown(context: InjectionContext):
7678
"""Teardown the plugin."""
7779
oid4vci = context.inject(Oid4vciServer)
7880
await oid4vci.stop()
81+
await AppResources.shutdown()

oid4vc/oid4vc/app_resources.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""App resources."""
2+
3+
import logging
4+
import aiohttp
5+
import asyncio
6+
7+
from .config import Config
8+
9+
LOGGER = logging.getLogger(__name__)
10+
11+
12+
class AppResources:
13+
"""Application-wide resources like HTTP client and cleanup tasks."""
14+
15+
_auth_server_url: str | None = None
16+
_http_client: aiohttp.ClientSession | None = None
17+
_cleanup_task: asyncio.Task | None = None
18+
_client_shutdown: bool = False
19+
20+
@classmethod
21+
async def startup(cls, config: Config | None = None):
22+
"""Initialize resources."""
23+
if config and config.auth_server_url:
24+
cls._auth_server_url = config.auth_server_url
25+
LOGGER.info("Initializing HTTP client...")
26+
cls._http_client = aiohttp.ClientSession()
27+
# LOGGER.info("Starting up cleanup task...")
28+
# cls._cleanup_task = asyncio.create_task(cls._background_cleanup())
29+
30+
@classmethod
31+
async def shutdown(cls):
32+
"""Clean up resources."""
33+
if cls._cleanup_task:
34+
LOGGER.info("Shutting down cleanup task...")
35+
cls._cleanup_task.cancel()
36+
try:
37+
await cls._cleanup_task
38+
except asyncio.CancelledError:
39+
pass
40+
cls._cleanup_task = None
41+
if cls._http_client:
42+
LOGGER.info("Closing HTTP client...")
43+
await cls._http_client.close()
44+
cls._http_client = None
45+
cls._client_shutdown = True
46+
47+
@classmethod
48+
def get_http_client(cls) -> aiohttp.ClientSession:
49+
"""Get the initialized HTTP client."""
50+
if cls._client_shutdown:
51+
raise RuntimeError("HTTP client was shut down and cannot be re-initialized")
52+
if cls._auth_server_url and cls._http_client is None:
53+
LOGGER.warning("Warning: HTTP client was None, re-initializing.")
54+
cls._http_client = aiohttp.ClientSession()
55+
if cls._http_client is None:
56+
raise RuntimeError("HTTP client is not initialized")
57+
return cls._http_client
58+
59+
@classmethod
60+
async def _background_cleanup(cls):
61+
"""Background task for periodic cleanup."""
62+
while True:
63+
try:
64+
await cleanup_expired_nonces()
65+
except Exception as e:
66+
LOGGER.exception(f"Nonce cleanup error: {e}")
67+
68+
await asyncio.sleep(3600) # Run every hour
69+
70+
71+
async def cleanup_expired_nonces():
72+
"""Cleanup expired nonces from storage."""
73+
pass

oid4vc/oid4vc/config.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ class Config:
2525
host: str
2626
port: int
2727
endpoint: str
28-
status_handler: str
28+
status_handler: str | None = None
29+
auth_server_url: str | None = None
30+
auth_server_client: str | None = None
2931

3032
@classmethod
3133
def from_settings(cls, settings: BaseSettings) -> "Config":
@@ -38,12 +40,19 @@ def from_settings(cls, settings: BaseSettings) -> "Config":
3840
status_handler = plugin_settings.get("status_handler") or getenv(
3941
"OID4VCI_STATUS_HANDLER"
4042
)
41-
43+
auth_server_url = plugin_settings.get("auth_server_url") or getenv(
44+
"OID4VCI_AUTH_SERVER_URL"
45+
)
46+
auth_server_client = plugin_settings.get("auth_server_client") or getenv(
47+
"OID4VCI_AUTH_SERVER_CLIENT"
48+
)
4249
if not host:
4350
raise ConfigError("host", "OID4VCI_HOST")
4451
if not port:
4552
raise ConfigError("port", "OID4VCI_PORT")
4653
if not endpoint:
4754
raise ConfigError("endpoint", "OID4VCI_ENDPOINT")
4855

49-
return cls(host, port, endpoint, status_handler)
56+
return cls(
57+
host, port, endpoint, status_handler, auth_server_url, auth_server_client
58+
)

oid4vc/oid4vc/jwt.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ async def jwt_sign(
100100
wallet = session.inject(BaseWallet)
101101
did_info = await wallet.get_local_did(did_lookup_name(did))
102102

103-
did_info = await wallet.get_local_did(did_lookup_name(did))
104103
if did_info.key_type == ED25519:
105104
headers["alg"] = "EdDSA"
106105
elif did_info.key_type == P256:

0 commit comments

Comments
 (0)