Skip to content

Commit

Permalink
Merge pull request #364 from esune/fix/subject-identifier
Browse files Browse the repository at this point in the history
Ensure user authentication sessions are independent
  • Loading branch information
esune authored Oct 12, 2023
2 parents 60c26cd + c56143a commit 3b493c1
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 87 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ After all these steps have been completed, you should be able to authenticate wi

To connect a debugger to the `vc-authn` controller service, start the project using `DEBUGGER=true ./manage start` and then launch the debugger, it should connect automatically to the container.

This is a sample debugger launch configuration for VSCode that can be used by adding it to `launch.json`:
This is a sample debugger launch configuration for VSCode that can be used by adding it to `launch.json`, it assumes a `.venv` folder containing the virtual environment was created in the repository root:
```json
{
"version": "0.1.0",
"version": "0.1.1",
"configurations": [
{
"name": "Python: Debug VC-AuthN Controller",
Expand All @@ -137,8 +137,13 @@ This is a sample debugger launch configuration for VSCode that can be used by ad
{
"localRoot": "${workspaceFolder}/oidc-controller",
"remoteRoot": "/app"
},
{
"localRoot": "${workspaceFolder}/.venv/Lib/site-packages",
"remoteRoot": "/usr/local/lib/python3.11/site-packages"
}
]
],
"justMyCode": false
}
]
}
Expand Down
22 changes: 22 additions & 0 deletions oidc-controller/api/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from bson import ObjectId
from pydantic import BaseModel, Field
from pyop.userinfo import Userinfo


class PyObjectId(ObjectId):
Expand Down Expand Up @@ -50,3 +51,24 @@ class GenericErrorMessage(BaseModel):
class RevealedAttribute(BaseModel):
sub_proof_index: int
values: dict


class VCUserinfo(Userinfo):
"""
User database for VC-based Identity provider: since no users are
known ahead of time, a new user is created with
every authentication request.
"""

def __getitem__(self, item):
"""
There is no user info database, we always return an empty dictionary
"""
return {}

def get_claims_for(self, user_id, requested_claims, userinfo=None):
# type: (str, Mapping[str, Optional[Mapping[str, Union[str, List[str]]]]) -> Dict[str, Union[str, List[str]]]
"""
There is no user info database, we always return an empty dictionary
"""
return {}
20 changes: 5 additions & 15 deletions oidc-controller/api/core/oidc/issue_token_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import dataclasses
import json
import uuid
from datetime import datetime
from typing import Any, Dict, List

Expand Down Expand Up @@ -86,22 +85,13 @@ def get_claims(
)
raise RuntimeError(err)

# look at all presentation_claims and one should
# match the configured subject_identifier
sub_id_value = None
# look at all presentation_claims for one
# matching the configured subject_identifier, if any
sub_id_claim = presentation_claims.get(ver_config.subject_identifier)

if not sub_id_claim:
logger.warning(
"""subject_identifer not found in presentation values,
generating random subject_identifier"""
)
sub_id_value = str(uuid.uuid4())
else:
sub_id_value = sub_id_claim.value

# add sub and append presentation_claims
oidc_claims.append(Claim(type="sub", value=sub_id_value))
if sub_id_claim:
# add sub and append presentation_claims
oidc_claims.append(Claim(type="sub", value=sub_id_claim.value))

result = {c.type: c.value for c in oidc_claims}
result[PROOF_CLAIMS_ATTRIBUTE_NAME] = json.dumps(
Expand Down
7 changes: 2 additions & 5 deletions oidc-controller/api/core/oidc/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import structlog.typing
from api.clientConfigurations.models import TOKENENDPOINTAUTHMETHODS
from api.core.config import settings
from api.core.models import VCUserinfo
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
Expand All @@ -15,7 +16,6 @@
from pyop.provider import Provider
from pyop.storage import StatelessWrapper
from pyop.subject_identifier import HashBasedSubjectIdentifierFactory
from pyop.userinfo import Userinfo

logger: structlog.typing.FilteringBoundLogger = structlog.get_logger()
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
Expand Down Expand Up @@ -128,9 +128,6 @@ async def init_provider(db: Database):

all_client_configs = await ClientConfigurationCRUD(db).get_all()
client_db = {d.client_name: d.dict() for d in all_client_configs}
user_db = {
"vc-user": {"sub": None}
} # placeholder, this will be replaced by the subject defined in the proof-configuration

provider = Provider(
signing_key,
Expand All @@ -142,5 +139,5 @@ async def init_provider(db: Database):
refresh_token_db=stateless_storage,
),
client_db,
Userinfo(user_db),
VCUserinfo({}),
)
94 changes: 59 additions & 35 deletions oidc-controller/api/core/oidc/tests/test_issue_token_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from api.authSessions.models import AuthSession
from api.core.oidc.issue_token_service import Token
from api.core.oidc.tests.__mocks__ import auth_session, presentation, ver_config
from api.test_utils import is_valid_uuid

basic_valid_requested_attributes = {
"req_attr_0": {
Expand All @@ -25,7 +24,7 @@
"raw": "[email protected]",
"encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915643",
}
}
},
}
}

Expand All @@ -52,24 +51,28 @@
"age_1": {
"raw": "30",
"encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915644",
}
}
},
},
}
}


@pytest.mark.asyncio
async def test_valid_proof_presentation_with_one_attribute_returns_claims():
presentation['presentation_request']['requested_attributes'] = basic_valid_requested_attributes
presentation['presentation']['requested_proof']['revealed_attr_groups'] = basic_valid_revealed_attr_groups
presentation["presentation_request"][
"requested_attributes"
] = basic_valid_requested_attributes
presentation["presentation"]["requested_proof"][
"revealed_attr_groups"
] = basic_valid_revealed_attr_groups
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
claims = Token.get_claims(auth_session, ver_config)
assert claims is not None


@pytest.mark.asyncio
async def test_valid_proof_presentation_with_multiple_attributes_returns_claims():
presentation['presentation_request']['requested_attributes'] = {
presentation["presentation_request"]["requested_attributes"] = {
"req_attr_0": {
"names": ["email"],
"restrictions": [
Expand All @@ -87,17 +90,17 @@ async def test_valid_proof_presentation_with_multiple_attributes_returns_claims(
"issuer_did": "MTYqmTBoLT7KLP5RNfgK3c",
}
],
}
},
}
presentation['presentation']['requested_proof']['revealed_attr_groups'] = {
presentation["presentation"]["requested_proof"]["revealed_attr_groups"] = {
"req_attr_0": {
"sub_proof_index": 0,
"values": {
"email": {
"raw": "[email protected]",
"encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915643",
}
}
},
},
"req_attr_1": {
"sub_proof_index": 0,
Expand All @@ -106,8 +109,8 @@ async def test_valid_proof_presentation_with_multiple_attributes_returns_claims(
"raw": "30",
"encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915644",
}
}
}
},
},
}
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
claims = Token.get_claims(auth_session, ver_config)
Expand All @@ -116,52 +119,65 @@ async def test_valid_proof_presentation_with_multiple_attributes_returns_claims(

@pytest.mark.asyncio
async def test_include_v1_attributes_false_does_not_add_the_named_attributes():
presentation['presentation_request']['requested_attributes'] = multiple_valid_requested_attributes
presentation['presentation']['requested_proof']['revealed_attr_groups'] = multiple_valid_revealed_attr_groups
presentation["presentation_request"][
"requested_attributes"
] = multiple_valid_requested_attributes
presentation["presentation"]["requested_proof"][
"revealed_attr_groups"
] = multiple_valid_revealed_attr_groups
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
ver_config.include_v1_attributes = False
claims = Token.get_claims(auth_session, ver_config)
vc_presented_attributes_obj = eval(claims["vc_presented_attributes"])
assert claims is not None
assert vc_presented_attributes_obj["email_1"] == '[email protected]'
assert vc_presented_attributes_obj["age_1"] == '30'
assert vc_presented_attributes_obj["email_1"] == "[email protected]"
assert vc_presented_attributes_obj["age_1"] == "30"
assert "email_1" not in claims
assert "age_1" not in claims


@pytest.mark.asyncio
async def test_include_v1_attributes_true_adds_the_named_attributes():
presentation['presentation_request']['requested_attributes'] = multiple_valid_requested_attributes
presentation['presentation']['requested_proof']['revealed_attr_groups'] = multiple_valid_revealed_attr_groups
presentation["presentation_request"][
"requested_attributes"
] = multiple_valid_requested_attributes
presentation["presentation"]["requested_proof"][
"revealed_attr_groups"
] = multiple_valid_revealed_attr_groups
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
ver_config.include_v1_attributes = True
claims = Token.get_claims(auth_session, ver_config)
vc_presented_attributes_obj = eval(claims["vc_presented_attributes"])
assert claims is not None
assert vc_presented_attributes_obj["email_1"] == '[email protected]'
assert vc_presented_attributes_obj["age_1"] == '30'
assert claims["email_1"] == '[email protected]'
assert claims["age_1"] == '30'
assert vc_presented_attributes_obj["email_1"] == "[email protected]"
assert vc_presented_attributes_obj["age_1"] == "30"
assert claims["email_1"] == "[email protected]"
assert claims["age_1"] == "30"


@pytest.mark.asyncio
async def test_include_v1_attributes_none_does_not_add_the_named_attributes():
presentation['presentation_request']['requested_attributes'] = multiple_valid_requested_attributes
presentation['presentation']['requested_proof']['revealed_attr_groups'] = multiple_valid_revealed_attr_groups
presentation["presentation_request"][
"requested_attributes"
] = multiple_valid_requested_attributes
presentation["presentation"]["requested_proof"][
"revealed_attr_groups"
] = multiple_valid_revealed_attr_groups
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
ver_config.include_v1_attributes = None
print(ver_config.include_v1_attributes)
claims = Token.get_claims(auth_session, ver_config)
vc_presented_attributes_obj = eval(claims["vc_presented_attributes"])
assert claims is not None
assert vc_presented_attributes_obj["email_1"] == '[email protected]'
assert vc_presented_attributes_obj["age_1"] == '30'
assert vc_presented_attributes_obj["email_1"] == "[email protected]"
assert vc_presented_attributes_obj["age_1"] == "30"
assert "email_1" not in claims
assert "age_1" not in claims


@pytest.mark.asyncio
async def test_revealed_attrs_dont_match_requested_attributes_throws_exception():
presentation['presentation_request']['requested_attributes'] = {
presentation["presentation_request"]["requested_attributes"] = {
"req_attr_0": {
"names": ["email"],
"restrictions": [
Expand All @@ -172,15 +188,15 @@ async def test_revealed_attrs_dont_match_requested_attributes_throws_exception()
],
}
}
presentation['presentation']['requested_proof']['revealed_attr_groups'] = {
presentation["presentation"]["requested_proof"]["revealed_attr_groups"] = {
"req_attr_0": {
"sub_proof_index": 0,
"values": {
"email-wrong": {
"raw": "[email protected]",
"encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915643",
}
}
},
}
}
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
Expand All @@ -190,19 +206,27 @@ async def test_revealed_attrs_dont_match_requested_attributes_throws_exception()

@pytest.mark.asyncio
async def test_valid_presentation_with_matching_subject_identifier_has_identifier_in_claims_sub():
presentation['presentation_request']['requested_attributes'] = basic_valid_requested_attributes
presentation['presentation']['requested_proof']['revealed_attr_groups'] = basic_valid_revealed_attr_groups
presentation["presentation_request"][
"requested_attributes"
] = basic_valid_requested_attributes
presentation["presentation"]["requested_proof"][
"revealed_attr_groups"
] = basic_valid_revealed_attr_groups
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
claims = Token.get_claims(auth_session, ver_config)
print(claims)
assert claims["sub"] == "[email protected]"


@pytest.mark.asyncio
async def test_valid_presentation_with_non_matching_subject_identifier_and_has_uuid_in_claims_sub():
presentation['presentation_request']['requested_attributes'] = basic_valid_requested_attributes
presentation['presentation']['requested_proof']['revealed_attr_groups'] = basic_valid_revealed_attr_groups
async def test_valid_presentation_with_non_matching_subject_identifier_and_has_no_sub():
presentation["presentation_request"][
"requested_attributes"
] = basic_valid_requested_attributes
presentation["presentation"]["requested_proof"][
"revealed_attr_groups"
] = basic_valid_revealed_attr_groups
with mock.patch.object(AuthSession, "presentation_exchange", presentation):
ver_config.subject_identifier = "not-email"
claims = Token.get_claims(auth_session, ver_config)
assert is_valid_uuid(claims["sub"]) is True
assert "sub" not in claims
Loading

0 comments on commit 3b493c1

Please sign in to comment.