Skip to content

Commit

Permalink
refactor: move public routes to new module
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Bluhm <[email protected]>
  • Loading branch information
dbluhm committed Oct 31, 2023
1 parent 2247a88 commit a952e12
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 122 deletions.
126 changes: 4 additions & 122 deletions oid4vci/oid4vci/v1_0/oid4vci_server.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"""Admin server classes."""

import logging
import jwt as pyjwt

import aiohttp_cors
from aiohttp import web
from aiohttp_apispec import (
docs,
querystring_schema,
request_schema,
response_schema,
setup_aiohttp_apispec,
validation_middleware,
Expand All @@ -21,43 +18,11 @@
from aries_cloudagent.core.profile import Profile
from aries_cloudagent.messaging.models.openapi import OpenAPISchema
from aries_cloudagent.utils.stats import Collector
from aries_cloudagent.wallet.jwt import jwt_verify
from marshmallow import fields
from .models.cred_sup_record import OID4VCICredentialSupported

LOGGER = logging.getLogger(__name__)


class IssueCredentialRequestSchema(OpenAPISchema):
format = fields.Str(
required=True,
metadata={"description": "The client ID for the token request.", "example": ""},
)
types = fields.List(
fields.Str(),
metadata={"description": "List of connection records"},
)
credentialsSubject = fields.Dict(metadata={"description": ""})
proof = fields.Dict(metadata={"description": ""})


class TokenRequestSchema(OpenAPISchema):
"""Request schema for the /token endpoint."""
from .public_routes import register as public_routes_register

client_id = fields.Str(
required=True,
metadata={"description": "The client ID for the token request.", "example": ""},
)


class GetTokenSchema(OpenAPISchema):
"""Schema for ..."""

grant_type = fields.Str(required=True, metadata={"description": "", "example": ""})

pre_authorized_code = fields.Str(
required=True, metadata={"description": "", "example": ""}
)
LOGGER = logging.getLogger(__name__)


class AdminResetSchema(OpenAPISchema):
Expand Down Expand Up @@ -109,46 +74,6 @@ async def make_application(self) -> web.Application:

middlewares = [ready_middleware, debug_middleware, validation_middleware]

def is_unprotected_path(path: str):
return path in [
# public oid4vci
"/.well-known/openid-credential-issuer",
"/token",
"/credential-offer",
# public swagger
"/api/doc",
"/api/docs/swagger.json",
# non protected health checks
"/status/live",
"/status/ready",
] or path.startswith("/static/swagger/")

@web.middleware
async def check_token(request: web.Request, handler):
# get token
authorization_header = request.headers.get("Authorization")
if is_unprotected_path(request.path):
return await handler(request)

if not authorization_header:
raise web.HTTPUnauthorized() # no authentication

scheme, cred = authorization_header.split(" ")
if scheme.lower() != "bearer" or ():
raise web.HTTPUnauthorized() # Invalid authentication credentials

jwt_header = pyjwt.get_unverified_header(cred)
if "did:key:" not in jwt_header["kid"]:
raise web.HTTPUnauthorized() # Invalid authentication credentials

result = await jwt_verify(self.profile, cred)
if result.valid:
return await handler(request)
else:
raise web.HTTPUnauthorized() # Invalid credentials

middlewares.append(check_token)

@web.middleware
async def setup_context(request: web.Request, handler):
profile = self.profile
Expand All @@ -174,22 +99,15 @@ async def setup_context(request: web.Request, handler):

app.add_routes(
[
web.get(
"/.well-known/openid-credential-issuer",
self.oid_cred_issuer,
allow_head=False,
),
# web.get("/.well-known/", self., allow_head=False),
# web.get("/.well-known/", self., allow_head=False),
web.post("/credential", self.issue_cred),
web.post("/token", self.get_token),
web.get("/", self.redirect_handler, allow_head=True),
web.post("/status/reset", self.status_reset_handler),
web.get("/status/live", self.liveliness_handler, allow_head=False),
web.get("/status/ready", self.readiness_handler, allow_head=False),
]
)

await public_routes_register(app)

cors = aiohttp_cors.setup(
app,
defaults={
Expand Down Expand Up @@ -249,42 +167,6 @@ async def stop(self) -> None:
await self.site.stop()
self.site = None

@docs(tags=["oid4vci"], summary="Get credential issuer metadata")
@querystring_schema(TokenRequestSchema())
async def oid_cred_issuer(self, request: web.BaseRequest):
"""Credential issuer metadata endpoint."""
profile = request["context"].profile
public_url = profile.context.settings.get("public_url") # TODO: check

# Wallet query to retrieve credential definitions
tag_filter = {"type": {"$in": ["sd_jwt", "jwt_vc_json"]}}
async with profile.session() as session:
credentials_supported = await OID4VCICredentialSupported.query(
session, tag_filter
)

metadata = {
"credential_issuer": f"{public_url}/issuer",
"credential_endpoint": f"{public_url}/credential",
"credentials_supported": [
cred.serialize() for cred in credentials_supported
],
"authorization_server": f"{public_url}/auth-server",
"batch_credential_endpoint": f"{public_url}/batch_credential",
}

return web.json_response(metadata)

@docs(tags=["oid4vci"], summary="Issue a credential")
@request_schema(IssueCredentialRequestSchema())
async def issue_cred(self, request: web.BaseRequest):
pass

@docs(tags=["oid4vci"], summary="Get credential issuance token")
@querystring_schema(TokenRequestSchema())
async def get_token(self, request: web.BaseRequest):
"""Token endpoint to exchange pre_authorized codes for access tokens."""

@docs(tags=["server"], summary="Reset statistics")
@response_schema(AdminResetSchema(), 200, description="")
async def status_reset_handler(self, request: web.BaseRequest):
Expand Down
127 changes: 127 additions & 0 deletions oid4vci/oid4vci/v1_0/public_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Public routes for OID4VCI."""

import logging
from typing import Optional
from aries_cloudagent.core.profile import Profile
import jwt as pyjwt

from aiohttp import web
from aiohttp_apispec import (
docs,
querystring_schema,
request_schema,
)
from aries_cloudagent.messaging.models.openapi import OpenAPISchema
from aries_cloudagent.wallet.jwt import jwt_verify
from marshmallow import fields
from .models.cred_sup_record import OID4VCICredentialSupported

LOGGER = logging.getLogger(__name__)


class IssueCredentialRequestSchema(OpenAPISchema):
"""Request schema for the /credential endpoint."""

format = fields.Str(
required=True,
metadata={"description": "The client ID for the token request.", "example": ""},
)
types = fields.List(
fields.Str(),
metadata={"description": "List of connection records"},
)
credentialsSubject = fields.Dict(metadata={"description": ""})
proof = fields.Dict(metadata={"description": ""})


class TokenRequestSchema(OpenAPISchema):
"""Request schema for the /token endpoint."""

client_id = fields.Str(
required=True,
metadata={"description": "The client ID for the token request.", "example": ""},
)


class GetTokenSchema(OpenAPISchema):
"""Schema for ..."""

grant_type = fields.Str(required=True, metadata={"description": "", "example": ""})

pre_authorized_code = fields.Str(
required=True, metadata={"description": "", "example": ""}
)


@docs(tags=["oid4vci"], summary="Get credential issuer metadata")
@querystring_schema(TokenRequestSchema())
async def oid_cred_issuer(request: web.Request):
"""Credential issuer metadata endpoint."""
profile = request["context"].profile
public_url = profile.context.settings.get("public_url") # TODO: check

# Wallet query to retrieve credential definitions
tag_filter = {"type": {"$in": ["sd_jwt", "jwt_vc_json"]}}
async with profile.session() as session:
credentials_supported = await OID4VCICredentialSupported.query(
session, tag_filter
)

metadata = {
"credential_issuer": f"{public_url}/issuer",
"credential_endpoint": f"{public_url}/credential",
"credentials_supported": [cred.serialize() for cred in credentials_supported],
"authorization_server": f"{public_url}/auth-server",
"batch_credential_endpoint": f"{public_url}/batch_credential",
}

return web.json_response(metadata)


async def check_token(profile: Profile, auth_header: Optional[str] = None):
"""Validate the OID4VCI token."""
if not auth_header:
raise web.HTTPUnauthorized() # no authentication

scheme, cred = auth_header.split(" ")
if scheme.lower() != "bearer" or ():
raise web.HTTPUnauthorized() # Invalid authentication credentials

jwt_header = pyjwt.get_unverified_header(cred)
if "did:key:" not in jwt_header["kid"]:
raise web.HTTPUnauthorized() # Invalid authentication credentials

result = await jwt_verify(profile, cred)
if not result.valid:
raise web.HTTPUnauthorized() # Invalid credentials


@docs(tags=["oid4vci"], summary="Issue a credential")
@request_schema(IssueCredentialRequestSchema())
async def issue_cred(request: web.Request):
"""Credential issuance endpoint."""
profile = request["context"].profile
await check_token(profile, request.headers.get("Authorization"))


@docs(tags=["oid4vci"], summary="Get credential issuance token")
@querystring_schema(TokenRequestSchema())
async def get_token(request: web.Request):
"""Token endpoint to exchange pre_authorized codes for access tokens."""


async def register(app: web.Application):
"""Register routes."""
app.add_routes(
[
web.get(
"/.well-known/openid-credential-issuer",
oid_cred_issuer,
allow_head=False,
),
# web.get("/.well-known/", self., allow_head=False),
# web.get("/.well-known/", self., allow_head=False),
web.post("/credential", issue_cred),
web.post("/token", get_token),
]
)

0 comments on commit a952e12

Please sign in to comment.