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

CRUD of OIDC clients #250

Merged
merged 9 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
20 changes: 12 additions & 8 deletions oidc-controller/api/authSessions/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
AuthSessionCreate,
AuthSessionPatch,
)
from ..db.session import COLLECTION_NAMES
from api.db.session import COLLECTION_NAMES


logger = logging.getLogger(__name__)
Expand All @@ -27,13 +27,13 @@ async def create(self, auth_session: AuthSessionCreate) -> AuthSession:
result = col.insert_one(jsonable_encoder(auth_session))
return AuthSession(**col.find_one({"_id": result.inserted_id}))

async def get(self, auth_session_id: str) -> AuthSession:
if not PyObjectId.is_valid(auth_session_id):
async def get(self, id: str) -> AuthSession:
if not PyObjectId.is_valid(id):
raise HTTPException(
status_code=http_status.HTTP_400_BAD_REQUEST, detail=f"Invalid id: {id}"
)
col = self._db.get_collection(COLLECTION_NAMES.AUTH_SESSION)
auth_sess = col.find_one({"_id": PyObjectId(auth_session_id)})
auth_sess = col.find_one({"_id": PyObjectId(id)})

if auth_sess is None:
raise HTTPException(
Expand All @@ -43,19 +43,23 @@ async def get(self, auth_session_id: str) -> AuthSession:

return AuthSession(**auth_sess)

async def patch(self, auth_session_id: str, data: AuthSessionPatch) -> AuthSession:
async def patch(self, id: str, data: AuthSessionPatch) -> AuthSession:
esune marked this conversation as resolved.
Show resolved Hide resolved
col = self._db.get_collection(COLLECTION_NAMES.AUTH_SESSION)
auth_sess = col.find_one_and_update(
{"_id": auth_session_id},
{"_id": id},
{"$set": data.dict(exclude_unset=True)},
return_document=ReturnDocument.AFTER,
)

return auth_sess

async def delete(self, auth_session_id: str) -> bool:
async def delete(self, id: str) -> bool:
if not PyObjectId.is_valid(id):
raise HTTPException(
status_code=http_status.HTTP_400_BAD_REQUEST, detail=f"Invalid id: {id}"
)
col = self._db.get_collection(COLLECTION_NAMES.AUTH_SESSION)
auth_sess = col.find_one_and_delete({"_id": auth_session_id})
auth_sess = col.find_one_and_delete({"_id": PyObjectId(id)})
return bool(auth_sess)

async def get_by_pres_exch_id(self, pres_exch_id: str) -> AuthSession:
Expand Down
86 changes: 86 additions & 0 deletions oidc-controller/api/clientConfigurations/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import logging

from typing import List
from pymongo import ReturnDocument
from pymongo.database import Database
from fastapi import HTTPException
from fastapi import status as http_status
from fastapi.encoders import jsonable_encoder

from ..core.models import PyObjectId
from .models import (
ClientConfiguration,
ClientConfigurationCreate,
ClientConfigurationPatch,
ClientConfigurationRead,
)
from ..db.session import COLLECTION_NAMES
from api.core.oidc.provider import init_provider


logger = logging.getLogger(__name__)


class ClientConfigurationCRUD:
def __init__(self, db: Database):
self._db = db

async def create(
self, client_config: ClientConfigurationCreate
) -> ClientConfiguration:
col = self._db.get_collection(COLLECTION_NAMES.CLIENT_CONFiGURATIONS)
result = col.insert_one(jsonable_encoder(client_config))

# remake provider instance to refresh provider client
await init_provider()
return ClientConfiguration(**col.find_one({"_id": result.inserted_id}))

async def get(self, id: str) -> ClientConfigurationRead:
if not PyObjectId.is_valid(id):
raise HTTPException(
status_code=http_status.HTTP_400_BAD_REQUEST, detail=f"Invalid id: {id}"
)
col = self._db.get_collection(COLLECTION_NAMES.CLIENT_CONFiGURATIONS)
obj = col.find_one({"_id": PyObjectId(id)})

if obj is None:
raise HTTPException(
status_code=http_status.HTTP_404_NOT_FOUND,
detail="The auth_session hasn't been found!",
)

return ClientConfiguration(**obj)

async def get_all(self) -> List[ClientConfigurationRead]:
col = self._db.get_collection(COLLECTION_NAMES.CLIENT_CONFiGURATIONS)
return [ClientConfigurationRead(**cc) for cc in col.find()]

async def patch(
self, id: str, data: ClientConfigurationPatch
) -> ClientConfiguration:
if not PyObjectId.is_valid(id):
raise HTTPException(
status_code=http_status.HTTP_400_BAD_REQUEST, detail=f"Invalid id: {id}"
)
col = self._db.get_collection(COLLECTION_NAMES.CLIENT_CONFiGURATIONS)
obj = col.find_one_and_update(
{"_id": PyObjectId(id)},
{"$set": data.dict(exclude_unset=True)},
return_document=ReturnDocument.AFTER,
)
# remake provider instance to refresh provider client
await init_provider()
return obj

async def delete(self, id: str) -> bool:
if not PyObjectId.is_valid(id):
raise HTTPException(
status_code=http_status.HTTP_400_BAD_REQUEST, detail=f"Invalid id: {id}"
)
col = self._db.get_collection(COLLECTION_NAMES.CLIENT_CONFiGURATIONS)
obj = col.find_one_and_delete({"_id": PyObjectId(id)})

# remake provider instance to refresh provider client
await init_provider()

return bool(obj)
10 changes: 10 additions & 0 deletions oidc-controller/api/clientConfigurations/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from api.core.config import settings

ex_client_config_create = {
"client_id": settings.KEYCLOAK_CLIENT_ID,
"client_name": settings.KEYCLOAK_CLIENT_NAME,
"client_secret": "**********",
"response_types": ["code", "id_token", "token"],
"token_endpoint_auth_method": "client_secret_basic",
"redirect_uris": [settings.KEYCLOAK_REDIRECT_URI],
}
44 changes: 44 additions & 0 deletions oidc-controller/api/clientConfigurations/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from enum import Enum
from typing import List
from pydantic import BaseModel, Field

from api.core.models import UUIDModel
from api.core.config import settings

from .examples import ex_client_config_create


class TOKENENDPOINTAUTHMETHODS(str, Enum):
client_secret_basic = "client_secret_basic"


class ClientConfigurationBase(BaseModel):
client_id: str = Field(default=settings.KEYCLOAK_CLIENT_ID)
client_name: str = Field(default=settings.KEYCLOAK_CLIENT_NAME)
response_types: List[str] = Field(default=["code", "id_token", "token"])
redirect_uris: List[str]
token_endpoint_auth_method: TOKENENDPOINTAUTHMETHODS = Field(
default=TOKENENDPOINTAUTHMETHODS.client_secret_basic
)

client_secret: str = Field(default=settings.KEYCLOAK_CLIENT_SECRET)

class Config:
allow_population_by_field_name = True


class ClientConfiguration(ClientConfigurationBase, UUIDModel):
pass


class ClientConfigurationRead(ClientConfigurationBase, UUIDModel):
pass


class ClientConfigurationCreate(ClientConfigurationBase):
class Config:
schema_extra = {"example": ex_client_config_create}


class ClientConfigurationPatch(ClientConfigurationBase):
pass
84 changes: 84 additions & 0 deletions oidc-controller/api/clientConfigurations/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from pymongo.database import Database

from fastapi import APIRouter, HTTPException, Depends
from fastapi import status as http_status

from ..core.models import StatusMessage

from .crud import ClientConfigurationCRUD
from .models import (
ClientConfigurationRead,
ClientConfigurationPatch,
ClientConfigurationCreate,
)
from ..core.auth import get_api_key
from ..db.session import get_db

router = APIRouter()


@router.post(
"/",
response_description="Add new verification configuration",
status_code=http_status.HTTP_201_CREATED,
response_model=ClientConfigurationCreate,
response_model_exclude_unset=True,
dependencies=[Depends(get_api_key)],
)
async def create_client_config(
ver_config: ClientConfigurationCreate, db: Database = Depends(get_db)
):
return await ClientConfigurationCRUD(db).create(ver_config)


@router.get(
"/",
status_code=http_status.HTTP_200_OK,
response_model_exclude_unset=True,
dependencies=[Depends(get_api_key)],
)
async def get_all_client_configs(db: Database = Depends(get_db)):
return await ClientConfigurationCRUD(db).get_all()


@router.get(
"/{client_config_id}",
status_code=http_status.HTTP_200_OK,
response_model=ClientConfigurationRead,
response_model_exclude_unset=True,
dependencies=[Depends(get_api_key)],
)
async def get_client_config(client_config_id: str, db: Database = Depends(get_db)):
return await ClientConfigurationCRUD(db).get(client_config_id)


@router.patch(
"/{client_config_id}",
status_code=http_status.HTTP_200_OK,
response_model=ClientConfigurationRead,
response_model_exclude_unset=True,
dependencies=[Depends(get_api_key)],
)
async def patch_client_config(
client_config_id: str,
data: ClientConfigurationPatch,
db: Database = Depends(get_db),
):
return await ClientConfigurationCRUD(db).patch(id=client_config_id, data=data)


@router.delete(
"/{client_config_id}",
status_code=http_status.HTTP_200_OK,
response_model=StatusMessage,
dependencies=[Depends(get_api_key)],
)
async def delete_client_config(client_config_id: str, db: Database = Depends(get_db)):
status = await ClientConfigurationCRUD(db).delete(id=client_config_id)

if not status:
raise HTTPException(
status_code=http_status.HTTP_404_NOT_FOUND,
detail="client_config does not exist",
)
return StatusMessage(status=status, message="The client_config was deleted")
43 changes: 19 additions & 24 deletions oidc-controller/api/core/oidc/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
from pyop.userinfo import Userinfo


from ..config import settings
from api.core.config import settings
from api.db.session import get_db

logger = logging.getLogger(__name__)

db_uri = settings.REDISDB_URL
issuer_url = settings.CONTROLLER_URL

if urlparse(issuer_url).scheme != "https":
logger.error("CONTROLLER_URL is not HTTPS. changing openid-config for development")
issuer_url = issuer_url[:4] + "s" + issuer_url[4:]
Expand Down Expand Up @@ -51,25 +50,21 @@

subject_id_factory = HashBasedSubjectIdentifierFactory(settings.SUBJECT_ID_HASH_SALT)

kc_client = {
"enabled": True,
"client_id": settings.KEYCLOAK_CLIENT_ID,
"client_name": settings.KEYCLOAK_CLIENT_NAME,
"allowed_grant_types": ["implicit", "code"],
"allowed_scopes": ["openid", "profile", "vc_authn"],
"response_types": ["code", "id_token", "token"],
"redirect_uris": [settings.KEYCLOAK_REDIRECT_URI],
"require_consent": False,
"token_endpoint_auth_method": "client_secret_basic",
"require_client_secret": True,
"client_secret": settings.KEYCLOAK_CLIENT_SECRET,
}
# placeholder that gets set on app_start and write operations to ClientConfigurationCRUD
provider = None


async def init_provider():
global provider
from api.clientConfigurations.crud import ClientConfigurationCRUD

all_kc_configs = await ClientConfigurationCRUD(await get_db()).get_all()
client_configs = {d.client_name: d.dict() for d in all_kc_configs}

print(kc_client)
provider = Provider(
signing_key,
configuration_information,
AuthorizationState(subject_id_factory),
{"keycloak": kc_client},
Userinfo({"Jason": {"sub": "Jason"}}),
)
provider = Provider(
signing_key,
configuration_information,
AuthorizationState(subject_id_factory),
client_configs,
Userinfo({"Jason": {"sub": "Jason"}}),
)
1 change: 1 addition & 0 deletions oidc-controller/api/db/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
class COLLECTION_NAMES(str, Enum):
VER_CONFIGS = "verification_configuration"
AUTH_SESSION = "auth_session"
CLIENT_CONFiGURATIONS = "client_configuration"
4 changes: 4 additions & 0 deletions oidc-controller/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

from .routers import acapy_handler, oidc, presentation_request, well_known_oid_config
from .verificationConfigs.router import router as ver_configs_router
from .clientConfigurations.router import router as client_config_router
from .db.session import init_db
from api.core.oidc.provider import init_provider

# setup loggers
# TODO: set config via env parameters...
Expand All @@ -35,6 +37,7 @@ def get_application() -> FastAPI:

app = get_application()
app.include_router(ver_configs_router, prefix="/ver_configs", tags=["ver_configs"])
app.include_router(client_config_router, prefix="/clients", tags=["oidc_clients"])
app.include_router(well_known_oid_config.router, tags=[".well-known"])
app.include_router(
oidc.router, tags=["OpenID Connect Provider"], include_in_schema=False
Expand Down Expand Up @@ -64,6 +67,7 @@ def get_application() -> FastAPI:
async def on_tenant_startup():
"""Register any events we need to respond to."""
await init_db()
await init_provider()
logger.warning(">>> Starting up app ...")


Expand Down
2 changes: 0 additions & 2 deletions oidc-controller/api/routers/oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,10 @@ async def get_authorize(request: Request, db: Database = Depends(get_db)):
model = AuthorizationRequest().from_dict(request.query_params._dict)
model.verify()

# pyop provider
auth_req = provider.provider.parse_authentication_request(
urlencode(request.query_params._dict), request.headers
)
authn_response = provider.provider.authorize(model, "Jason")
# pyop provider END

# retrieve presentation_request config.
client = AcapyClient()
Expand Down