Skip to content
Open
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
60 changes: 41 additions & 19 deletions app/core/auth/endpoints_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@
get_token_data,
get_user_from_token_with_scopes,
)
from app.types.exceptions import AuthHTTPException
from app.types.exceptions import AuthHTTPException, ObjectExpectedInDbNotFoundError
from app.types.module import CoreModule
from app.types.scopes_type import ScopeType
from app.utils.auth import auth_utils
from app.utils.auth.providers import BaseAuthClient
from app.utils.tools import has_user_permission

Expand All @@ -61,7 +62,6 @@
# WARNING: if new flow are added, openid_config should be updated accordingly


# TODO: maybe remove
@router.post(
"/auth/simple_token",
response_model=schemas_auth.AccessToken,
Expand All @@ -71,6 +71,7 @@
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(get_settings),
request_id: str = Depends(get_request_id),
):
"""
Ask for a JWT access token using oauth password flow.
Expand All @@ -79,8 +80,12 @@

Note: the request body needs to use **form-data** and not json.
"""
user = await authenticate_user(db, form_data.username, form_data.password)
email = form_data.username
user = await authenticate_user(db, email, form_data.password)
if not user:
hyperion_access_logger.warning(
f"Authorize-validation: Invalid user email or password for email {email} ({request_id})",
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect login or password",
Expand All @@ -90,7 +95,10 @@
# The subject `sub` is a JWT registered claim name, see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
data = schemas_auth.TokenData(sub=user.id, scopes=ScopeType.auth)
access_token = create_access_token(settings=settings, data=data)
return {"access_token": access_token, "token_type": "bearer"}
return schemas_auth.AccessToken(
access_token=access_token,
token_type="bearer", # noqa: S106
)


# Authorization Code Grant #
Expand Down Expand Up @@ -221,8 +229,7 @@


* parameters that allows to authenticate the user and know which scopes he grants access to.
* `email`
* `password`
* `auth_access_token`: a JWT granting the scope *auth*

References:
* https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2
Expand Down Expand Up @@ -300,20 +307,35 @@
url += "&state=" + authorizereq.state
return RedirectResponse(url, status_code=status.HTTP_302_FOUND)

# TODO: Currently if the user enters the wrong credentials in the form, they won't be redirected to the login page again but the OAuth process will fail.
user = await authenticate_user(db, authorizereq.email, authorizereq.password)
if not user:
hyperion_access_logger.warning(
f"Authorize-validation: Invalid user email or password for email {authorizereq.email} ({request_id})",
# We check the validity of the jwt token
try:
token_data = auth_utils.get_token_data(
settings=settings,
token=authorizereq.auth_access_token,
request_id=request_id,
)
return RedirectResponse(
settings.CLIENT_URL
+ calypsso.get_login_relative_url(
**authorizereq.model_dump(exclude={"email", "password"}),
credentials_error=True,
),
status_code=status.HTTP_302_FOUND,
except HTTPException as e:
# OAuth specifications requires to redirect to the `redirect_uri` with an error parameter if the request is invalid
# instead of returning a 4xx error code
hyperion_access_logger.warning(
f"Authorize-validation: Invalid auth_access_token {e.status_code}: {e.detail} ({request_id})",
)
url = redirect_uri + "?error=" + e.detail
if authorizereq.state:
url += "&state=" + authorizereq.state
return RedirectResponse(url, status_code=status.HTTP_302_FOUND)

# The token should have the scope *auth* in order to be able to use this endpoint
user_id = auth_utils.get_user_id_from_token_with_scopes(
scopes=[[ScopeType.auth]],
token_data=token_data,
)
user = await cruds_users.get_user_by_id(
db=db,
user_id=user_id,
)
if not user:
raise ObjectExpectedInDbNotFoundError("user", user_id)

# The auth_client may restrict the usage of the client to specific Hyperion permissions
if auth_client.permission is not None:
Expand All @@ -325,7 +347,7 @@
)
):
hyperion_access_logger.warning(
f"Authorize-validation: user is not member of an allowed group {authorizereq.email} ({request_id})",
f"Authorize-validation: user is not member of an allowed group {user.email} ({request_id})",
)
return RedirectResponse(
settings.CLIENT_URL
Expand Down
20 changes: 5 additions & 15 deletions app/core/auth/schemas_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
from typing import Literal

from fastapi import Form
from pydantic import BaseModel, field_validator
from pydantic import BaseModel

from app.utils import validators
from app.utils.examples import examples_auth


Expand Down Expand Up @@ -34,8 +33,7 @@ class AuthorizeValidation(Authorize):
```
"""

email: str
password: str
auth_access_token: str

# If we don't add these parameters
# the heritage from Authorize does not allow Mypy to infer the str | None
Expand All @@ -46,12 +44,6 @@ class AuthorizeValidation(Authorize):
code_challenge: str | None = None
code_challenge_method: str | None = None

# Email normalization, this will modify the email variable
# https://pydantic-docs.helpmanual.io/usage/validators/#reuse-validators
_normalize_email = field_validator(
"email",
)(validators.email_normalizer)

class config:
schema_extra = {"example": examples_auth.example_AuthorizeValidation}

Expand All @@ -66,8 +58,7 @@ def as_form(
nonce: str | None = Form(None),
code_challenge: str | None = Form(None),
code_challenge_method: str | None = Form(None),
email: str = Form(...),
password: str = Form(...),
auth_access_token: str = Form(...),
):
if nonce == "None":
nonce = None
Expand All @@ -81,14 +72,13 @@ def as_form(
nonce=nonce,
code_challenge=code_challenge,
code_challenge_method=code_challenge_method,
email=email,
password=password,
auth_access_token=auth_access_token,
)


class AccessToken(BaseModel):
access_token: str
token_type: str
token_type: Literal["bearer"]


class TokenData(BaseModel):
Expand Down
10 changes: 7 additions & 3 deletions app/modules/sport_competition/endpoints_sport_competition.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from io import BytesIO
from uuid import UUID, uuid4

from fastapi import Body, Depends, HTTPException, Query, Response
from fastapi import Depends, HTTPException, Query, Response
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession

Expand Down Expand Up @@ -271,12 +271,12 @@ async def activate_edition(
status_code=204,
)
async def enable_inscription(
enable_inscription: schemas_sport_competition.CompetitionEnableInscription,
edition_id: UUID,
db: AsyncSession = Depends(get_db),
user: models_users.CoreUser = Depends(
is_user_allowed_to([SportCompetitionPermissions.manage_sport_competition]),
),
enable: bool = Body(),
) -> None:
"""
Enable inscription for a competition edition.
Expand All @@ -293,7 +293,11 @@ async def enable_inscription(
status_code=400,
detail="Edition is not active, cannot patch inscription",
)
await cruds_sport_competition.patch_edition_inscription(edition_id, enable, db)
await cruds_sport_competition.patch_edition_inscription(
edition_id,
enable_inscription.enable,
db,
)


@module.router.patch(
Expand Down
4 changes: 4 additions & 0 deletions app/modules/sport_competition/schemas_sport_competition.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class CompetitionEditionEdit(BaseModel):
end_date: datetime | None = None


class CompetitionEnableInscription(BaseModel):
enable: bool


class SchoolExtensionBase(BaseModel):
school_id: UUID
from_lyon: bool
Expand Down
3 changes: 1 addition & 2 deletions app/utils/examples/examples_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"state": "azerty",
"code_challenge": "c2cf464b7901205c037cd821bc493b191943bdb5244a665e9fcab6478bf79415", # hashlib.sha256("AntoineMonBelAntoine".encode()).hexdigest()
"code_challenge_method": "S256",
"email": "email@myecl.fr",
"password": "azerty",
"auth_access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30",
}

example_TokenReq_access_token = {
Expand Down
Loading
Loading