Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure user authentication sessions are independent #364

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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