canaille-globuzma/canaille/oidc/oauth2utils.py

334 lines
11 KiB
Python
Raw Normal View History

2020-08-16 17:39:14 +00:00
import datetime
2021-12-20 22:57:27 +00:00
from authlib.integrations.flask_oauth2 import AuthorizationServer
from authlib.integrations.flask_oauth2 import ResourceProtector
2020-08-14 13:26:14 +00:00
from authlib.oauth2.rfc6749.grants import (
AuthorizationCodeGrant as _AuthorizationCodeGrant,
2021-12-20 22:57:27 +00:00
)
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 (
2020-08-14 13:26:14 +00:00
ResourceOwnerPasswordCredentialsGrant as _ResourceOwnerPasswordCredentialsGrant,
2020-08-14 11:18:08 +00:00
)
2020-08-14 13:26:14 +00:00
from authlib.oauth2.rfc6750 import BearerTokenValidator as _BearerTokenValidator
2020-08-24 13:56:30 +00:00
from authlib.oauth2.rfc7009 import RevocationEndpoint as _RevocationEndpoint
2020-08-25 13:28:13 +00:00
from authlib.oauth2.rfc7636 import CodeChallenge as _CodeChallenge
2020-08-24 12:44:32 +00:00
from authlib.oauth2.rfc7662 import IntrospectionEndpoint as _IntrospectionEndpoint
2020-08-14 13:26:14 +00:00
from authlib.oidc.core import UserInfo
2021-12-20 22:57:27 +00:00
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
2020-08-24 08:03:48 +00:00
from flask import current_app
2021-12-20 22:57:27 +00:00
2022-01-11 18:49:06 +00:00
from ..models import Group
from ..models import User
2021-12-20 22:57:27 +00:00
from .models import AuthorizationCode
from .models import Client
from .models import Token
2021-12-03 17:37:25 +00:00
DEFAULT_JWT_KTY = "RSA"
DEFAULT_JWT_ALG = "RS256"
DEFAULT_JWT_EXP = 3600
2020-08-14 13:26:14 +00:00
def exists_nonce(nonce, req):
2020-08-17 16:49:05 +00:00
exists = AuthorizationCode.filter(oauthClientID=req.client_id, oauthNonce=nonce)
2020-08-14 13:26:14 +00:00
return bool(exists)
2020-08-24 08:03:48 +00:00
def get_jwt_config(grant):
2021-12-03 17:37:25 +00:00
2020-08-28 14:07:39 +00:00
with open(current_app.config["JWT"]["PRIVATE_KEY"]) as pk:
return {
"key": pk.read(),
2021-12-03 17:37:25 +00:00
"alg": current_app.config["JWT"].get("ALG", DEFAULT_JWT_ALG),
2020-08-28 14:07:39 +00:00
"iss": authorization.metadata["issuer"],
2021-12-03 17:37:25 +00:00
"exp": current_app.config["JWT"].get("EXP", DEFAULT_JWT_EXP),
2020-08-28 14:07:39 +00:00
}
2020-08-24 08:03:48 +00:00
2020-08-14 13:26:14 +00:00
def generate_user_info(user, scope):
2020-10-21 15:15:33 +00:00
user = User.get(dn=user)
claims = ["sub"]
2020-08-24 08:03:48 +00:00
if "profile" in scope:
claims += [
2020-08-24 08:03:48 +00:00
"name",
"family_name",
"given_name",
"nickname",
"preferred_username",
"profile",
"picture",
"website",
"gender",
"birthdate",
"zoneinfo",
"locale",
"updated_at",
]
if "email" in scope:
claims += ["email", "email_verified"]
2020-08-24 08:03:48 +00:00
if "address" in scope:
claims += ["address"]
2020-08-24 08:03:48 +00:00
if "phone" in scope:
claims += ["phone_number", "phone_number_verified"]
2021-06-03 15:24:36 +00:00
if "groups" in scope:
claims += ["groups"]
data = generate_user_claims(user, claims)
return UserInfo(**data)
def generate_user_claims(user, claims, jwt_mapping_config=None):
jwt_mapping_config = jwt_mapping_config or current_app.config["JWT"]["MAPPING"]
2020-08-24 08:03:48 +00:00
data = {}
for claim in claims:
2021-12-12 15:15:06 +00:00
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":
2021-12-03 17:37:25 +00:00
group_name_attr = current_app.config["LDAP"].get(
"GROUP_NAME_ATTRIBUTE", Group.DEFAULT_NAME_ATTRIBUTE
)
data[claim] = [getattr(g, group_name_attr)[0] for g in user.groups]
return data
2020-08-14 13:26:14 +00:00
2020-08-19 09:16:18 +00:00
def save_authorization_code(code, request):
2020-08-14 13:26:14 +00:00
nonce = request.data.get("nonce")
2020-08-17 15:49:49 +00:00
now = datetime.datetime.now()
code = AuthorizationCode(
2020-08-19 09:16:18 +00:00
oauthCode=code,
oauthSubject=request.user,
oauthClient=request.client.dn,
2020-08-19 09:16:18 +00:00
oauthRedirectURI=request.redirect_uri or request.client.oauthRedirectURIs[0],
2020-08-17 15:49:49 +00:00
oauthScope=request.scope,
2020-08-17 16:49:05 +00:00
oauthNonce=nonce,
2021-12-08 14:53:20 +00:00
oauthAuthorizationDate=now,
2020-08-17 15:49:49 +00:00
oauthAuthorizationLifetime=str(84000),
2020-08-24 12:44:32 +00:00
oauthCodeChallenge=request.data.get("code_challenge"),
oauthCodeChallengeMethod=request.data.get("code_challenge_method"),
2020-08-14 13:26:14 +00:00
)
2020-08-17 15:49:49 +00:00
code.save()
return code.oauthCode
2020-08-14 11:18:08 +00:00
2020-08-14 13:26:14 +00:00
class AuthorizationCodeGrant(_AuthorizationCodeGrant):
2020-08-25 13:51:49 +00:00
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"]
2020-08-19 09:16:18 +00:00
def save_authorization_code(self, code, request):
return save_authorization_code(code, request)
2020-08-14 11:18:08 +00:00
2020-08-19 08:28:28 +00:00
def query_authorization_code(self, code, client):
item = AuthorizationCode.filter(oauthCode=code, oauthClient=client.dn)
2020-08-17 16:49:05 +00:00
if item and not item[0].is_expired():
2020-08-17 15:49:49 +00:00
return item[0]
2020-08-14 11:18:08 +00:00
def delete_authorization_code(self, authorization_code):
2020-08-17 16:02:38 +00:00
authorization_code.delete()
2020-08-14 11:18:08 +00:00
def authenticate_user(self, authorization_code):
2021-11-21 10:31:18 +00:00
user = User.get(dn=authorization_code.oauthSubject)
if user:
return user.dn
2020-08-14 11:18:08 +00:00
2020-08-14 13:26:14 +00:00
class OpenIDCode(_OpenIDCode):
def exists_nonce(self, nonce, request):
return exists_nonce(nonce, request)
def get_jwt_config(self, grant):
2020-08-24 08:03:48 +00:00
return get_jwt_config(grant)
2020-08-14 13:26:14 +00:00
def generate_user_info(self, user, scope):
return generate_user_info(user, scope)
2021-10-13 09:52:02 +00:00
def get_audiences(self, request):
client = request.client
return [Client.get(aud).oauthClientID for aud in client.oauthAudience]
2020-08-14 13:26:14 +00:00
class PasswordGrant(_ResourceOwnerPasswordCredentialsGrant):
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post", "none"]
2020-08-14 11:18:08 +00:00
def authenticate_user(self, username, password):
2021-11-21 10:29:41 +00:00
user = User.authenticate(username, password)
if user:
return user.dn
2020-08-14 11:18:08 +00:00
2020-08-14 13:26:14 +00:00
class RefreshTokenGrant(_RefreshTokenGrant):
2020-08-14 11:18:08 +00:00
def authenticate_refresh_token(self, refresh_token):
2020-08-24 08:52:21 +00:00
token = Token.filter(oauthRefreshToken=refresh_token)
if token and token[0].is_refresh_token_active():
return token[0]
2020-08-14 11:18:08 +00:00
def authenticate_user(self, credential):
2021-11-21 10:31:18 +00:00
user = User.get(dn=credential.oauthSubject)
if user:
return user.dn
2020-08-14 11:18:08 +00:00
def revoke_old_credential(self, credential):
2021-12-08 14:53:20 +00:00
credential.oauthRevokationDate = datetime.datetime.now()
2020-08-25 09:35:30 +00:00
credential.save()
2020-08-14 13:26:14 +00:00
2020-08-16 17:39:14 +00:00
2020-08-20 12:30:42 +00:00
class OpenIDImplicitGrant(_OpenIDImplicitGrant):
2020-08-14 13:26:14 +00:00
def exists_nonce(self, nonce, request):
return exists_nonce(nonce, request)
2020-08-23 17:56:37 +00:00
def get_jwt_config(self, grant=None):
2020-08-24 08:03:48 +00:00
return get_jwt_config(grant)
2020-08-14 13:26:14 +00:00
def generate_user_info(self, user, scope):
return generate_user_info(user, scope)
2021-10-13 09:52:02 +00:00
def get_audiences(self, request):
client = request.client
return [Client.get(aud).oauthClientID for aud in client.oauthAudience]
2020-08-14 13:26:14 +00:00
2020-08-20 12:30:42 +00:00
class OpenIDHybridGrant(_OpenIDHybridGrant):
2020-08-19 09:16:18 +00:00
def save_authorization_code(self, code, request):
return save_authorization_code(code, request)
2020-08-14 13:26:14 +00:00
def exists_nonce(self, nonce, request):
return exists_nonce(nonce, request)
2020-08-24 08:03:48 +00:00
def get_jwt_config(self, grant=None):
return get_jwt_config(grant)
2020-08-14 13:26:14 +00:00
def generate_user_info(self, user, scope):
return generate_user_info(user, scope)
2020-08-14 11:18:08 +00:00
2021-10-13 09:52:02 +00:00
def get_audiences(self, request):
client = request.client
return [Client.get(aud).oauthClientID for aud in client.oauthAudience]
2020-08-14 11:18:08 +00:00
def query_client(client_id):
return Client.get(client_id)
def save_token(token, request):
2020-08-16 17:39:14 +00:00
now = datetime.datetime.now()
2020-08-17 16:02:38 +00:00
t = Token(
2020-08-16 17:39:14 +00:00
oauthTokenType=token["token_type"],
oauthAccessToken=token["access_token"],
2021-12-08 14:53:20 +00:00
oauthIssueDate=now,
2020-08-16 17:39:14 +00:00
oauthTokenLifetime=str(token["expires_in"]),
2020-08-17 07:45:35 +00:00
oauthScope=token["scope"],
oauthClient=request.client.dn,
2020-08-24 12:44:32 +00:00
oauthRefreshToken=token.get("refresh_token"),
2020-08-27 08:50:50 +00:00
oauthSubject=request.user,
2021-10-13 09:52:02 +00:00
oauthAudience=request.client.oauthAudience,
2020-08-16 17:39:14 +00:00
)
2020-08-17 16:02:38 +00:00
t.save()
2020-08-14 11:18:08 +00:00
2020-08-14 13:26:14 +00:00
class BearerTokenValidator(_BearerTokenValidator):
2020-08-14 11:18:08 +00:00
def authenticate_token(self, token_string):
return Token.get(token_string)
def request_invalid(self, request):
return False
def token_revoked(self, token):
2020-09-17 09:10:12 +00:00
return bool(token.oauthRevokationDate)
2020-08-14 11:18:08 +00:00
2020-08-14 13:26:14 +00:00
2020-08-24 13:56:30 +00:00
class RevocationEndpoint(_RevocationEndpoint):
def query_token(self, token, token_type_hint, client):
if token_type_hint == "access_token":
return Token.filter(oauthClient=client.dn, oauthAccessToken=token)
2020-08-24 13:56:30 +00:00
elif token_type_hint == "refresh_token":
return Token.filter(oauthClient=client.dn, oauthRefreshToken=token)
2020-08-24 13:56:30 +00:00
item = Token.filter(oauthClient=client.dn, oauthAccessToken=token)
2020-08-24 13:56:30 +00:00
if item:
return item[0]
item = Token.filter(oauthClient=client.dn, oauthRefreshToken=token)
2020-08-24 13:56:30 +00:00
if item:
return item[0]
return None
def revoke_token(self, token):
2021-12-08 14:53:20 +00:00
token.oauthRevokationDate = datetime.datetime.now()
2020-08-24 13:56:30 +00:00
token.save()
2020-08-24 12:44:32 +00:00
class IntrospectionEndpoint(_IntrospectionEndpoint):
def query_token(self, token, token_type_hint, client):
if token_type_hint == "access_token":
tok = Token.filter(oauthAccessToken=token)
elif token_type_hint == "refresh_token":
tok = Token.filter(oauthRefreshToken=token)
else:
tok = Token.filter(oauthAccessToken=token)
if not tok:
tok = Token.filter(oauthRefreshToken=token)
if tok:
tok = tok[0]
2021-10-13 10:08:08 +00:00
if client.dn in tok.oauthAudience:
2020-08-24 12:44:32 +00:00
return tok
def introspect_token(self, token):
client_id = Client.get(token.oauthClient).oauthClientID
2021-10-03 18:26:47 +00:00
user = User.get(dn=token.oauthSubject)
2021-10-13 09:52:02 +00:00
audience = [Client.get(aud).oauthClientID for aud in token.oauthAudience]
2020-08-24 12:44:32 +00:00
return {
"active": True,
"client_id": client_id,
2020-08-24 12:44:32 +00:00
"token_type": token.oauthTokenType,
2021-10-03 18:26:47 +00:00
"username": user.name,
2020-08-24 12:44:32 +00:00
"scope": token.get_scope(),
2021-10-03 18:26:47 +00:00
"sub": user.uid[0],
2021-10-13 09:52:02 +00:00
"aud": audience,
2020-08-28 14:07:39 +00:00
"iss": authorization.metadata["issuer"],
2020-08-24 12:44:32 +00:00
"exp": token.get_expires_at(),
"iat": token.get_issued_at(),
}
2020-08-25 13:28:13 +00:00
class CodeChallenge(_CodeChallenge):
def get_authorization_code_challenge(self, authorization_code):
return authorization_code.oauthCodeChallenge
def get_authorization_code_challenge_method(self, authorization_code):
return authorization_code.oauthCodeChallengeMethod
2020-08-14 13:26:14 +00:00
authorization = AuthorizationServer()
2020-08-14 11:18:08 +00:00
require_oauth = ResourceProtector()
def setup_oauth(app):
2020-08-14 13:26:14 +00:00
authorization.init_app(app, query_client=query_client, save_token=save_token)
2020-08-14 11:18:08 +00:00
2020-08-20 12:30:42 +00:00
authorization.register_grant(PasswordGrant)
authorization.register_grant(ImplicitGrant)
2020-08-24 08:52:21 +00:00
authorization.register_grant(RefreshTokenGrant)
2020-08-20 12:30:42 +00:00
authorization.register_grant(ClientCredentialsGrant)
2020-08-14 13:26:14 +00:00
authorization.register_grant(
2020-08-24 12:44:32 +00:00
AuthorizationCodeGrant,
2020-08-25 13:39:44 +00:00
[OpenIDCode(require_nonce=True), CodeChallenge(required=True)],
2020-08-14 13:26:14 +00:00
)
2020-08-20 12:30:42 +00:00
authorization.register_grant(OpenIDImplicitGrant)
authorization.register_grant(OpenIDHybridGrant)
2020-08-14 11:18:08 +00:00
require_oauth.register_token_validator(BearerTokenValidator())
2020-08-24 12:44:32 +00:00
authorization.register_endpoint(IntrospectionEndpoint)
2020-08-24 13:56:30 +00:00
authorization.register_endpoint(RevocationEndpoint)