Add self resouce provider
This commit is contained in:
parent
dc181bd3a8
commit
af49242192
7 changed files with 77 additions and 29 deletions
|
@ -4,13 +4,12 @@ import logging
|
||||||
|
|
||||||
from fastapi import HTTPException, Request, Depends, status
|
from fastapi import HTTPException, Request, Depends, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from authlib.oauth2.rfc6749 import OAuth2Token
|
|
||||||
from authlib.integrations.starlette_client import OAuth, OAuthError, StarletteOAuth2App
|
from authlib.integrations.starlette_client import OAuth, OAuthError, StarletteOAuth2App
|
||||||
from jwt import ExpiredSignatureError, InvalidKeyError
|
from jwt import ExpiredSignatureError, InvalidKeyError
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
# from authlib.oauth1.auth import OAuthToken
|
# from authlib.oauth1.auth import OAuthToken
|
||||||
# from authlib.oauth2.auth import OAuth2Token
|
from authlib.oauth2.auth import OAuth2Token
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
from .database import TokenNotInDb, db, UserNotInDB
|
from .database import TokenNotInDb, db, UserNotInDB
|
||||||
|
@ -21,7 +20,6 @@ logger = logging.getLogger(__name__)
|
||||||
oidc_providers_settings: dict[str, OIDCProvider] = dict(
|
oidc_providers_settings: dict[str, OIDCProvider] = dict(
|
||||||
[(provider.id, provider) for provider in settings.oidc.providers]
|
[(provider.id, provider) for provider in settings.oidc.providers]
|
||||||
)
|
)
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,9 +35,19 @@ async def fetch_token(name, request):
|
||||||
# return token.to_token()
|
# return token.to_token()
|
||||||
|
|
||||||
|
|
||||||
async def update_token(*args, **kwargs):
|
async def update_token(name, token, refresh_token=None, access_token=None):
|
||||||
logger.warn("TODO: update_token")
|
breakpoint()
|
||||||
...
|
if refresh_token:
|
||||||
|
item = OAuth2Token.find(name=name, refresh_token=refresh_token)
|
||||||
|
elif access_token:
|
||||||
|
item = OAuth2Token.find(name=name, access_token=access_token)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
# update old token
|
||||||
|
item.access_token = token["access_token"]
|
||||||
|
item.refresh_token = token.get("refresh_token")
|
||||||
|
item.expires_at = token["expires_at"]
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
authlib_oauth = OAuth(cache=None, fetch_token=fetch_token, update_token=update_token)
|
authlib_oauth = OAuth(cache=None, fetch_token=fetch_token, update_token=update_token)
|
||||||
|
@ -52,7 +60,10 @@ def init_providers():
|
||||||
name=id,
|
name=id,
|
||||||
server_metadata_url=provider.openid_configuration,
|
server_metadata_url=provider.openid_configuration,
|
||||||
client_kwargs={
|
client_kwargs={
|
||||||
"scope": "openid email offline_access profile",
|
"scope": " ".join(
|
||||||
|
["openid", "email", "offline_access", "profile"]
|
||||||
|
+ provider.resource_provider_scopes
|
||||||
|
),
|
||||||
},
|
},
|
||||||
client_id=provider.client_id,
|
client_id=provider.client_id,
|
||||||
client_secret=provider.client_secret,
|
client_secret=provider.client_secret,
|
||||||
|
|
|
@ -102,6 +102,7 @@ async def home(
|
||||||
context={
|
context={
|
||||||
"settings": settings.model_dump(),
|
"settings": settings.model_dump(),
|
||||||
"user": user,
|
"user": user,
|
||||||
|
"access_token_scope": user.access_token_parsed()["scope"] if user else None,
|
||||||
"now": now,
|
"now": now,
|
||||||
"oidc_provider": oidc_provider,
|
"oidc_provider": oidc_provider,
|
||||||
"oidc_provider_settings": oidc_provider_settings,
|
"oidc_provider_settings": oidc_provider_settings,
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
@ -22,15 +23,20 @@ async def get_resource(resource_id: str, user: User) -> dict:
|
||||||
# but this has to be refined for production
|
# but this has to be refined for production
|
||||||
required_scope = f"get:{resource_id}"
|
required_scope = f"get:{resource_id}"
|
||||||
# Check if the required scope is in the scopes allowed in userinfo
|
# Check if the required scope is in the scopes allowed in userinfo
|
||||||
if user.has_scope(required_scope):
|
try:
|
||||||
await process(user, resource_id, resp)
|
if user.has_scope(required_scope):
|
||||||
else:
|
await process(user, resource_id, resp)
|
||||||
## For the showcase, giving a explanation.
|
else:
|
||||||
## Alternatively, raise HTTP_401_UNAUTHORIZED
|
## For the showcase, giving a explanation.
|
||||||
resp["sorry"] = (
|
## Alternatively, raise HTTP_401_UNAUTHORIZED
|
||||||
f"No scope {required_scope} in the access token "
|
resp["sorry"] = (
|
||||||
+ "but it is required for accessing this resource."
|
f"No scope {required_scope} in the access token "
|
||||||
)
|
+ "but it is required for accessing this resource."
|
||||||
|
)
|
||||||
|
except ExpiredSignatureError:
|
||||||
|
resp["sorry"] = "The token's signature has expired"
|
||||||
|
except InvalidTokenError:
|
||||||
|
resp["sorry"] = "The token is invalid"
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ class OIDCProvider(BaseModel):
|
||||||
)
|
)
|
||||||
public_key: str | None = None
|
public_key: str | None = None
|
||||||
signature_alg: str = "RS256"
|
signature_alg: str = "RS256"
|
||||||
|
resource_provider_scopes: list[str] = []
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -170,3 +170,23 @@ hr {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource {
|
||||||
|
padding: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
flex-direction: column;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
margin: 5px auto;
|
||||||
|
box-shadow: 0px 0px 10px #90c3eeA0;
|
||||||
|
background-color: #90c3eeA0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resources {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
|
@ -36,5 +36,21 @@ async function get_resource(id, token, authProvider) {
|
||||||
err => msg.value = err
|
err => msg.value = err
|
||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
console.log(await resp.json())
|
const resource = await resp.json()
|
||||||
|
const rootElem = document.getElementById('resource')
|
||||||
|
rootElem.innerHTML = ""
|
||||||
|
Object.entries(resource).forEach(
|
||||||
|
([k, v]) => {
|
||||||
|
let r = document.createElement('div')
|
||||||
|
let kElem = document.createElement('div')
|
||||||
|
kElem.innerText = k
|
||||||
|
kElem.className = "key"
|
||||||
|
let vElem = document.createElement('div')
|
||||||
|
vElem.innerText = v
|
||||||
|
vElem.className = "value"
|
||||||
|
r.appendChild(kElem)
|
||||||
|
r.appendChild(vElem)
|
||||||
|
rootElem.appendChild(r)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,10 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.access_token.scope %}
|
{% if access_token_scope %}
|
||||||
<div>
|
<div>
|
||||||
<span>Scopes</span>:
|
<span>Scopes</span>:
|
||||||
{% for scope in user.access_token.scope.split(' ') %}
|
{% for scope in access_token_scope.split(' ') %}
|
||||||
<span class="scope">{{ scope }}</span>
|
<span class="scope">{{ scope }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,6 +61,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
|
{% if user %}
|
||||||
<p class="center">
|
<p class="center">
|
||||||
Fetch resources from the resource server with your authentication token:
|
Fetch resources from the resource server with your authentication token:
|
||||||
</p>
|
</p>
|
||||||
|
@ -68,17 +69,9 @@
|
||||||
<button onclick="get_resource('time', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">Time</button>
|
<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>
|
<button onclick="get_resource('bs', '{{ user.access_token }}', '{{ oidc_provider_settings.id }}')">BS</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="resources">
|
<div id="resource" class="resource"></div>
|
||||||
<div v-if="Object.entries(resource).length > 0" class="resource">
|
|
||||||
<div v-for="(value, key) in resource">
|
|
||||||
<div class="key">{{ key }}</div>
|
|
||||||
<div v-if="key == 'sorry' || key == 'error'" class="error">{{ value }}</div>
|
|
||||||
<div v-else class="value">{{ value }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="msg" class="msg resource">{{ msg }}</div>
|
|
||||||
<hr>
|
<hr>
|
||||||
|
{% endif %}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p>
|
||||||
These links should get different response codes depending on the authorization:
|
These links should get different response codes depending on the authorization:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue