Skip to content

Commit

Permalink
Merge branch 'openwallet-foundation:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
ODCP-DevSecOps-Automation authored Dec 11, 2024
2 parents bc8f9ea + a85b656 commit 718933f
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 35 deletions.
7 changes: 7 additions & 0 deletions oid4vc/demo/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ services:
--plugin oid4vc
--plugin sd_jwt_vc
--plugin mso_mdoc
--multitenant
--multitenant-admin
--jwt-secret insecure
--multitenancy-config wallet_type=single-wallet-askar key_derivation_method=RAW
healthcheck:
test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null
start_period: 30s
Expand Down Expand Up @@ -89,3 +93,6 @@ services:
- WDS_SOCKET_PORT=0
- API_BASE_URL=http://issuer:3001
- FORCE_COLOR=3
depends_on:
issuer:
condition: service_healthy
36 changes: 36 additions & 0 deletions oid4vc/demo/frontend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,16 @@ async function issue_jwt_credential(req, res) {
const commonHeaders = {
accept: "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + token.token,
};
if (API_KEY) {
commonHeaders["X-API-KEY"] = API_KEY;
}
axios.defaults.withCredentials = true;
axios.defaults.headers.common["Access-Control-Allow-Origin"] = API_BASE_URL;
axios.defaults.headers.common["X-API-KEY"] = API_KEY;
axios.defaults.headers.common["Authorization"] = "Bearer " + token.token;


const fetchApiData = async (url, options) => {
const response = await fetch(url, options);
Expand Down Expand Up @@ -253,13 +256,15 @@ async function issue_sdjwt_credential(req, res) {
const commonHeaders = {
accept: "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + token.token,
};
if (API_KEY) {
commonHeaders["X-API-KEY"] = API_KEY;
}
axios.defaults.withCredentials = true;
axios.defaults.headers.common["Access-Control-Allow-Origin"] = API_BASE_URL;
axios.defaults.headers.common["X-API-KEY"] = API_KEY;
axios.defaults.headers.common["Authorization"] = "Bearer " + token.token;

const fetchApiData = async (url, options) => {
const response = await fetch(url, options);
Expand Down Expand Up @@ -442,13 +447,16 @@ async function create_jwt_vc_presentation(req, res) {
const commonHeaders = {
accept: "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + token.token,
};
if (API_KEY) {
commonHeaders["X-API-KEY"] = API_KEY;
}
axios.defaults.withCredentials = true;
axios.defaults.headers.common["Access-Control-Allow-Origin"] = API_BASE_URL;
axios.defaults.headers.common["X-API-KEY"] = API_KEY;
axios.defaults.headers.common["Authorization"] = "Bearer " + token.token;


const fetchApiData = async (url, options) => {
const response = await fetch(url, options);
Expand Down Expand Up @@ -595,13 +603,16 @@ async function create_sd_jwt_presentation(req, res) {
const commonHeaders = {
accept: "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + token.token,
};
if (API_KEY) {
commonHeaders["X-API-KEY"] = API_KEY;
}
axios.defaults.withCredentials = true;
axios.defaults.headers.common["Access-Control-Allow-Origin"] = API_BASE_URL;
axios.defaults.headers.common["X-API-KEY"] = API_KEY;
axios.defaults.headers.common["Authorization"] = "Bearer " + token.token;


const fetchApiData = async (url, options) => {
const response = await fetch(url, options);
Expand Down Expand Up @@ -838,6 +849,31 @@ app.get("/", (req, res) => {
res.render("index", {"registrationId": uuidv4()});
});

const fetchApiData = async (url, options) => {
const response = await fetch(url, options);
return await response.json();
};

const token = await fetchApiData(
`${API_BASE_URL}/multitenancy/wallet`,
{
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(
{
"label": "Alice",
"wallet_type": "askar",
}
)
}
);

console.log("_______TOKEN________\n\n\n");
console.log(token);

// Render Credential Issuance form
app.get("/issue", (req, res) => {
res.render("issue-form", {"page": "register", "registrationId": uuidv4()});
Expand Down
58 changes: 46 additions & 12 deletions oid4vc/oid4vc/oid4vci_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
from acapy_agent.admin.server import debug_middleware, ready_middleware
from acapy_agent.config.injection_context import InjectionContext
from acapy_agent.core.profile import Profile
from acapy_agent.wallet.models.wallet_record import WalletRecord
from acapy_agent.storage.error import StorageError
from acapy_agent.messaging.models.base import BaseModelError
from acapy_agent.multitenant.base import BaseMultitenantManager
from aiohttp import web
from aiohttp_apispec import setup_aiohttp_apispec, validation_middleware

Expand Down Expand Up @@ -51,6 +55,7 @@ def __init__(
self.context = context
self.profile = root_profile
self.site = None
self.multitenant_manager = context.inject_or(BaseMultitenantManager)

async def make_application(self) -> web.Application:
"""Get the aiohttp application instance."""
Expand All @@ -61,18 +66,47 @@ async def make_application(self) -> web.Application:
async def setup_context(request: web.Request, handler):
"""Set up request context.
TODO: support Multitenancy context setup
Right now, this will only work for a standard agent instance. To
support multitenancy, we will need to include wallet identifiers in
the path and report that path in credential offers and issuer
metadata from a tenant.
This middleware is responsible for setting up the request context for the
handler. If multitenancy is enabled and a wallet_id is provided in the request
the wallet profile is retrieved and injected into the context.
Args:
request (web.Request): The incoming web request.
handler: The handler function to be executed.
Returns:
The result of executing the handler function with the updated request
context.
"""
admin_context = AdminRequestContext(
profile=self.profile,
# root_profile=self.profile, # TODO: support Multitenancy context setup
# metadata={}, # TODO: support Multitenancy context setup
)
request["context"] = admin_context
multitenant = self.multitenant_manager
wallet_id = request.match_info.get("wallet_id")

if multitenant and wallet_id:
try:
async with self.profile.session() as session:
wallet_record = await WalletRecord.retrieve_by_id(
session, wallet_id
)
except (StorageError, BaseModelError) as err:
raise web.HTTPBadRequest(reason=err.roll_up) from err
wallet_info = wallet_record.serialize()
wallet_key = wallet_info["settings"]["wallet.key"]
_, wallet_profile = await multitenant.get_wallet_and_profile(
self.context, wallet_id, wallet_key
)
admin_context = AdminRequestContext(
profile=wallet_profile,
root_profile=self.profile,
metadata={
"wallet_id": wallet_id,
"wallet_key": wallet_key,
},
)
request["context"] = admin_context
else:
request["context"] = AdminRequestContext(
profile=self.profile,
)
return await handler(request)

middlewares.append(setup_context)
Expand All @@ -94,7 +128,7 @@ async def setup_context(request: web.Request, handler):
]
)

await public_routes_register(app)
await public_routes_register(app, self.multitenant_manager)

cors = aiohttp_cors.setup(
app,
Expand Down
60 changes: 41 additions & 19 deletions oid4vc/oid4vc/public_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ class CredentialIssuerMetadataSchema(OpenAPISchema):
)
authorization_server = fields.Str(
required=False,
metadata={"description": "The authorization server endpoint. Currently ignored."},
metadata={
"description": "The authorization server endpoint. Currently ignored."
},
)
batch_credential_endpoint = fields.Str(
required=False,
Expand All @@ -91,13 +93,15 @@ async def credential_issuer_metadata(request: web.Request):
# TODO If there's a lot, this will be a problem
credentials_supported = await SupportedCredential.query(session)

metadata = {
"credential_issuer": f"{public_url}/",
"credential_endpoint": f"{public_url}/credential",
"credentials_supported": [
supported.to_issuer_metadata() for supported in credentials_supported
],
}
wallet_id = request.match_info.get("wallet_id")
subpath = f"/tenant/{wallet_id}" if wallet_id else ""
metadata = {
"credential_issuer": f"{public_url}{subpath}",
"credential_endpoint": f"{public_url}{subpath}/credential",
"credentials_supported": [
supported.to_issuer_metadata() for supported in credentials_supported
],
}

LOGGER.debug("METADATA: %s", metadata)

Expand Down Expand Up @@ -203,7 +207,9 @@ async def check_token(
return result


async def handle_proof_of_posession(profile: Profile, proof: Dict[str, Any], nonce: str):
async def handle_proof_of_posession(
profile: Profile, proof: Dict[str, Any], nonce: str
):
"""Handle proof of posession."""
encoded_headers, encoded_payload, encoded_signature = proof["jwt"].split(".", 3)
headers = b64_to_dict(encoded_headers)
Expand Down Expand Up @@ -309,7 +315,9 @@ async def issue_cred(request: web.Request):
if "proof" not in body:
raise web.HTTPBadRequest(reason=f"proof is required for {supported.format}")

pop = await handle_proof_of_posession(context.profile, body["proof"], ex_record.nonce)
pop = await handle_proof_of_posession(
context.profile, body["proof"], ex_record.nonce
)
if not pop.verified:
raise web.HTTPBadRequest(reason="Invalid proof")

Expand Down Expand Up @@ -447,6 +455,12 @@ async def get_request(request: web.Request):

now = int(time.time())
config = Config.from_settings(context.settings)
wallet_id = (
context.profile.settings.get("wallet.id")
if context.profile.settings.get("multitenant.enabled")
else None
)
subpath = f"/tenant/{wallet_id}" if wallet_id else ""
payload = {
"iss": jwk.did,
"sub": jwk.did,
Expand All @@ -455,7 +469,9 @@ async def get_request(request: web.Request):
"exp": now + 120,
"jti": str(uuid.uuid4()),
"client_id": config.endpoint,
"response_uri": f"{config.endpoint}/oid4vp/response/{pres.presentation_id}",
"response_uri": (
f"{config.endpoint}{subpath}/oid4vp/response/{pres.presentation_id}"
),
"state": pres.presentation_id,
"nonce": pres.nonce,
"id_token_signing_alg_values_supported": ["ES256", "EdDSA"],
Expand Down Expand Up @@ -529,7 +545,9 @@ async def verify_presentation(

processors = profile.inject(CredProcessors)
if not submission.descriptor_maps:
raise web.HTTPBadRequest(reason="Descriptor map of submission must not be empty")
raise web.HTTPBadRequest(
reason="Descriptor map of submission must not be empty"
)

# TODO: Support longer descriptor map arrays
if len(submission.descriptor_maps) != 1:
Expand Down Expand Up @@ -618,20 +636,24 @@ async def post_response(request: web.Request):
return web.Response(status=200)


async def register(app: web.Application):
"""Register routes."""
async def register(app: web.Application, multitenant: bool):
"""Register routes with support for multitenant mode.
Adds the subpath with Wallet ID as a path parameter if multitenant is True.
"""
subpath = "/tenant/{wallet_id}" if multitenant else ""
app.add_routes(
[
web.get(
"/.well-known/openid-credential-issuer",
f"{subpath}/.well-known/openid-credential-issuer",
credential_issuer_metadata,
allow_head=False,
),
# TODO Add .well-known/did-configuration.json
# Spec: https://identity.foundation/.well-known/resources/did-configuration/
web.post("/token", token),
web.post("/credential", issue_cred),
web.get("/oid4vp/request/{request_id}", get_request),
web.post("/oid4vp/response/{presentation_id}", post_response),
web.post(f"{subpath}/token", token),
web.post(f"{subpath}/credential", issue_cred),
web.get(f"{subpath}/oid4vp/request/{{request_id}}", get_request),
web.post(f"{subpath}/oid4vp/response/{{presentation_id}}", post_response),
]
)
16 changes: 14 additions & 2 deletions oid4vc/oid4vc/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,14 @@ async def get_cred_offer(request: web.BaseRequest):
raise web.HTTPBadRequest(reason=err.roll_up) from err

user_pin_required: bool = record.pin is not None
wallet_id = (
context.profile.settings.get("wallet.id")
if context.profile.settings.get("multitenant.enabled")
else None
)
subpath = f"/tenant/{wallet_id}" if wallet_id else ""
offer = {
"credential_issuer": config.endpoint,
"credential_issuer": f"{config.endpoint}{subpath}",
"credentials": [supported.identifier],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
Expand Down Expand Up @@ -782,7 +788,13 @@ async def create_oid4vp_request(request: web.Request):
await pres_record.save(session=session)

config = Config.from_settings(context.settings)
request_uri = quote(f"{config.endpoint}/oid4vp/request/{req_record._id}")
wallet_id = (
context.profile.settings.get("wallet.id")
if context.profile.settings.get("multitenant.enabled")
else None
)
subpath = f"/tenant/{wallet_id}" if wallet_id else ""
request_uri = quote(f"{config.endpoint}{subpath}/oid4vp/request/{req_record._id}")
full_uri = f"openid://?request_uri={request_uri}"

return web.json_response(
Expand Down
4 changes: 2 additions & 2 deletions oid4vc/oid4vc/tests/routes/test_public_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ async def test_issuer_metadata(context: AdminRequestContext, req: web.Request):
await test_module.credential_issuer_metadata(req)
mock_web.json_response.assert_called_once_with(
{
"credential_issuer": "http://localhost:8020/",
"credential_endpoint": "http://localhost:8020/credential",
"credential_issuer": f"http://localhost:8020/tenant/{req.match_info.get()}",
"credential_endpoint": f"http://localhost:8020/tenant/{req.match_info.get()}/credential",
"credentials_supported": [
{
"format": "jwt_vc_json",
Expand Down

0 comments on commit 718933f

Please sign in to comment.