Skip to content

Commit 4f08d5a

Browse files
committed
feat: sso profiles
1 parent fc75cb4 commit 4f08d5a

File tree

3 files changed

+102
-18
lines changed

3 files changed

+102
-18
lines changed

backend/api/auth/sso.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from urllib.parse import urlparse
22

3-
from authlib.integrations.starlette_client import OAuthError
43
from fastapi import Request
54
from fastapi.responses import RedirectResponse
65

@@ -39,14 +38,34 @@ class SSOLoginCallback(APIRequest):
3938
methods = ["GET"]
4039

4140
async def handle(self, request: Request, provider: str) -> RedirectResponse:
42-
client = NebulaSSO.client(provider)
41+
remote = NebulaSSO.client(provider)
42+
if not remote:
43+
return RedirectResponse("/?error=Invalid provider")
4344

44-
try:
45-
token = await client.authorize_access_token(request)
46-
except OAuthError as error:
47-
return RedirectResponse(f"/?error={error}")
48-
user = token.get("userinfo", {})
49-
email = user.get("email")
45+
code = request.query_params.get("code")
46+
id_token = request.query_params.get("id_token")
47+
oauth_verifier = request.query_params.get("oauth_verifier")
48+
49+
user_info = {}
50+
51+
if code:
52+
token = await remote.authorize_access_token(request)
53+
user_info = token.get("userinfo", {})
54+
55+
if id_token and not user_info:
56+
token = {"id_token": id_token}
57+
user_info = await remote.parse_id_token(request, token)
58+
59+
if oauth_verifier and not user_info:
60+
token = await remote.authorize_access_token(request)
61+
62+
if token and not user_info:
63+
user_info = await remote.userinfo(token=token)
64+
65+
if not user_info:
66+
return RedirectResponse("/?error=Invalid response from provider")
67+
68+
email = user_info.get("email")
5069

5170
if not email:
5271
return RedirectResponse("/?error=User email not found")

backend/nebula/settings/models.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,55 @@ class BaseSystemSettings(SettingsModel):
9494

9595

9696
class SSOProvider(SettingsModel):
97-
name: str
98-
title: str | None = None
99-
entrypoint: str
100-
client_id: str
101-
client_secret: str
97+
name: Annotated[
98+
str,
99+
Field(
100+
title="Name",
101+
example="myoauth",
102+
),
103+
]
104+
105+
title: Annotated[
106+
str,
107+
Field(
108+
title="Title",
109+
description="Used on the SSO button on the login page",
110+
example="Log in using MyOauth",
111+
),
112+
]
113+
114+
profile: Annotated[
115+
Literal["google", "github"] | None,
116+
Field(
117+
title="Profile",
118+
description="Configuration preset if entrypoint is not provided",
119+
),
120+
] = None
121+
122+
entrypoint: Annotated[
123+
str | None,
124+
Field(
125+
title="Entrypoint",
126+
description="URL to the SSO provider configuration endpoint",
127+
example="https://iam.example.com/realms/nebula/.well-known/openid-configuration",
128+
),
129+
] = None
130+
131+
client_id: Annotated[
132+
str,
133+
Field(
134+
title="Client ID",
135+
example="myclientid",
136+
),
137+
]
138+
139+
client_secret: Annotated[
140+
str,
141+
Field(
142+
title="Client secret",
143+
example="myclientsecret",
144+
),
145+
]
102146

103147

104148
class SystemSettings(BaseSystemSettings):

backend/server/sso.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1+
from typing import Any
2+
13
from authlib.integrations.httpx_client import AsyncOAuth2Client
24
from authlib.integrations.starlette_client import OAuth
35

46
import nebula
57
from server.models import ResponseModel
68

9+
PROFILES = {
10+
"github": {
11+
"api_base_url": "https://api.github.com/",
12+
"access_token_url": "https://github.com/login/oauth/access_token",
13+
"authorize_url": "https://github.com/login/oauth/authorize",
14+
"client_kwargs": {"scope": "user:email"},
15+
"userinfo_endpoint": "https://api.github.com/user",
16+
},
17+
"google": {
18+
"api_base_url": "https://www.googleapis.com/",
19+
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
20+
"client_kwargs": {"scope": "openid email profile"},
21+
},
22+
}
23+
724

825
class SSOOption(ResponseModel):
926
name: str
@@ -37,11 +54,15 @@ def create_oauth(cls) -> OAuth:
3754
cls._oauth = OAuth(ssoconfig)
3855
assert cls._oauth is not None, "OAuth is not initialized"
3956
for provider in nebula.settings.system.sso_providers:
40-
cls._oauth.register(
41-
name=provider.name,
42-
server_metadata_url=provider.entrypoint,
43-
client_kwargs={"scope": "openid email profile"},
44-
)
57+
kw: dict[str, Any]
58+
if provider.profile:
59+
kw = PROFILES[provider.profile]
60+
else:
61+
kw = {
62+
"server_metadata_url": provider.entrypoint,
63+
"client_kwargs": {"scope": "openid email profile"},
64+
}
65+
cls._oauth.register(name=provider.name, **kw)
4566
return cls._oauth
4667

4768
@classmethod

0 commit comments

Comments
 (0)