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