Migrate all resources to json contents;
improve token decoding & logging error messages
This commit is contained in:
parent
d39adf41ef
commit
3eb6dc3dcf
6 changed files with 77 additions and 87 deletions
|
@ -5,7 +5,7 @@ import logging
|
|||
from fastapi import HTTPException, Request, Depends, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from authlib.integrations.starlette_client import OAuth, OAuthError, StarletteOAuth2App
|
||||
from jwt import ExpiredSignatureError, InvalidKeyError
|
||||
from jwt import ExpiredSignatureError, InvalidKeyError, DecodeError
|
||||
from httpx import AsyncClient
|
||||
|
||||
# from authlib.oauth1.auth import OAuthToken
|
||||
|
@ -147,8 +147,8 @@ async def get_token(request: Request) -> OAuth2Token:
|
|||
oidc_provider_settings,
|
||||
request.session.get("sid"),
|
||||
)
|
||||
except (TokenNotInDb, InvalidKeyError):
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No token")
|
||||
except (TokenNotInDb, InvalidKeyError, DecodeError) as err:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, err.__class__.__name__)
|
||||
|
||||
|
||||
async def get_current_user_or_none(request: Request) -> User | None:
|
||||
|
@ -208,9 +208,14 @@ async def get_user_from_token(
|
|||
try:
|
||||
auth_provider_settings = oidc_providers_settings[auth_provider_id]
|
||||
except KeyError:
|
||||
raise HTTPException(
|
||||
status.HTTP_401_UNAUTHORIZED, f"Unknown auth provider '{auth_provider_id}'"
|
||||
)
|
||||
if auth_provider_id == "":
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No auth provider")
|
||||
else:
|
||||
raise HTTPException(
|
||||
status.HTTP_401_UNAUTHORIZED, f"Unknown auth provider '{auth_provider_id}'"
|
||||
)
|
||||
if token == "":
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No token")
|
||||
try:
|
||||
payload = auth_provider_settings.decode(token)
|
||||
except ExpiredSignatureError as err:
|
||||
|
|
|
@ -4,8 +4,7 @@ import logging
|
|||
|
||||
from httpx import AsyncClient
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, status
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi import FastAPI, HTTPException, Depends, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
# from starlette.middleware.sessions import SessionMiddleware
|
||||
|
@ -16,8 +15,8 @@ from .models import User
|
|||
from .auth_utils import (
|
||||
get_user_from_token,
|
||||
UserWithRole,
|
||||
get_oidc_provider,
|
||||
get_token,
|
||||
# get_oidc_provider,
|
||||
# get_token,
|
||||
)
|
||||
from .settings import settings
|
||||
|
||||
|
@ -47,44 +46,46 @@ resource_server.add_middleware(
|
|||
|
||||
|
||||
@resource_server.get("/public")
|
||||
async def public() -> HTMLResponse:
|
||||
return HTMLResponse("<h1>Not protected</h1>")
|
||||
async def public() -> dict:
|
||||
return {"msg": "Not protected"}
|
||||
|
||||
|
||||
@resource_server.get("/protected")
|
||||
async def get_protected(user: Annotated[User, Depends(get_user_from_token)]) -> HTMLResponse:
|
||||
async def get_protected(user: Annotated[User, Depends(get_user_from_token)]):
|
||||
assert user is not None # Just to keep QA checks happy
|
||||
return HTMLResponse("<h1>Only authenticated users can see this</h1>")
|
||||
return {"msg": "Only authenticated users can see this"}
|
||||
|
||||
|
||||
@resource_server.get("/protected-by-foorole")
|
||||
async def get_protected_by_foorole(
|
||||
user: Annotated[User, Depends(UserWithRole("foorole"))]
|
||||
) -> HTMLResponse:
|
||||
return HTMLResponse("<h1>Only users with foorole can see this</h1>")
|
||||
user: Annotated[User, Depends(UserWithRole("foorole"))],
|
||||
):
|
||||
assert user is not None
|
||||
return {"msg": "Only users with foorole can see this"}
|
||||
|
||||
|
||||
@resource_server.get("/protected-by-barrole")
|
||||
async def get_protected_by_barrole(
|
||||
user: Annotated[User, Depends(UserWithRole("barrole"))]
|
||||
) -> HTMLResponse:
|
||||
return HTMLResponse("<h1>Protected by barrole</h1>")
|
||||
user: Annotated[User, Depends(UserWithRole("barrole"))],
|
||||
):
|
||||
assert user is not None
|
||||
return {"msg": "Protected by barrole"}
|
||||
|
||||
|
||||
@resource_server.get("/protected-by-foorole-and-barrole")
|
||||
async def get_protected_by_foorole_and_barrole(
|
||||
user: Annotated[User, Depends(UserWithRole("foorole")), Depends(UserWithRole("barrole"))],
|
||||
) -> HTMLResponse:
|
||||
):
|
||||
assert user is not None # Just to keep QA checks happy
|
||||
return HTMLResponse("<h1>Only users with foorole and barrole can see this</h1>")
|
||||
return {"msg": "Only users with foorole and barrole can see this"}
|
||||
|
||||
|
||||
@resource_server.get("/protected-by-foorole-or-barrole")
|
||||
async def get_protected_by_foorole_or_barrole(
|
||||
user: Annotated[User, Depends(UserWithRole(["foorole", "barrole"]))]
|
||||
) -> HTMLResponse:
|
||||
user: Annotated[User, Depends(UserWithRole(["foorole", "barrole"]))],
|
||||
):
|
||||
assert user is not None # Just to keep QA checks happy
|
||||
return HTMLResponse("<h1>Only users with foorole or barrole can see this</h1>")
|
||||
return {"msg": "Only users with foorole or barrole can see this"}
|
||||
|
||||
|
||||
# @resource_server.get("/introspect")
|
||||
|
@ -118,9 +119,9 @@ async def get_resource_(
|
|||
# oidc_provider: Annotated[StarletteOAuth2App, Depends(get_oidc_provider)],
|
||||
# token: Annotated[OAuth2Token, Depends(get_token)],
|
||||
user: Annotated[User, Depends(get_user_from_token)],
|
||||
) -> JSONResponse:
|
||||
):
|
||||
"""Generic path for testing a resource provided by a provider"""
|
||||
return JSONResponse(await get_resource(id, user))
|
||||
return await get_resource(id, user)
|
||||
|
||||
|
||||
async def get_resource(resource_id: str, user: User) -> dict:
|
||||
|
|
|
@ -43,9 +43,7 @@ class OIDCProvider(BaseModel):
|
|||
info_url: str | None = (
|
||||
None # Used eg. for Keycloak's public key (see https://stackoverflow.com/questions/54318633/getting-keycloaks-public-key)
|
||||
)
|
||||
info: dict[str, str | int] | None = (
|
||||
None # Info fetched from info_url, eg. public key
|
||||
)
|
||||
info: dict[str, str | int] | None = None # Info fetched from info_url, eg. public key
|
||||
public_key: str | None = None
|
||||
signature_alg: str = "RS256"
|
||||
resource_provider_scopes: list[str] = []
|
||||
|
@ -62,25 +60,17 @@ class OIDCProvider(BaseModel):
|
|||
|
||||
def get_account_url(self, request: Request, user: User) -> str | None:
|
||||
if self.account_url_template:
|
||||
if not (
|
||||
self.url.endswith("/") or self.account_url_template.startswith("/")
|
||||
):
|
||||
if not (self.url.endswith("/") or self.account_url_template.startswith("/")):
|
||||
sep = "/"
|
||||
else:
|
||||
sep = ""
|
||||
return (
|
||||
self.url
|
||||
+ sep
|
||||
+ self.account_url_template.format(request=request, user=user)
|
||||
)
|
||||
return self.url + sep + self.account_url_template.format(request=request, user=user)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_public_key(self) -> str:
|
||||
"""Return the public key formatted for decoding token"""
|
||||
public_key = self.public_key or (
|
||||
self.info is not None and self.info["public_key"]
|
||||
)
|
||||
public_key = self.public_key or (self.info is not None and self.info["public_key"])
|
||||
if public_key is None:
|
||||
raise AttributeError(f"Cannot get public key for {self.name}")
|
||||
return f"""
|
||||
|
@ -91,17 +81,18 @@ class OIDCProvider(BaseModel):
|
|||
|
||||
def decode(self, token: str, verify_signature: bool = True) -> dict[str, Any]:
|
||||
"""Decode the token with signature check"""
|
||||
decoded = decode(
|
||||
token,
|
||||
self.get_public_key(),
|
||||
algorithms=[self.signature_alg],
|
||||
audience=["account", "oidc-test", "oidc-test-web"],
|
||||
options={
|
||||
"verify_signature": False,
|
||||
"verify_aud": False,
|
||||
}, # not settings.insecure.skip_verify_signature},
|
||||
)
|
||||
logger.debug(str(decoded))
|
||||
if settings.debug_token:
|
||||
decoded = decode(
|
||||
token,
|
||||
self.get_public_key(),
|
||||
algorithms=[self.signature_alg],
|
||||
audience=["account", "oidc-test", "oidc-test-web"],
|
||||
options={
|
||||
"verify_signature": False,
|
||||
"verify_aud": False,
|
||||
}, # not settings.insecure.skip_verify_signature},
|
||||
)
|
||||
logger.debug(str(decoded))
|
||||
return decode(
|
||||
token,
|
||||
self.get_public_key(),
|
||||
|
@ -143,6 +134,7 @@ class Settings(BaseSettings):
|
|||
log: bool = False
|
||||
insecure: Insecure = Insecure()
|
||||
cors_origins: list[str] = []
|
||||
debug_token: bool = False
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
|
@ -161,9 +153,7 @@ class Settings(BaseSettings):
|
|||
settings_cls,
|
||||
Path(
|
||||
Path(
|
||||
environ.get(
|
||||
"OIDC_TEST_SETTINGS_FILE", Path.cwd() / "settings.yaml"
|
||||
),
|
||||
environ.get("OIDC_TEST_SETTINGS_FILE", Path.cwd() / "settings.yaml"),
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
|
@ -171,11 +171,13 @@ hr {
|
|||
gap: 0.5em;
|
||||
flex-flow: wrap;
|
||||
}
|
||||
.content .links-to-check a {
|
||||
.content .links-to-check button {
|
||||
color: black;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.token {
|
||||
|
@ -183,12 +185,6 @@ hr {
|
|||
font-family: monospace;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.resourceResult {
|
||||
padding: 0.5em;
|
||||
gap: 0.5em;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
async function checkHref(elem, token, authProvider) {
|
||||
const msg = document.getElementById("msg")
|
||||
const resp = await fetch(elem.href, {
|
||||
const url = `resource/${elem.getAttribute("resource-id")}`
|
||||
const resp = await fetch(url, {
|
||||
headers: new Headers({
|
||||
"Content-type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
|
|
|
@ -61,33 +61,30 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<hr>
|
||||
{% if user %}
|
||||
<p class="center">
|
||||
Fetch resources from the resource server with your authentication token:
|
||||
</p>
|
||||
<div class="actions">
|
||||
<button onclick="get_resource('time', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Time</button>
|
||||
<button onclick="get_resource('bs', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">BS</button>
|
||||
</div>
|
||||
<div class="resourceResult">
|
||||
<div id="resource" class="resource"></div>
|
||||
<div id="msg" class="msg error"></div>
|
||||
</div>
|
||||
<hr>
|
||||
{% endif %}
|
||||
<div class="content">
|
||||
<p>
|
||||
These links should get different response codes depending on the authorization:
|
||||
<p class="center">
|
||||
Resources validated by scope:
|
||||
</p>
|
||||
<div class="links-to-check">
|
||||
<a href="resource/public">Public</a>
|
||||
<a href="resource/protected">Auth protected content</a>
|
||||
<a href="resource/protected-by-foorole">Auth + foorole protected content</a>
|
||||
<a href="resource/protected-by-foorole-or-barrole">Auth + foorole or barrole protected content</a>
|
||||
<a href="resource/protected-by-barrole">Auth + barrole protected content</a>
|
||||
<a href="resource/protected-by-foorole-and-barrole">Auth + foorole and barrole protected content</a>
|
||||
<a href="resource/fast_api_depends" class="hidden">Using FastAPI Depends</a>
|
||||
<!--<a href="resource/introspect">Introspect token (401 expected)</a>-->
|
||||
<button resource-id="time" onclick="get_resource('time', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Time</button>
|
||||
<button resource-id="bs" onclick="get_resource('bs', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">BS</button>
|
||||
</div>
|
||||
<p>
|
||||
Resources validated by role:
|
||||
</p>
|
||||
<div class="links-to-check">
|
||||
<button resource-id="public" onclick="get_resource('public', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Public</button>
|
||||
<button resource-id="protected" onclick="get_resource('protected', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Auth protected content</button>
|
||||
<button resource-id="protected-by-foorole" onclick="get_resource('protected-by-foorole', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Auth + foorole protected content</button>
|
||||
<button resource-id="protected-by-foorole-or-barrole" onclick="get_resource('protected-by-foorole-or-barrole', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Auth + foorole or barrole protected content</button>
|
||||
<button resource-id="protected-by-barrole" onclick="get_resource('protected-by-barrole', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Auth + barrole protected content</button>
|
||||
<button resource-id="protected-by-foorole-and-barrole" onclick="get_resource('protected-by-foorole-and-barrole', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Auth + foorole and barrole protected content</button>
|
||||
<button resource-id="fast_api_depends" class="hidden" onclick="get_resource('fast_api_depends', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Using FastAPI Depends</button>
|
||||
<!--<button resource-id="introspect" onclick="get_resource('introspect', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Introspect token (401 expected)</button>-->
|
||||
</div>
|
||||
<div class="resourceResult">
|
||||
<div id="resource" class="resource"></div>
|
||||
<div id="msg" class="msg error"></div>
|
||||
</div>
|
||||
{% if resources %}
|
||||
<p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue