2024-04-09 23:33:30 +02:00
|
|
|
"""Authentication Backends for the People core app."""
|
2024-03-07 10:57:05 +01:00
|
|
|
|
2024-02-15 12:55:00 +01:00
|
|
|
from django.conf import settings
|
2024-02-15 11:00:30 +01:00
|
|
|
from django.core.exceptions import SuspiciousOperation
|
2024-01-03 10:09:31 +01:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
2024-02-15 11:00:30 +01:00
|
|
|
import requests
|
|
|
|
|
from mozilla_django_oidc.auth import (
|
|
|
|
|
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
|
|
|
|
)
|
|
|
|
|
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-02-15 11:00:30 +01:00
|
|
|
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|
|
|
|
"""Custom OpenID Connect (OIDC) Authentication Backend.
|
|
|
|
|
|
|
|
|
|
This class overrides the default OIDC Authentication Backend to accommodate differences
|
2024-06-09 22:43:42 +02:00
|
|
|
in the User model, and handles signed and/or encrypted UserInfo response.
|
2024-02-15 11:00:30 +01:00
|
|
|
"""
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-02-15 11:00:30 +01:00
|
|
|
def get_userinfo(self, access_token, id_token, payload):
|
|
|
|
|
"""Return user details dictionary.
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-02-15 11:00:30 +01:00
|
|
|
Parameters:
|
|
|
|
|
- access_token (str): The access token.
|
|
|
|
|
- id_token (str): The id token (unused).
|
|
|
|
|
- payload (dict): The token payload (unused).
|
|
|
|
|
|
|
|
|
|
Note: The id_token and payload parameters are unused in this implementation,
|
|
|
|
|
but were kept to preserve base method signature.
|
|
|
|
|
|
|
|
|
|
Note: It handles signed and/or encrypted UserInfo Response. It is required by
|
|
|
|
|
Agent Connect, which follows the OIDC standard. It forces us to override the
|
|
|
|
|
base method, which deal with 'application/json' response.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
- dict: User details dictionary obtained from the OpenID Connect user endpoint.
|
2024-01-03 10:09:31 +01:00
|
|
|
"""
|
2024-02-15 11:00:30 +01:00
|
|
|
|
|
|
|
|
user_response = requests.get(
|
|
|
|
|
self.OIDC_OP_USER_ENDPOINT,
|
|
|
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
|
|
|
verify=self.get_settings("OIDC_VERIFY_SSL", True),
|
|
|
|
|
timeout=self.get_settings("OIDC_TIMEOUT", None),
|
|
|
|
|
proxies=self.get_settings("OIDC_PROXY", None),
|
|
|
|
|
)
|
|
|
|
|
user_response.raise_for_status()
|
|
|
|
|
userinfo = self.verify_token(user_response.text)
|
|
|
|
|
return userinfo
|
|
|
|
|
|
|
|
|
|
def get_or_create_user(self, access_token, id_token, payload):
|
|
|
|
|
"""Return a User based on userinfo. Get or create a new user if no user matches the Sub.
|
|
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
|
- access_token (str): The access token.
|
|
|
|
|
- id_token (str): The ID token.
|
|
|
|
|
- payload (dict): The user payload.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
- User: An existing or newly created User instance.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
- Exception: Raised when user creation is not allowed and no existing user is found.
|
2024-01-03 10:09:31 +01:00
|
|
|
"""
|
|
|
|
|
|
2024-02-15 11:00:30 +01:00
|
|
|
user_info = self.get_userinfo(access_token, id_token, payload)
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-02-15 12:55:00 +01:00
|
|
|
# Compute user name from OIDC name fields as defined in settings
|
|
|
|
|
names_list = [
|
|
|
|
|
user_info[field]
|
|
|
|
|
for field in settings.USER_OIDC_FIELDS_TO_NAME
|
|
|
|
|
if user_info.get(field)
|
|
|
|
|
]
|
|
|
|
|
user_info["name"] = " ".join(names_list) or None
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-02-15 12:55:00 +01:00
|
|
|
sub = user_info.get("sub")
|
2024-02-15 11:00:30 +01:00
|
|
|
if sub is None:
|
|
|
|
|
raise SuspiciousOperation(
|
|
|
|
|
_("User info contained no recognizable user identification")
|
|
|
|
|
)
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-06-09 22:43:42 +02:00
|
|
|
try:
|
✨(backend) add resource server backend
Why:
Many services in La Suite rely on Agent Connect to authenticate their users.
Delegating authentication to Agent Connect is highly beneficial. With a central
party (Agent Connect) handling user authentication, our services can seamlessly
communicate with each other. Our backend must be able to receive and verify
access tokens issued by Agent Connect.
Additionally, it should ensure that the resource owner has granted permission
for our data to the service provider transmitting the access token.
How:
Our backend needs to verify access tokens by introspecting them. This involves
requesting the Authorization Server to validate the access token received in
the authentication header. The Authorization Server validates the token's
integrity, provides authentication and authorization information about
the user currently logged into the service provider requesting data from
the resource server.
The data returned by the Authorization Server to the resource server
is encrypted and signed. To encrypt the introspection token, the Authorization
Server retrieves the resource server's public key from
the new ‘/jwks’ endpoint.
Encryption parameters, such as algorithm and encoding, are configured on
the resource server. Ensure that these parameters match between
the Authorization Server and the resource server.
The resource server verifies the token signature using the Authorization
Server's public key, exposed through its `/jwks` endpoint. Make sure
the signature algorithms match between both servers. Finally, introspection
token claims are verified to adhere to good practices for handling JWTs,
including checks on issuer, audience, and expiration time.
The introspection token contains a subject (`sub`). The resource server uses
this subject to retrieve the requested database user, compatible
with both pairwise and public subjects.
Important:
Agent Connect does not follow RFC 7662 but uses a draft RFC that adds security
(signing/encryption) to the initial specification. Refer to the "References"
section for more information.
References:
The initial RFC describing token introspection is RFC 7662 "OAuth 2.0 Token
Introspection". However, this RFC specifies that the introspection
response is a plain JSON object.
In eGovernment applications, our resource server requires stronger assurance
that the Authorization Server issued the token introspection response.
France Connect's team implemented a stronger version of the spec, returning
a signed and encrypted token introspection response. This version is still
a draft, available under:
"draft-ietf-oauth-jwt-introspection-response".
2024-07-29 14:24:47 +02:00
|
|
|
user = self.UserModel.objects.get(sub=sub, is_active=True)
|
2024-06-09 22:43:42 +02:00
|
|
|
except self.UserModel.DoesNotExist:
|
|
|
|
|
if self.get_settings("OIDC_CREATE_USER", True):
|
|
|
|
|
user = self.create_user(user_info)
|
|
|
|
|
else:
|
2024-02-15 12:55:00 +01:00
|
|
|
email = user_info.get("email")
|
|
|
|
|
name = user_info.get("name")
|
2024-06-09 22:43:42 +02:00
|
|
|
if email and email != user.email or name and name != user.name:
|
|
|
|
|
self.UserModel.objects.filter(sub=sub).update(email=email, name=name)
|
2024-02-15 11:00:30 +01:00
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
def create_user(self, claims):
|
|
|
|
|
"""Return a newly created User instance."""
|
|
|
|
|
sub = claims.get("sub")
|
|
|
|
|
if sub is None:
|
|
|
|
|
raise SuspiciousOperation(
|
|
|
|
|
_("Claims contained no recognizable user identification")
|
|
|
|
|
)
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-06-09 22:43:42 +02:00
|
|
|
return self.UserModel.objects.create(
|
|
|
|
|
password="!", # noqa: S106
|
|
|
|
|
sub=sub,
|
|
|
|
|
email=claims.get("email"),
|
|
|
|
|
name=claims.get("name"),
|
2024-02-15 12:55:00 +01:00
|
|
|
)
|