List of resources for OIDC providers
This commit is contained in:
parent
f14d8d3114
commit
54345dcafd
6 changed files with 74 additions and 32 deletions
|
@ -19,18 +19,22 @@ logger = logging.getLogger(__name__)
|
|||
OIDC_PROVIDERS = set([provider.id for provider in settings.oidc.providers])
|
||||
|
||||
|
||||
def get_oidc_provider(request: Request) -> StarletteOAuth2App:
|
||||
def get_oidc_provider_or_none(request: Request) -> StarletteOAuth2App | None:
|
||||
"""Return the oidc_provider from a request object, from the session.
|
||||
It can be used in Depends()"""
|
||||
if (oidc_provider_id := request.session.get("oidc_provider_id")) is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
"Not logged in (no provider in session)",
|
||||
)
|
||||
return
|
||||
try:
|
||||
return getattr(authlib_oauth, str(oidc_provider_id))
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
|
||||
def get_oidc_provider(request: Request) -> StarletteOAuth2App:
|
||||
if (oidc_provider := get_oidc_provider_or_none(request)) is None:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No such provider")
|
||||
else:
|
||||
return oidc_provider
|
||||
|
||||
|
||||
async def get_current_user(request: Request) -> User:
|
||||
|
|
|
@ -21,10 +21,11 @@ from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|||
from authlib.oauth2.rfc6749 import OAuth2Token
|
||||
from pkce import generate_code_verifier, generate_pkce_pair
|
||||
|
||||
from .settings import settings
|
||||
from .settings import settings, OIDCProvider
|
||||
from .models import User
|
||||
from .auth_utils import (
|
||||
get_oidc_provider,
|
||||
get_oidc_provider_or_none,
|
||||
hasrole,
|
||||
get_current_user_or_none,
|
||||
get_current_user,
|
||||
|
@ -55,8 +56,8 @@ app.add_middleware(
|
|||
|
||||
# Add oidc providers to authlib from the settings
|
||||
|
||||
fastapi_providers = {}
|
||||
_providers = {}
|
||||
# fastapi_providers: dict[str, OpenIdConnect] = {}
|
||||
providers_settings: dict[str, OIDCProvider] = {}
|
||||
|
||||
for provider in settings.oidc.providers:
|
||||
authlib_oauth.register(
|
||||
|
@ -74,17 +75,27 @@ for provider in settings.oidc.providers:
|
|||
# update_token=update_token,
|
||||
# client_id="some-client-id", # if enabled, authlib will also check that the access token belongs to this client id (audience)
|
||||
)
|
||||
fastapi_providers[provider.id] = OpenIdConnect(
|
||||
openIdConnectUrl=provider.openid_configuration
|
||||
)
|
||||
_providers[provider.id] = provider
|
||||
# fastapi_providers[provider.id] = OpenIdConnect(
|
||||
# openIdConnectUrl=provider.openid_configuration
|
||||
# )
|
||||
providers_settings[provider.id] = provider
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def home(
|
||||
request: Request, user: Annotated[User, Depends(get_current_user_or_none)]
|
||||
request: Request,
|
||||
user: Annotated[User, Depends(get_current_user_or_none)],
|
||||
oidc_provider: Annotated[
|
||||
StarletteOAuth2App | None, Depends(get_oidc_provider_or_none)
|
||||
],
|
||||
) -> HTMLResponse:
|
||||
now = datetime.now()
|
||||
if oidc_provider and (
|
||||
(provider := providers_settings.get(oidc_provider.name)) is not None
|
||||
):
|
||||
resources = provider.resources
|
||||
else:
|
||||
resources = []
|
||||
return templates.TemplateResponse(
|
||||
name="home.html",
|
||||
request=request,
|
||||
|
@ -92,6 +103,7 @@ async def home(
|
|||
"settings": settings.model_dump(),
|
||||
"user": user,
|
||||
"now": now,
|
||||
"resources": resources,
|
||||
"user_info_details": (
|
||||
pretty_details(user, now)
|
||||
if user and settings.oidc.show_session_details
|
||||
|
@ -112,11 +124,13 @@ async def login(request: Request, oidc_provider_id: str) -> RedirectResponse:
|
|||
"""
|
||||
redirect_uri = request.url_for("auth", oidc_provider_id=oidc_provider_id)
|
||||
try:
|
||||
provider_: StarletteOAuth2App = getattr(authlib_oauth, oidc_provider_id)
|
||||
provider: StarletteOAuth2App = getattr(authlib_oauth, oidc_provider_id)
|
||||
except AttributeError:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No such provider")
|
||||
if (
|
||||
code_challenge_method := _providers[oidc_provider_id].code_challenge_method
|
||||
code_challenge_method := providers_settings[
|
||||
oidc_provider_id
|
||||
].code_challenge_method
|
||||
) is not None:
|
||||
client = AsyncOAuth2Client(..., code_challenge_method=code_challenge_method)
|
||||
code_verifier = generate_code_verifier()
|
||||
|
@ -124,7 +138,7 @@ async def login(request: Request, oidc_provider_id: str) -> RedirectResponse:
|
|||
else:
|
||||
code_verifier = None
|
||||
try:
|
||||
response = await provider_.authorize_redirect(
|
||||
response = await provider.authorize_redirect(
|
||||
request,
|
||||
redirect_uri,
|
||||
access_type="offline",
|
||||
|
@ -238,26 +252,37 @@ async def non_compliant_logout(
|
|||
# Route for OAuth resource server
|
||||
|
||||
|
||||
@app.get("/resource/{name}")
|
||||
@app.get("/resource/{id}")
|
||||
async def get_resource(
|
||||
name: str,
|
||||
id: str,
|
||||
request: Request,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
oidc_provider: Annotated[StarletteOAuth2App, Depends(get_oidc_provider)],
|
||||
token: Annotated[OAuth2Token, Depends(get_token)],
|
||||
) -> HTMLResponse:
|
||||
) -> JSONResponse:
|
||||
"""Generic path for testing a resource provided by a provider"""
|
||||
provider = _providers[oidc_provider.name]
|
||||
if oidc_provider is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_406_NOT_ACCEPTABLE, detail="No such oidc provider"
|
||||
)
|
||||
if (provider := providers_settings.get(oidc_provider.name)) is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_406_NOT_ACCEPTABLE, detail="No oidc provider setting"
|
||||
)
|
||||
try:
|
||||
resource = next(x for x in provider.resources if x.id == id)
|
||||
except StopIteration:
|
||||
raise HTTPException(
|
||||
status.HTTP_406_NOT_ACCEPTABLE, detail="No such resource for this provider"
|
||||
)
|
||||
if (
|
||||
response := await oidc_provider.get(
|
||||
"/api/v1/user/repos",
|
||||
resource.url,
|
||||
# headers={"Authorization": f"token {token['access_token']}"},
|
||||
token=token,
|
||||
)
|
||||
).is_success:
|
||||
repos = response.json()
|
||||
names = [repo["name"] for repo in repos]
|
||||
return HTMLResponse(f"{user.name} has {len(repos)} repos: {', '.join(names)}")
|
||||
return JSONResponse(response.json())
|
||||
else:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ from pydantic_settings import (
|
|||
class Resource(BaseModel):
|
||||
"""A resource with an URL that can be accessed with an OAuth2 access token"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
url: str
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ hr {
|
|||
.user-info a.logout:hover {
|
||||
background-color: orange;
|
||||
}
|
||||
debug-auth {
|
||||
.debug-auth {
|
||||
font-size: 90%;
|
||||
background-color: #d8bebc75;
|
||||
padding: 6px;
|
||||
|
@ -152,14 +152,14 @@ debug-auth {
|
|||
font-weight: bold;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.content #links-to-check {
|
||||
.content .links-to-check {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
flex-flow: wrap;
|
||||
}
|
||||
.content #links-to-check a {
|
||||
.content .links-to-check a {
|
||||
color: black;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -11,7 +11,9 @@ function checkHref(elem) {
|
|||
xmlHttp.send(null)
|
||||
}
|
||||
|
||||
function checkPerms(rootId) {
|
||||
var rootElem = document.getElementById(rootId)
|
||||
Array.from(rootElem.children).forEach(elem => checkHref(elem))
|
||||
function checkPerms(className) {
|
||||
var rootElems = document.getElementsByClassName(className)
|
||||
Array.from(rootElems).forEach(elem =>
|
||||
Array.from(elem.children).forEach(elem => checkHref(elem))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<p>
|
||||
These links should get different response codes depending on the authorization:
|
||||
</p>
|
||||
<div id="links-to-check">
|
||||
<div class="links-to-check">
|
||||
<a href="public">Public</a>
|
||||
<a href="protected">Auth protected content</a>
|
||||
<a href="protected-by-foorole">Auth + foorole protected content</a>
|
||||
|
@ -62,6 +62,16 @@
|
|||
<a href="oauth2-forgejo-test">OAuth2 test (forgejo user info)</a>
|
||||
<a href="introspect">Introspect token (401 expected)</a>
|
||||
</div>
|
||||
{% if resources %}
|
||||
<p>
|
||||
Resources for this provider:
|
||||
</p>
|
||||
<div class="links-to-check">
|
||||
{% for resource in resources %}
|
||||
<a href="{{ request.url_for("get_resource", id=resource.id) }}">{{ resource.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user_info_details %}
|
||||
<hr>
|
||||
<div class="debug-auth">
|
||||
|
@ -74,7 +84,7 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div>Now is: {{ now }}</div>
|
||||
<div>Now is: {{ now.strftime("%T, %D") }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue