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

[bugfix] Add credentials authorization #18

Merged
merged 8 commits into from
Oct 26, 2023
23 changes: 23 additions & 0 deletions py_ocpi/core/authentication/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,31 @@ async def authenticate(cls, auth_token: str) -> None:
if auth_token not in list_token_c:
raise AuthorizationOCPIError

@classmethod
async def authenticate_credentials(
cls,
auth_token: str,
) -> str | dict | None:
"""Authenticate given auth token where both tokens valid."""
if auth_token:
list_token_a = await cls.get_valid_token_a()
if auth_token in list_token_a:
return {}

list_token_c = await cls.get_valid_token_c()
if auth_token in list_token_c:
return auth_token

return None

@classmethod
@abstractmethod
async def get_valid_token_c(cls) -> List[str]:
"""Return valid token c list."""
pass

@classmethod
@abstractmethod
async def get_valid_token_a(cls) -> List[str]:
"""Return valid token a list."""
pass
62 changes: 59 additions & 3 deletions py_ocpi/core/authentication/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,62 @@ async def __call__(
try:
token = authorization.split()[1]
if self.version.startswith("2.2"):
token = decode_string_base64(token)
try:
token = decode_string_base64(token)
except UnicodeDecodeError:
raise AuthorizationOCPIError
await authenticator.authenticate(token)
except IndexError:
raise AuthorizationOCPIError


class CredentialsAuthorizationVerifier:
"""
A class responsible for verifying authorization tokens
based on the specified version number.

:param version (VersionNumber): OCPI version used.
"""

def __init__(self, version: VersionNumber | None) -> None:
self.version = version

async def __call__(
self,
authorization: str = Header(...),
authenticator: Authenticator = Depends(get_authenticator),
) -> str | dict | None:
"""
Verifies the authorization token using the specified version
and an Authenticator.

:param authorization (str): The authorization header containing
the token.
:param authenticator (Authenticator): An Authenticator instance used
for authentication.

:raises AuthorizationOCPIError: If there is an issue with
the authorization token.
"""
try:
token = authorization.split()[1]
except IndexError:
raise AuthorizationOCPIError

if self.version:
if self.version.startswith("2.2"):
try:
token = decode_string_base64(token)
except UnicodeDecodeError:
raise AuthorizationOCPIError
else:
try:
token = decode_string_base64(token)
except UnicodeDecodeError:
pass
return await authenticator.authenticate_credentials(token)


class HttpPushVerifier:
"""
A class responsible for verifying authorization tokens if using push.
Expand Down Expand Up @@ -72,7 +122,10 @@ async def __call__(
try:
token = authorization.split()[1]
if version.value.startswith("2.2"):
token = decode_string_base64(token)
try:
token = decode_string_base64(token)
except UnicodeDecodeError:
raise AuthorizationOCPIError
await authenticator.authenticate(token)
except IndexError:
raise AuthorizationOCPIError
Expand Down Expand Up @@ -107,7 +160,10 @@ async def __call__(
raise AuthorizationOCPIError

if version.value.startswith("2.2"):
token = decode_string_base64(token)
try:
token = decode_string_base64(token)
except UnicodeDecodeError:
raise AuthorizationOCPIError
await authenticator.authenticate(token)
except AuthorizationOCPIError:
raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
39 changes: 20 additions & 19 deletions py_ocpi/modules/credentials/v_2_1_1/api/cpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@

from py_ocpi.core import status
from py_ocpi.core.adapter import Adapter
from py_ocpi.core.authentication.verifier import (
AuthorizationVerifier,
CredentialsAuthorizationVerifier,
)
from py_ocpi.core.crud import Crud
from py_ocpi.core.dependencies import get_crud, get_adapter
from py_ocpi.core.enums import Action, ModuleID, RoleEnum
from py_ocpi.core.enums import ModuleID, RoleEnum
from py_ocpi.core.schemas import OCPIResponse
from py_ocpi.core.utils import get_auth_token

Expand All @@ -22,9 +26,14 @@
router = APIRouter(
prefix="/credentials",
)
cred_dependency = CredentialsAuthorizationVerifier(VersionNumber.v_2_1_1)


@router.get("/", response_model=OCPIResponse)
@router.get(
"/",
response_model=OCPIResponse,
dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))],
)
async def get_credentials(
request: Request,
crud: Crud = Depends(get_crud),
Expand All @@ -51,18 +60,11 @@ async def post_credentials(
credentials: Credentials,
crud: Crud = Depends(get_crud),
adapter: Adapter = Depends(get_adapter),
server_cred: str | dict | None = Depends(cred_dependency),
):
auth_token = get_auth_token(request, VersionNumber.v_2_1_1)

# Check if the client is already registered
credentials_client_token = credentials.token
server_cred = await crud.do(
ModuleID.credentials_and_registration,
RoleEnum.cpo,
Action.get_client_token,
auth_token=auth_token,
version=VersionNumber.v_2_1_1,
)
if server_cred:
raise HTTPException(
fastapistatus.HTTP_405_METHOD_NOT_ALLOWED,
Expand All @@ -76,6 +78,7 @@ async def post_credentials(

# Retrieve the versions and endpoints from the client
async with httpx.AsyncClient() as client:
credentials_client_token = credentials.token
authorization_token = f"Token {credentials_client_token}"
response_versions = await client.get(
credentials.url, headers={"authorization": authorization_token}
Expand Down Expand Up @@ -129,18 +132,11 @@ async def update_credentials(
credentials: Credentials,
crud: Crud = Depends(get_crud),
adapter: Adapter = Depends(get_adapter),
server_cred: str | dict | None = Depends(cred_dependency),
):
auth_token = get_auth_token(request, VersionNumber.v_2_1_1)

# Check if the client is already registered
credentials_client_token = credentials.token
server_cred = await crud.do(
ModuleID.credentials_and_registration,
RoleEnum.cpo,
Action.get_client_token,
auth_token=auth_token,
version=VersionNumber.v_2_1_1,
)
if not server_cred:
raise HTTPException(
fastapistatus.HTTP_405_METHOD_NOT_ALLOWED,
Expand All @@ -149,6 +145,7 @@ async def update_credentials(

# Retrieve the versions and endpoints from the client
async with httpx.AsyncClient() as client:
credentials_client_token = credentials.token
authorization_token = f"Token {credentials_client_token}"
response_versions = await client.get(
credentials.url, headers={"authorization": authorization_token}
Expand Down Expand Up @@ -198,7 +195,11 @@ async def update_credentials(
)


@router.delete("/", response_model=OCPIResponse)
@router.delete(
"/",
response_model=OCPIResponse,
dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))],
)
async def remove_credentials(
request: Request,
crud: Crud = Depends(get_crud),
Expand Down
39 changes: 20 additions & 19 deletions py_ocpi/modules/credentials/v_2_1_1/api/emsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@

from py_ocpi.core import status
from py_ocpi.core.adapter import Adapter
from py_ocpi.core.authentication.verifier import (
AuthorizationVerifier,
CredentialsAuthorizationVerifier,
)
from py_ocpi.core.crud import Crud
from py_ocpi.core.dependencies import get_crud, get_adapter
from py_ocpi.core.enums import Action, ModuleID, RoleEnum
from py_ocpi.core.enums import ModuleID, RoleEnum
from py_ocpi.core.schemas import OCPIResponse
from py_ocpi.core.utils import get_auth_token

Expand All @@ -22,9 +26,14 @@
router = APIRouter(
prefix="/credentials",
)
cred_dependency = CredentialsAuthorizationVerifier(VersionNumber.v_2_1_1)


@router.get("/", response_model=OCPIResponse)
@router.get(
"/",
response_model=OCPIResponse,
dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))],
)
async def get_credentials(
request: Request,
crud: Crud = Depends(get_crud),
Expand All @@ -51,18 +60,11 @@ async def post_credentials(
credentials: Credentials,
crud: Crud = Depends(get_crud),
adapter: Adapter = Depends(get_adapter),
server_cred: str | dict | None = Depends(cred_dependency),
):
auth_token = get_auth_token(request, VersionNumber.v_2_1_1)

# Check if the client is already registered
credentials_client_token = credentials.token
server_cred = await crud.do(
ModuleID.credentials_and_registration,
RoleEnum.emsp,
Action.get_client_token,
auth_token=auth_token,
version=VersionNumber.v_2_1_1,
)
if server_cred:
raise HTTPException(
fastapistatus.HTTP_405_METHOD_NOT_ALLOWED,
Expand All @@ -76,6 +78,7 @@ async def post_credentials(

# Retrieve the versions and endpoints from the client
async with httpx.AsyncClient() as client:
credentials_client_token = credentials.token
authorization_token = f"Token {credentials_client_token}"
response_versions = await client.get(
credentials.url, headers={"authorization": authorization_token}
Expand Down Expand Up @@ -129,18 +132,11 @@ async def update_credentials(
credentials: Credentials,
crud: Crud = Depends(get_crud),
adapter: Adapter = Depends(get_adapter),
server_cred: str | dict | None = Depends(cred_dependency),
):
auth_token = get_auth_token(request, VersionNumber.v_2_1_1)

# Check if the client is already registered
credentials_client_token = credentials.token
server_cred = await crud.do(
ModuleID.credentials_and_registration,
RoleEnum.emsp,
Action.get_client_token,
auth_token=auth_token,
version=VersionNumber.v_2_1_1,
)
if not server_cred:
raise HTTPException(
fastapistatus.HTTP_405_METHOD_NOT_ALLOWED,
Expand All @@ -149,6 +145,7 @@ async def update_credentials(

# Retrieve the versions and endpoints from the client
async with httpx.AsyncClient() as client:
credentials_client_token = credentials.token
authorization_token = f"Token {credentials_client_token}"
response_versions = await client.get(
credentials.url, headers={"authorization": authorization_token}
Expand Down Expand Up @@ -198,7 +195,11 @@ async def update_credentials(
)


@router.delete("/", response_model=OCPIResponse)
@router.delete(
"/",
response_model=OCPIResponse,
dependencies=[Depends(AuthorizationVerifier(VersionNumber.v_2_1_1))],
)
async def remove_credentials(
request: Request,
crud: Crud = Depends(get_crud),
Expand Down
Loading