2020-09-17 10:01:21 +00:00
|
|
|
import datetime
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
from authlib.integrations.flask_oauth2 import AuthorizationServer
|
|
|
|
from authlib.integrations.flask_oauth2 import ResourceProtector
|
2023-09-18 21:01:30 +00:00
|
|
|
from authlib.jose import JsonWebKey
|
2022-10-06 11:32:41 +00:00
|
|
|
from authlib.oauth2.rfc6749.grants import (
|
|
|
|
AuthorizationCodeGrant as _AuthorizationCodeGrant,
|
|
|
|
)
|
|
|
|
from authlib.oauth2.rfc6749.grants import ClientCredentialsGrant
|
|
|
|
from authlib.oauth2.rfc6749.grants import ImplicitGrant
|
|
|
|
from authlib.oauth2.rfc6749.grants import RefreshTokenGrant as _RefreshTokenGrant
|
|
|
|
from authlib.oauth2.rfc6749.grants import (
|
|
|
|
ResourceOwnerPasswordCredentialsGrant as _ResourceOwnerPasswordCredentialsGrant,
|
|
|
|
)
|
|
|
|
from authlib.oauth2.rfc6750 import BearerTokenValidator as _BearerTokenValidator
|
|
|
|
from authlib.oauth2.rfc7009 import RevocationEndpoint as _RevocationEndpoint
|
2022-10-19 19:57:25 +00:00
|
|
|
from authlib.oauth2.rfc7591 import (
|
|
|
|
ClientRegistrationEndpoint as _ClientRegistrationEndpoint,
|
|
|
|
)
|
2022-10-24 15:18:46 +00:00
|
|
|
from authlib.oauth2.rfc7592 import (
|
|
|
|
ClientConfigurationEndpoint as _ClientConfigurationEndpoint,
|
|
|
|
)
|
2022-10-06 11:32:41 +00:00
|
|
|
from authlib.oauth2.rfc7636 import CodeChallenge as _CodeChallenge
|
|
|
|
from authlib.oauth2.rfc7662 import IntrospectionEndpoint as _IntrospectionEndpoint
|
|
|
|
from authlib.oidc.core import UserInfo
|
|
|
|
from authlib.oidc.core.grants import OpenIDCode as _OpenIDCode
|
|
|
|
from authlib.oidc.core.grants import OpenIDHybridGrant as _OpenIDHybridGrant
|
|
|
|
from authlib.oidc.core.grants import OpenIDImplicitGrant as _OpenIDImplicitGrant
|
|
|
|
from authlib.oidc.core.grants.util import generate_id_token
|
2021-12-20 22:57:27 +00:00
|
|
|
from flask import current_app
|
2023-12-25 23:23:47 +00:00
|
|
|
from flask import g
|
2022-11-17 16:44:54 +00:00
|
|
|
from flask import request
|
2023-12-25 23:23:47 +00:00
|
|
|
from flask import url_for
|
2022-10-06 11:32:41 +00:00
|
|
|
from werkzeug.security import gen_salt
|
|
|
|
|
2024-03-15 18:58:06 +00:00
|
|
|
from canaille.app import models
|
2024-04-16 20:42:29 +00:00
|
|
|
from canaille.backends import Backend
|
2023-11-24 08:26:15 +00:00
|
|
|
|
2023-01-13 19:26:35 +00:00
|
|
|
AUTHORIZATION_CODE_LIFETIME = 84400
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
|
2023-12-25 23:23:47 +00:00
|
|
|
def oauth_authorization_server():
|
|
|
|
return {
|
|
|
|
"issuer": get_issuer(),
|
|
|
|
"authorization_endpoint": url_for("oidc.endpoints.authorize", _external=True),
|
|
|
|
"token_endpoint": url_for("oidc.endpoints.issue_token", _external=True),
|
|
|
|
"token_endpoint_auth_methods_supported": [
|
|
|
|
"client_secret_basic",
|
|
|
|
"private_key_jwt",
|
|
|
|
"client_secret_post",
|
|
|
|
"none",
|
|
|
|
],
|
|
|
|
"token_endpoint_auth_signing_alg_values_supported": ["RS256", "ES256"],
|
|
|
|
"userinfo_endpoint": url_for("oidc.endpoints.userinfo", _external=True),
|
|
|
|
"introspection_endpoint": url_for(
|
|
|
|
"oidc.endpoints.introspect_token", _external=True
|
|
|
|
),
|
|
|
|
"jwks_uri": url_for("oidc.endpoints.jwks", _external=True),
|
|
|
|
"registration_endpoint": url_for(
|
|
|
|
"oidc.endpoints.client_registration", _external=True
|
|
|
|
),
|
|
|
|
"scopes_supported": [
|
|
|
|
"openid",
|
|
|
|
"profile",
|
|
|
|
"email",
|
|
|
|
"address",
|
|
|
|
"phone",
|
|
|
|
"groups",
|
|
|
|
],
|
|
|
|
"response_types_supported": [
|
|
|
|
"code",
|
|
|
|
"token",
|
|
|
|
"id_token",
|
|
|
|
"code token",
|
|
|
|
"code id_token",
|
|
|
|
"token id_token",
|
|
|
|
],
|
|
|
|
"ui_locales_supported": g.available_language_codes,
|
|
|
|
"code_challenge_methods_supported": ["plain", "S256"],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def openid_configuration():
|
|
|
|
return {
|
|
|
|
**oauth_authorization_server(),
|
|
|
|
"end_session_endpoint": url_for("oidc.endpoints.end_session", _external=True),
|
|
|
|
"claims_supported": [
|
|
|
|
"sub",
|
|
|
|
"iss",
|
|
|
|
"auth_time",
|
|
|
|
"acr",
|
|
|
|
"name",
|
|
|
|
"given_name",
|
|
|
|
"family_name",
|
|
|
|
"nickname",
|
|
|
|
"profile",
|
|
|
|
"picture",
|
|
|
|
"website",
|
|
|
|
"email",
|
|
|
|
"email_verified",
|
|
|
|
"locale",
|
|
|
|
"zoneinfo",
|
|
|
|
"groups",
|
|
|
|
"nonce",
|
|
|
|
],
|
|
|
|
"subject_types_supported": ["pairwise", "public"],
|
|
|
|
"id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
|
|
|
|
"prompt_values_supported": ["none"]
|
2023-12-18 17:06:03 +00:00
|
|
|
+ (["create"] if current_app.config["CANAILLE"]["ENABLE_REGISTRATION"] else []),
|
2023-12-25 23:23:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def exists_nonce(nonce, req):
|
2024-04-16 20:42:29 +00:00
|
|
|
client = Backend.instance.get(models.Client, id=req.client_id)
|
|
|
|
exists = Backend.instance.query(
|
2024-04-10 13:44:11 +00:00
|
|
|
models.AuthorizationCode, client=client, nonce=nonce
|
|
|
|
)
|
2022-10-06 11:32:41 +00:00
|
|
|
return bool(exists)
|
2021-09-28 10:06:41 +00:00
|
|
|
|
2020-10-26 18:09:38 +00:00
|
|
|
|
2022-11-17 16:44:54 +00:00
|
|
|
def get_issuer():
|
2023-12-18 17:06:03 +00:00
|
|
|
if current_app.config["CANAILLE_OIDC"]["JWT"]["ISS"]:
|
|
|
|
return current_app.config["CANAILLE_OIDC"]["JWT"]["ISS"]
|
2022-11-17 16:44:54 +00:00
|
|
|
|
|
|
|
if current_app.config.get("SERVER_NAME"):
|
|
|
|
return current_app.config.get("SERVER_NAME")
|
|
|
|
|
|
|
|
return request.url_root
|
|
|
|
|
|
|
|
|
2023-09-18 21:01:30 +00:00
|
|
|
def get_jwt_config(grant=None):
|
2023-07-01 16:46:11 +00:00
|
|
|
return {
|
2023-12-18 17:06:03 +00:00
|
|
|
"key": current_app.config["CANAILLE_OIDC"]["JWT"]["PRIVATE_KEY"],
|
|
|
|
"alg": current_app.config["CANAILLE_OIDC"]["JWT"]["ALG"],
|
2023-07-01 16:46:11 +00:00
|
|
|
"iss": get_issuer(),
|
2023-12-18 17:06:03 +00:00
|
|
|
"exp": current_app.config["CANAILLE_OIDC"]["JWT"]["EXP"],
|
2023-07-01 16:46:11 +00:00
|
|
|
}
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
|
2023-09-18 21:01:30 +00:00
|
|
|
def get_jwks():
|
2023-12-18 17:06:03 +00:00
|
|
|
kty = current_app.config["CANAILLE_OIDC"]["JWT"]["KTY"]
|
|
|
|
alg = current_app.config["CANAILLE_OIDC"]["JWT"]["ALG"]
|
2023-09-18 21:01:30 +00:00
|
|
|
jwk = JsonWebKey.import_key(
|
2023-12-18 17:06:03 +00:00
|
|
|
current_app.config["CANAILLE_OIDC"]["JWT"]["PUBLIC_KEY"], {"kty": kty}
|
2023-09-18 21:01:30 +00:00
|
|
|
)
|
|
|
|
return {
|
|
|
|
"keys": [
|
|
|
|
{
|
|
|
|
"use": "sig",
|
|
|
|
"alg": alg,
|
|
|
|
**jwk,
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-12-24 00:44:16 +00:00
|
|
|
def claims_from_scope(scope):
|
|
|
|
claims = {"sub"}
|
2022-10-06 11:32:41 +00:00
|
|
|
if "profile" in scope:
|
2022-12-24 00:44:16 +00:00
|
|
|
claims |= {
|
2022-10-06 11:32:41 +00:00
|
|
|
"name",
|
|
|
|
"family_name",
|
|
|
|
"given_name",
|
|
|
|
"nickname",
|
|
|
|
"preferred_username",
|
|
|
|
"profile",
|
|
|
|
"picture",
|
|
|
|
"website",
|
|
|
|
"gender",
|
|
|
|
"birthdate",
|
|
|
|
"zoneinfo",
|
|
|
|
"locale",
|
|
|
|
"updated_at",
|
2022-12-24 00:44:16 +00:00
|
|
|
}
|
2022-10-06 11:32:41 +00:00
|
|
|
if "email" in scope:
|
2022-12-24 00:44:16 +00:00
|
|
|
claims |= {"email", "email_verified"}
|
2022-10-06 11:32:41 +00:00
|
|
|
if "address" in scope:
|
2022-12-24 00:44:16 +00:00
|
|
|
claims |= {"address"}
|
2022-10-06 11:32:41 +00:00
|
|
|
if "phone" in scope:
|
2022-12-24 00:44:16 +00:00
|
|
|
claims |= {"phone_number", "phone_number_verified"}
|
2022-10-06 11:32:41 +00:00
|
|
|
if "groups" in scope:
|
2022-12-24 00:44:16 +00:00
|
|
|
claims |= {"groups"}
|
|
|
|
return claims
|
2022-10-06 11:32:41 +00:00
|
|
|
|
2022-12-24 00:44:16 +00:00
|
|
|
|
|
|
|
def generate_user_info(user, scope):
|
|
|
|
claims = claims_from_scope(scope)
|
2022-10-06 11:32:41 +00:00
|
|
|
data = generate_user_claims(user, claims)
|
|
|
|
return UserInfo(**data)
|
|
|
|
|
|
|
|
|
|
|
|
def generate_user_claims(user, claims, jwt_mapping_config=None):
|
2023-04-10 18:09:47 +00:00
|
|
|
jwt_mapping_config = {
|
2023-12-18 17:06:03 +00:00
|
|
|
**(current_app.config["CANAILLE_OIDC"]["JWT"]["MAPPING"]),
|
2023-04-10 18:09:47 +00:00
|
|
|
**(jwt_mapping_config or {}),
|
|
|
|
}
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
data = {}
|
|
|
|
for claim in claims:
|
|
|
|
raw_claim = jwt_mapping_config.get(claim.upper())
|
|
|
|
if raw_claim:
|
|
|
|
formatted_claim = current_app.jinja_env.from_string(raw_claim).render(
|
|
|
|
user=user
|
|
|
|
)
|
|
|
|
if formatted_claim:
|
|
|
|
# According to https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
|
|
|
# it's better to not insert a null or empty string value
|
|
|
|
data[claim] = formatted_claim
|
|
|
|
if claim == "groups":
|
2023-02-05 18:39:52 +00:00
|
|
|
data[claim] = [group.display_name for group in user.groups]
|
2022-10-06 11:32:41 +00:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
def save_authorization_code(code, request):
|
|
|
|
nonce = request.data.get("nonce")
|
2023-03-17 23:38:56 +00:00
|
|
|
now = datetime.datetime.now(datetime.timezone.utc)
|
2022-10-06 11:32:41 +00:00
|
|
|
scope = request.client.get_allowed_scope(request.scope)
|
2023-04-09 09:37:04 +00:00
|
|
|
code = models.AuthorizationCode(
|
2022-10-06 11:32:41 +00:00
|
|
|
authorization_code_id=gen_salt(48),
|
|
|
|
code=code,
|
|
|
|
subject=request.user,
|
2023-03-08 22:53:53 +00:00
|
|
|
client=request.client,
|
2022-10-06 11:32:41 +00:00
|
|
|
redirect_uri=request.redirect_uri or request.client.redirect_uris[0],
|
2023-11-22 15:30:38 +00:00
|
|
|
scope=scope.split(" "),
|
2022-10-06 11:32:41 +00:00
|
|
|
nonce=nonce,
|
|
|
|
issue_date=now,
|
2023-05-17 07:29:32 +00:00
|
|
|
lifetime=AUTHORIZATION_CODE_LIFETIME,
|
2022-10-06 11:32:41 +00:00
|
|
|
challenge=request.data.get("code_challenge"),
|
|
|
|
challenge_method=request.data.get("code_challenge_method"),
|
2022-07-07 14:05:34 +00:00
|
|
|
)
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.save(code)
|
2022-10-06 11:32:41 +00:00
|
|
|
return code.code
|
2020-09-17 08:00:39 +00:00
|
|
|
|
2020-08-17 15:49:49 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
class AuthorizationCodeGrant(_AuthorizationCodeGrant):
|
|
|
|
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"]
|
2020-09-17 08:00:39 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def save_authorization_code(self, code, request):
|
|
|
|
return save_authorization_code(code, request)
|
2020-08-17 15:49:49 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def query_authorization_code(self, code, client):
|
2024-04-16 20:42:29 +00:00
|
|
|
item = Backend.instance.query(
|
2024-04-10 13:44:11 +00:00
|
|
|
models.AuthorizationCode, code=code, client=client
|
|
|
|
)
|
2022-10-06 11:32:41 +00:00
|
|
|
if item and not item[0].is_expired():
|
|
|
|
return item[0]
|
2020-08-17 15:49:49 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def delete_authorization_code(self, authorization_code):
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.delete(authorization_code)
|
2020-08-17 15:49:49 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def authenticate_user(self, authorization_code):
|
2024-04-17 11:05:14 +00:00
|
|
|
if authorization_code.subject and not authorization_code.subject.locked:
|
|
|
|
return authorization_code.subject
|
2021-12-06 23:07:32 +00:00
|
|
|
|
2020-09-17 08:00:39 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
class OpenIDCode(_OpenIDCode):
|
|
|
|
def exists_nonce(self, nonce, request):
|
|
|
|
return exists_nonce(nonce, request)
|
2020-08-24 12:44:32 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def get_jwt_config(self, grant):
|
|
|
|
return get_jwt_config(grant)
|
2020-08-24 12:44:32 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def generate_user_info(self, user, scope):
|
|
|
|
return generate_user_info(user, scope)
|
2020-08-24 13:56:30 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def get_audiences(self, request):
|
|
|
|
client = request.client
|
2023-03-08 22:53:53 +00:00
|
|
|
return [aud.client_id for aud in client.audience]
|
2020-08-24 13:56:30 +00:00
|
|
|
|
2020-08-26 09:54:35 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
class PasswordGrant(_ResourceOwnerPasswordCredentialsGrant):
|
|
|
|
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"]
|
2020-08-26 09:54:35 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def authenticate_user(self, username, password):
|
2024-04-16 20:42:29 +00:00
|
|
|
user = Backend.instance.get_user_from_login(username)
|
2022-11-01 11:25:21 +00:00
|
|
|
if not user:
|
|
|
|
return None
|
2023-05-17 10:48:14 +00:00
|
|
|
|
2024-04-16 20:42:29 +00:00
|
|
|
success, _ = Backend.instance.check_user_password(user, password)
|
2022-11-01 11:25:21 +00:00
|
|
|
if not success:
|
2023-05-17 10:48:14 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
return user
|
2020-09-25 09:26:41 +00:00
|
|
|
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
class RefreshTokenGrant(_RefreshTokenGrant):
|
2023-11-18 18:20:14 +00:00
|
|
|
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"]
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def authenticate_refresh_token(self, refresh_token):
|
2024-04-16 20:42:29 +00:00
|
|
|
token = Backend.instance.query(models.Token, refresh_token=refresh_token)
|
2022-10-06 11:32:41 +00:00
|
|
|
if token and token[0].is_refresh_token_active():
|
|
|
|
return token[0]
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def authenticate_user(self, credential):
|
2024-04-17 10:07:11 +00:00
|
|
|
if credential.subject and not credential.subject.locked:
|
|
|
|
return credential.subject
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def revoke_old_credential(self, credential):
|
2023-03-17 23:38:56 +00:00
|
|
|
credential.revokation_date = datetime.datetime.now(datetime.timezone.utc)
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.save(credential)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
class OpenIDImplicitGrant(_OpenIDImplicitGrant):
|
|
|
|
def exists_nonce(self, nonce, request):
|
|
|
|
return exists_nonce(nonce, request)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def get_jwt_config(self, grant=None):
|
|
|
|
return get_jwt_config(grant)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def generate_user_info(self, user, scope):
|
|
|
|
return generate_user_info(user, scope)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def get_audiences(self, request):
|
|
|
|
client = request.client
|
2023-03-08 22:53:53 +00:00
|
|
|
return [aud.client_id for aud in client.audience]
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-07-07 14:28:28 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
class OpenIDHybridGrant(_OpenIDHybridGrant):
|
|
|
|
def save_authorization_code(self, code, request):
|
|
|
|
return save_authorization_code(code, request)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def exists_nonce(self, nonce, request):
|
|
|
|
return exists_nonce(nonce, request)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def get_jwt_config(self, grant=None):
|
|
|
|
return get_jwt_config(grant)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def generate_user_info(self, user, scope):
|
|
|
|
return generate_user_info(user, scope)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def get_audiences(self, request):
|
|
|
|
client = request.client
|
2023-03-08 22:53:53 +00:00
|
|
|
return [aud.client_id for aud in client.audience]
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def query_client(client_id):
|
2024-04-16 20:42:29 +00:00
|
|
|
return Backend.instance.get(models.Client, client_id=client_id)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def save_token(token, request):
|
2023-03-17 23:38:56 +00:00
|
|
|
now = datetime.datetime.now(datetime.timezone.utc)
|
2023-04-09 09:37:04 +00:00
|
|
|
t = models.Token(
|
2022-10-06 11:32:41 +00:00
|
|
|
token_id=gen_salt(48),
|
|
|
|
type=token["token_type"],
|
|
|
|
access_token=token["access_token"],
|
|
|
|
issue_date=now,
|
2023-01-13 19:26:35 +00:00
|
|
|
lifetime=token["expires_in"],
|
2023-11-22 15:36:42 +00:00
|
|
|
scope=token["scope"].split(" "),
|
2023-03-08 22:53:53 +00:00
|
|
|
client=request.client,
|
2022-10-06 11:32:41 +00:00
|
|
|
refresh_token=token.get("refresh_token"),
|
|
|
|
subject=request.user,
|
|
|
|
audience=request.client.audience,
|
|
|
|
)
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.save(t)
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BearerTokenValidator(_BearerTokenValidator):
|
|
|
|
def authenticate_token(self, token_string):
|
2024-04-16 20:42:29 +00:00
|
|
|
return Backend.instance.get(models.Token, access_token=token_string)
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
|
2022-12-10 20:10:18 +00:00
|
|
|
def query_token(token, token_type_hint):
|
|
|
|
if token_type_hint == "access_token":
|
2024-04-16 20:42:29 +00:00
|
|
|
return Backend.instance.get(models.Token, access_token=token)
|
2022-12-10 20:10:18 +00:00
|
|
|
elif token_type_hint == "refresh_token":
|
2024-04-16 20:42:29 +00:00
|
|
|
return Backend.instance.get(models.Token, refresh_token=token)
|
2022-12-10 20:10:18 +00:00
|
|
|
|
2024-04-16 20:42:29 +00:00
|
|
|
item = Backend.instance.get(models.Token, access_token=token)
|
2022-12-10 20:10:18 +00:00
|
|
|
if item:
|
|
|
|
return item
|
2022-10-06 11:32:41 +00:00
|
|
|
|
2024-04-16 20:42:29 +00:00
|
|
|
item = Backend.instance.get(models.Token, refresh_token=token)
|
2022-12-10 20:10:18 +00:00
|
|
|
if item:
|
|
|
|
return item
|
2022-10-06 11:32:41 +00:00
|
|
|
|
2022-12-10 20:10:18 +00:00
|
|
|
return None
|
2022-10-06 11:32:41 +00:00
|
|
|
|
2022-12-10 20:10:18 +00:00
|
|
|
|
|
|
|
class RevocationEndpoint(_RevocationEndpoint):
|
|
|
|
def query_token(self, token, token_type_hint):
|
|
|
|
return query_token(token, token_type_hint)
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
def revoke_token(self, token, request):
|
2023-03-17 23:38:56 +00:00
|
|
|
token.revokation_date = datetime.datetime.now(datetime.timezone.utc)
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.save(token)
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
class IntrospectionEndpoint(_IntrospectionEndpoint):
|
|
|
|
def query_token(self, token, token_type_hint):
|
2022-12-10 20:10:18 +00:00
|
|
|
return query_token(token, token_type_hint)
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
def check_permission(self, token, client, request):
|
2023-03-08 22:53:53 +00:00
|
|
|
return client in token.audience
|
2022-10-06 11:32:41 +00:00
|
|
|
|
|
|
|
def introspect_token(self, token):
|
2023-03-08 22:53:53 +00:00
|
|
|
audience = [aud.client_id for aud in token.audience]
|
2022-10-06 11:32:41 +00:00
|
|
|
return {
|
|
|
|
"active": True,
|
2023-03-08 22:53:53 +00:00
|
|
|
"client_id": token.client.client_id,
|
2022-10-06 11:32:41 +00:00
|
|
|
"token_type": token.type,
|
2023-11-15 17:20:13 +00:00
|
|
|
"username": token.subject.formatted_name,
|
2022-10-06 11:32:41 +00:00
|
|
|
"scope": token.get_scope(),
|
2023-11-15 17:20:13 +00:00
|
|
|
"sub": token.subject.user_name,
|
2022-10-06 11:32:41 +00:00
|
|
|
"aud": audience,
|
2022-11-17 16:44:54 +00:00
|
|
|
"iss": get_issuer(),
|
2022-10-06 11:32:41 +00:00
|
|
|
"exp": token.get_expires_at(),
|
|
|
|
"iat": token.get_issued_at(),
|
|
|
|
}
|
2022-05-20 12:07:56 +00:00
|
|
|
|
|
|
|
|
2022-10-24 15:18:46 +00:00
|
|
|
class ClientManagementMixin:
|
2022-10-19 19:57:25 +00:00
|
|
|
def authenticate_token(self, request):
|
2023-12-18 17:06:03 +00:00
|
|
|
if current_app.config["CANAILLE_OIDC"]["DYNAMIC_CLIENT_REGISTRATION_OPEN"]:
|
2022-10-19 19:57:25 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
auth_header = request.headers.get("Authorization")
|
|
|
|
if not auth_header or not auth_header.lower().startswith("bearer "):
|
|
|
|
return None
|
|
|
|
|
|
|
|
bearer_token = auth_header.split()[1]
|
2023-12-19 17:22:31 +00:00
|
|
|
if bearer_token not in (
|
2023-12-18 17:06:03 +00:00
|
|
|
current_app.config["CANAILLE_OIDC"]["DYNAMIC_CLIENT_REGISTRATION_TOKENS"]
|
2023-12-19 17:22:31 +00:00
|
|
|
or []
|
2022-10-19 19:57:25 +00:00
|
|
|
):
|
|
|
|
return None
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def get_server_metadata(self):
|
2022-12-15 22:00:52 +00:00
|
|
|
result = openid_configuration()
|
2022-10-19 19:57:25 +00:00
|
|
|
return result
|
|
|
|
|
2022-11-15 18:02:45 +00:00
|
|
|
def resolve_public_key(self, request):
|
|
|
|
# At the moment the only keypair accepted in software statement
|
|
|
|
# is the one used to isues JWTs. This might change somedays.
|
2023-12-18 17:06:03 +00:00
|
|
|
return current_app.config["CANAILLE_OIDC"]["JWT"]["PUBLIC_KEY"]
|
2022-11-15 18:02:45 +00:00
|
|
|
|
2023-11-24 08:30:52 +00:00
|
|
|
def client_convert_data(self, **kwargs):
|
|
|
|
if "client_id_issued_at" in kwargs:
|
|
|
|
kwargs["client_id_issued_at"] = datetime.datetime.fromtimestamp(
|
|
|
|
kwargs["client_id_issued_at"], datetime.timezone.utc
|
|
|
|
)
|
2022-10-19 19:57:25 +00:00
|
|
|
|
2023-11-24 08:30:52 +00:00
|
|
|
if "client_secret_expires_at" in kwargs:
|
|
|
|
kwargs["client_secret_expires_at"] = datetime.datetime.fromtimestamp(
|
|
|
|
kwargs["client_secret_expires_at"], datetime.timezone.utc
|
|
|
|
)
|
2023-11-24 08:26:15 +00:00
|
|
|
|
2023-11-24 08:30:52 +00:00
|
|
|
if "scope" in kwargs and not isinstance(kwargs["scope"], list):
|
|
|
|
kwargs["scope"] = kwargs["scope"].split(" ")
|
2023-11-24 08:26:15 +00:00
|
|
|
|
2023-11-24 08:30:52 +00:00
|
|
|
return kwargs
|
2023-11-24 08:26:15 +00:00
|
|
|
|
|
|
|
|
2022-10-24 15:18:46 +00:00
|
|
|
class ClientRegistrationEndpoint(ClientManagementMixin, _ClientRegistrationEndpoint):
|
|
|
|
software_statement_alg_values_supported = ["RS256"]
|
|
|
|
|
|
|
|
def save_client(self, client_info, client_metadata, request):
|
2023-11-24 08:30:52 +00:00
|
|
|
client = models.Client(
|
2023-12-23 20:32:31 +00:00
|
|
|
# this won't be needed when OIDC RP Initiated Logout is
|
|
|
|
# directly implemented in authlib:
|
|
|
|
# https://gitlab.com/yaal/canaille/-/issues/157
|
|
|
|
post_logout_redirect_uris=request.data.get("post_logout_redirect_uris"),
|
2023-12-26 00:13:11 +00:00
|
|
|
**self.client_convert_data(**client_info, **client_metadata),
|
2023-11-24 08:30:52 +00:00
|
|
|
)
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.save(client)
|
2023-12-23 18:37:14 +00:00
|
|
|
client.audience = [client]
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.save(client)
|
2022-10-24 15:18:46 +00:00
|
|
|
return client
|
|
|
|
|
|
|
|
|
|
|
|
class ClientConfigurationEndpoint(ClientManagementMixin, _ClientConfigurationEndpoint):
|
|
|
|
def authenticate_client(self, request):
|
|
|
|
client_id = request.uri.split("/")[-1]
|
2024-04-16 20:42:29 +00:00
|
|
|
return Backend.instance.get(models.Client, client_id=client_id)
|
2022-10-24 15:18:46 +00:00
|
|
|
|
|
|
|
def revoke_access_token(self, request, token):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def check_permission(self, client, request):
|
|
|
|
return True
|
|
|
|
|
|
|
|
def delete_client(self, client, request):
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.delete(client)
|
2022-10-24 15:18:46 +00:00
|
|
|
|
|
|
|
def update_client(self, client, client_metadata, request):
|
2024-04-16 20:42:29 +00:00
|
|
|
Backend.instance.update(client, **self.client_convert_data(**client_metadata))
|
|
|
|
Backend.instance.save(client)
|
2022-10-24 15:18:46 +00:00
|
|
|
return client
|
|
|
|
|
|
|
|
def generate_client_registration_info(self, client, request):
|
|
|
|
access_token = request.headers["Authorization"].split(" ")[1]
|
|
|
|
return {
|
|
|
|
"registration_client_uri": request.uri,
|
|
|
|
"registration_access_token": access_token,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
class CodeChallenge(_CodeChallenge):
|
|
|
|
def get_authorization_code_challenge(self, authorization_code):
|
|
|
|
return authorization_code.challenge
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def get_authorization_code_challenge_method(self, authorization_code):
|
|
|
|
return authorization_code.challenge_method
|
2022-05-20 12:07:56 +00:00
|
|
|
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
authorization = AuthorizationServer()
|
|
|
|
require_oauth = ResourceProtector()
|
2022-05-20 12:07:56 +00:00
|
|
|
|
|
|
|
|
2023-01-13 19:26:35 +00:00
|
|
|
def generate_access_token(client, grant_type, user, scope):
|
2023-03-08 22:53:53 +00:00
|
|
|
audience = [client.client_id for client in client.audience]
|
2023-01-13 19:26:35 +00:00
|
|
|
bearer_token_generator = authorization._token_generators["default"]
|
|
|
|
kwargs = {
|
|
|
|
"token": {},
|
|
|
|
"user_info": generate_user_info(user, scope),
|
|
|
|
"aud": audience,
|
|
|
|
**get_jwt_config(grant_type),
|
|
|
|
}
|
|
|
|
kwargs["exp"] = bearer_token_generator._get_expires_in(client, grant_type)
|
|
|
|
return generate_id_token(**kwargs)
|
|
|
|
|
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
def setup_oauth(app):
|
2023-12-18 17:06:03 +00:00
|
|
|
# hacky, but needed for tests as somehow the same 'authorization' object is used
|
|
|
|
# between tests
|
|
|
|
authorization.__init__()
|
2022-10-06 11:32:41 +00:00
|
|
|
authorization.init_app(app, query_client=query_client, save_token=save_token)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
authorization.register_grant(PasswordGrant)
|
|
|
|
authorization.register_grant(ImplicitGrant)
|
|
|
|
authorization.register_grant(RefreshTokenGrant)
|
|
|
|
authorization.register_grant(ClientCredentialsGrant)
|
|
|
|
|
|
|
|
authorization.register_grant(
|
|
|
|
AuthorizationCodeGrant,
|
2023-07-06 15:54:03 +00:00
|
|
|
[
|
2023-12-18 17:06:03 +00:00
|
|
|
OpenIDCode(require_nonce=app.config["CANAILLE_OIDC"]["REQUIRE_NONCE"]),
|
2023-07-06 15:54:03 +00:00
|
|
|
CodeChallenge(required=True),
|
|
|
|
],
|
2022-10-06 11:32:41 +00:00
|
|
|
)
|
|
|
|
authorization.register_grant(OpenIDImplicitGrant)
|
|
|
|
authorization.register_grant(OpenIDHybridGrant)
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
require_oauth.register_token_validator(BearerTokenValidator())
|
2022-05-20 12:07:56 +00:00
|
|
|
|
2022-10-06 11:32:41 +00:00
|
|
|
authorization.register_endpoint(IntrospectionEndpoint)
|
|
|
|
authorization.register_endpoint(RevocationEndpoint)
|
2022-10-19 19:57:25 +00:00
|
|
|
authorization.register_endpoint(ClientRegistrationEndpoint)
|
2022-10-24 15:18:46 +00:00
|
|
|
authorization.register_endpoint(ClientConfigurationEndpoint)
|