Skip to content

Commit 9ab27e7

Browse files
committed
🐛 Generic OIDC authentication
1 parent b803c28 commit 9ab27e7

File tree

5 files changed

+60
-45
lines changed

5 files changed

+60
-45
lines changed

backend/trip/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.6.0"
1+
__version__ = "1.7.0"

backend/trip/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class Settings(BaseSettings):
1818
REFRESH_TOKEN_EXPIRE_MINUTES: int = 1440
1919

2020
REGISTER_ENABLE: bool = True
21+
OIDC_DISCOVERY_URL: str = ""
2122
OIDC_PROTOCOL: str = "https"
2223
OIDC_CLIENT_ID: str = ""
2324
OIDC_CLIENT_SECRET: str = ""
2425
OIDC_HOST: str = ""
25-
OIDC_REALM: str = "master"
2626
OIDC_REDIRECT_URI: str = ""
2727

2828
class Config:

backend/trip/deps.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from typing import Annotated
22

33
import jwt
4-
from authlib.integrations.httpx_client import OAuth2Client
54
from fastapi import Depends, HTTPException
65
from fastapi.security import OAuth2PasswordBearer
76
from sqlmodel import Session
@@ -35,12 +34,3 @@ def get_current_username(token: Annotated[str, Depends(oauth_password_scheme)],
3534
if not user:
3635
raise HTTPException(status_code=401, detail="Invalid Token")
3736
return user.username
38-
39-
40-
def get_oidc_client():
41-
return OAuth2Client(
42-
client_id=settings.OIDC_CLIENT_ID,
43-
client_secret=settings.OIDC_CLIENT_SECRET,
44-
scope="openid",
45-
redirect_uri=settings.OIDC_REDIRECT_URI,
46-
)

backend/trip/routers/auth.py

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import json
2-
31
import jwt
42
from fastapi import APIRouter, Body, HTTPException
5-
from jwt.algorithms import RSAAlgorithm
63

74
from ..config import settings
85
from ..db.core import init_user_data
9-
from ..deps import SessionDep, get_oidc_client
6+
from ..deps import SessionDep
107
from ..models.models import AuthParams, LoginRegisterModel, Token, User
11-
from ..security import (create_access_token, create_tokens, hash_password,
12-
verify_password)
13-
from ..utils.utils import generate_filename, httpx_get
8+
from ..security import (create_access_token, create_tokens, get_oidc_client,
9+
get_oidc_config, hash_password, verify_password)
10+
from ..utils.utils import generate_filename
1411

1512
router = APIRouter(prefix="/api/auth", tags=["auth"])
1613

@@ -20,21 +17,26 @@ async def auth_params() -> AuthParams:
2017
data = {"oidc": None, "register_enabled": settings.REGISTER_ENABLE}
2118

2219
if settings.OIDC_HOST and settings.OIDC_CLIENT_ID and settings.OIDC_CLIENT_SECRET:
23-
oidc_complete_url = f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}/protocol/openid-connect/auth?client_id={settings.OIDC_CLIENT_ID}&redirect_uri={settings.OIDC_REDIRECT_URI}&response_type=code&scope=openid"
24-
data["oidc"] = oidc_complete_url
20+
oidc_config = await get_oidc_config()
21+
auth_endpoint = oidc_config.get("authorization_endpoint")
22+
data["oidc"] = (
23+
f"{auth_endpoint}?client_id={settings.OIDC_CLIENT_ID}&redirect_uri={settings.OIDC_REDIRECT_URI}&response_type=code&scope=openid"
24+
)
2525

2626
return data
2727

2828

2929
@router.post("/oidc/login", response_model=Token)
3030
async def oidc_login(session: SessionDep, code: str = Body(..., embed=True)) -> Token:
31-
if settings.AUTH_METHOD != "oidc":
32-
raise HTTPException(status_code=400, detail="Bad request")
31+
if not (settings.OIDC_HOST or settings.OIDC_CLIENT_ID or settings.OIDC_CLIENT_SECRET):
32+
raise HTTPException(status_code=400, detail="Partial OIDC config")
3333

34+
oidc_config = await get_oidc_config()
35+
token_endpoint = oidc_config.get("token_endpoint")
3436
try:
3537
oidc_client = get_oidc_client()
3638
token = oidc_client.fetch_token(
37-
f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}/protocol/openid-connect/token",
39+
token_endpoint,
3840
grant_type="authorization_code",
3941
code=code,
4042
)
@@ -49,37 +51,35 @@ async def oidc_login(session: SessionDep, code: str = Body(..., embed=True)) ->
4951
decoded = jwt.decode(
5052
id_token,
5153
settings.OIDC_CLIENT_SECRET,
52-
algorithms=alg,
54+
algorithms=["HS256"],
5355
audience=settings.OIDC_CLIENT_ID,
5456
)
5557
case "RS256":
56-
config = await httpx_get(
57-
f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}/.well-known/openid-configuration"
58-
)
59-
jwks_uri = config.get("jwks_uri")
60-
jwks = await httpx_get(jwks_uri)
61-
keys = jwks.get("keys")
62-
63-
for key in keys:
64-
try:
65-
pk = RSAAlgorithm.from_jwk(json.dumps(key))
66-
decoded = jwt.decode(
67-
id_token,
68-
key=pk,
69-
algorithms=alg,
70-
audience=settings.OIDC_CLIENT_ID,
71-
issuer=f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}",
72-
)
73-
break
74-
except Exception:
75-
continue
58+
jwks_uri = oidc_config.get("jwks_uri")
59+
issuer = oidc_config.get("issuer")
60+
jwks_client = jwt.PyJWKClient(jwks_uri)
61+
62+
try:
63+
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
64+
decoded = jwt.decode(
65+
id_token,
66+
key=signing_key.key,
67+
algorithms=["RS256"],
68+
audience=settings.OIDC_CLIENT_ID,
69+
issuer=issuer,
70+
)
71+
except Exception:
72+
raise HTTPException(status_code=401, detail="Invalid ID token")
7673
case _:
7774
raise HTTPException(status_code=500, detail="OIDC login failed, algorithm not handled")
7875

7976
if not decoded:
8077
raise HTTPException(status_code=401, detail="Invalid ID token")
8178

8279
username = decoded.get("preferred_username")
80+
if not username:
81+
raise HTTPException(status_code=401, detail="OIDC login failed, preferred_username missing")
82+
8383
user = session.get(User, username)
8484
if not user:
8585
# TODO: password is non-null, we must init the pw with something, the model is not made for OIDC

backend/trip/security.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import jwt
44
from argon2 import PasswordHasher
55
from argon2 import exceptions as argon_exceptions
6+
from authlib.integrations.httpx_client import OAuth2Client
67
from fastapi import HTTPException
78

89
from .config import settings
910
from .models.models import Token
11+
from .utils.utils import httpx_get
1012

1113
ph = PasswordHasher()
14+
OIDC_CONFIG = {}
1215

1316

1417
def hash_password(password: str) -> str:
@@ -52,3 +55,25 @@ def verify_exists_and_owns(username: str, obj) -> None:
5255
raise PermissionError
5356

5457
return None
58+
59+
60+
def get_oidc_client():
61+
return OAuth2Client(
62+
client_id=settings.OIDC_CLIENT_ID,
63+
client_secret=settings.OIDC_CLIENT_SECRET,
64+
scope="openid",
65+
redirect_uri=settings.OIDC_REDIRECT_URI,
66+
)
67+
68+
69+
async def get_oidc_config():
70+
global OIDC_CONFIG
71+
if OIDC_CONFIG:
72+
return OIDC_CONFIG
73+
74+
discovery_url = f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/.well-known/openid-configuration"
75+
if settings.OIDC_DISCOVERY_URL:
76+
discovery_url = settings.OIDC_DISCOVERY_URL
77+
78+
OIDC_CONFIG = await httpx_get(discovery_url)
79+
return OIDC_CONFIG

0 commit comments

Comments
 (0)