Compare commits

...

13 Commits

Author SHA1 Message Date
lebaudantoine e502368c8b 🚧(backend) return groups when requesting the resource server
Work in progress code, used during last Monday demo.
I will need to iterate.
2024-05-31 18:15:00 +02:00
lebaudantoine e4da9b23cd 🚧(backend) refactor token introspection to separate responsibilities
Note: This commit is work in progress and has not been tested on my
machine. I'm pushing it now to gather early feedback.

Based on @Jonathan Perret's recommendations, I've split the responsibilities
of resource server authentication into three distinct classes, each mapping
to specific roles:

- A DRF authentication class
- A client to interact with the Authorization Server
- A client to interact with the Resource Server

In the OAuth 2.0 context, the Authorization Server is equivalent to the OIDC
provider. This separation aims to improve the readability of the code and
facilitate unit testing.

Additionally, this refactor provides better documentation of the technical
specifications followed by my code.
2024-05-31 18:07:52 +02:00
antoine lebaud cc157904d0 (backend) implement token introspection matching Agent Connect specs
**important** Agent Connect doesn't follow the RFC 7662, but a draft
RFC, which adds security (signing/encryption) to the initial spec.
Please take a loot at the "references" part.

Concept:

This commit initiates the implementation of token introspection by
requesting the OIDC provider (OP) to validate the access token received
from the client. The access token is initially issued by the OP, passed to
the Service Provider (SP), and then forwarded to the Resource Server (RS) by
the SP.

The token introspection is done by our custom RS authentication class.

The OP is the service capable of validating the integrity of the access
token. Token introspection requests to the OP should provide authentication
and authorization information about the user currently logged in the SP,
which requests data from the RS.

Data returned by the OP to the RS are encrypted and signed.

To encrypt the introspection token, the OP retrieves DP public key from the
newly introduced endpoint '/jwks'. The encryption parameters (algorithm and
encoding) are set while configuring the RS. Please ensure that the encryption
algorithm and encoding are matching between the OP and the RS.

Token signature is verified by the RS, using OP's public key, exposed through
its '/jwks' endpoint. Please make sure the signature algorithm are matching
between the OP and the RS.

Finally, introspection token claims are validated to follow good practices while
handling JWT. Claims as issuer, audience, or expiration time are validated.

The introspection token contains a sub. RS retrieves the requested db user from
using this sub. This should work with a paiwise or a public sub.

References:

The initial RFC describing the token introspection is the RFC 7662 "Oauth 2.0
Token Introspection". However, the introspection response specified in this RFC,
is a plain JSON object

In eGovernement applications, our resource server requires a stronger assurance
that the authorization server issued the token introspection response.

France Connect's team decided to implement a stronger version of the spec, which
returns a signed and encrypted token introspection response. This stronger
version is still draft, you can find it by searching:

"draft-ietf-oauth-jwt-introspection-response"

For documentation on the token introspection flow, refer to the France
Connect documentation at the "validation token" section. It mentions both specs
but doesn't explain their choice.

Although the documentation references France Connect, it describes the
general behavior required by the draft specification, which is also supported
in Agent Connect.
2024-05-31 18:07:52 +02:00
antoine lebaud b06e30f436 🔧(docker) integrate app-dev with the France Connect Docker network
!!! this commit will be dropped before merging, it's useful only for
dev purposes.

Enabled communication between the Desk backend and France Connect backend
by housing both within the same Docker virtual network.

Instead of creating a new network, I opted to incorporate the Desk backend
container into the existing network spawned by the France Connect Docker stack.
This approach required configuring the network as external to ensure proper
integration and functionality.

This is inspired by the suggestion from @sam, drawing insights from
the connections between Richie and Open edX.
2024-05-31 18:07:23 +02:00
antoine lebaud d124b57d16 ♻️(backend) rename Django settings related to the Resource Server (RS)
To align with the previous OIDC configuration format, this commit prefixes
every setting with "OIDC", followed by the relevant part (e.g., "RS"). This
ensures consistency across the OIDC configurations.
2024-05-31 14:38:24 +02:00
antoine lebaud bc182fe313 ♻️(backend) reorganize Django settings related to OIDC
This commit proposes a reorganization of Django settings related to OIDC
based on the OIDC parts they are associated with.

Settings are now categorized into four groups:
- OIDC Provider (OP) settings
- Resource Provider (RP) settings
- Resource Server (RS) settings
- Miscellaneous settings related to the Authorization flow

This commit is a proposition, and and may be discarded.
2024-05-31 14:38:06 +02:00
antoine lebaud be364d9227 (backend) add get_settings method
Copied from mozzilla-django-oidc repository, add a static method
on my authentication backend, which get settings from Django settings,
or give a default value if provided as an arg.

It will be use a lot when accessing Django settings in the next methods.
Might be discussed, and considered as an unnecessary wrapper.
2024-05-31 14:38:06 +02:00
antoine lebaud 681065dd85 🛂(backend) introduce an authentication class for the resource server
This commit introduces a new resource server authentication backend.
It's work in progress and will handle token introspection. It is designed to seamlessly
integrate with DRF code elements.

[explain in which context it will be useful]
2024-05-31 14:38:06 +02:00
antoine lebaud a1065e55e0 ♻️(backend) encapsulate cryptographic key loading
I explained the responsibilities of the private key in the context of the
resource server.

Consider enhancing this utility function, as it currently imposes an
opinionated approach to loading the private key and passing parameters to the
joserfc utilities.

The documentation could be more concise to avoid excessive detail.

The current API and return type are tightly coupled with joserfc usage and
types. Should we consider exposing a generic Python object to simplify the
interface?
2024-05-31 14:38:01 +02:00
antoine lebaud 6b15954d05 (backend) expose cryptographic key for encryption
This commit focuses on generating a valid keyset for the project. The following
points need to be discussed:

- Error Handling
- Settings' Names
- Storing Private Key as a decoded string or a PEM file

The JWK structure and parameters align with the implementation of France
Connect (FC) mocked data provider, named resource server in Oauth spec.

The current implementation is fully parameterizable, allowing for easy
configuration changes via Django settings. For instance, switching to another
key type is as simple as updating the settings.

However, one critique of this design is its lack of extensibility. If the
decision is made to offer more than one encryption method, this view will
require refactoring.

This commit is laying the foundation for further discussions and improvements.
2024-05-31 11:41:01 +02:00
antoine lebaud 23cf6c31f7 (backend) handle JSON Object Signing and Encryption (JOSE)
joserfc has been chosen as the dependency to implement a JWKs endpoint in the
Django app. While there are several alternatives available such as pyjwt,
python-jose, jwcrypto, etc., joserfc has been opted for the following reasons:

- Cryptography: Although using only cryptography is feasible, its
  interface/API is not as user-friendly.

- pyjwt: While pyjwt is popular, it lacks support for JWK and JWE, which are
  essential for the requirements.

- python-jose: The latest release of python-jose was in 2021, and the
  project seems less active compared to other alternatives.

- Authlib: Authlib is the second most popular library after pyjwt and seems
  modern with an active community. However, the parts relevant to the use case
  were extracted into a relatively new package named joserfc.

- joserfc: Although joserfc has fewer stars compared to Authlib, it was
  extracted from Authlib, which has more than 4k stars, indicating a solid
  foundation.

While the low star count of joserfc might raise concerns about its stability, it
is believed to be worth considering its addition. Adding Authlib and refactoring
later, once they finish migrating to joserfc, is a possibility to address
any potential regressions.

One of my concern is that joserfc hasn’t been released in a major version yet.

This commit is a work in progress, and further evaluation and testing are
needed before finalizing the integration of joserfc into the project.
2024-05-31 11:08:46 +02:00
antoine lebaud 5b2988491a 🤡(backend) mock jwks endpoint
This introduce a new endpoint, which is a read-only URL that returns JWKSs
as JSON objects in response to GET requests.

JWKSs are sets of JWKs, which are JavaScript Object Notation (JSON) data structures
that represent cryptographic keys.

This /jwks route might be refactored to the be located at a well-known location.
2024-05-31 11:08:37 +02:00
antoine lebaud 669633285c 🏗️(backend) create a new python package for the resource server
Encapsulate all Resource Server (RS) sources in a dedicated python package.
2024-05-31 10:57:23 +02:00
11 changed files with 579 additions and 26 deletions
+11
View File
@@ -40,6 +40,10 @@ services:
- postgresql
- mailcatcher
- redis
# FIXME: temporary configuration to connect AC and desk
networks:
- default
- dataprovider_outside
celery-dev:
user: ${DOCKER_USER:-1000}
@@ -169,3 +173,10 @@ services:
- "8080:8080"
depends_on:
- kc_postgresql
# FIXME: temporary configuration to connect AC and desk
networks:
dataprovider_outside:
external: true
driver: bridge
name: "${DESK_NETWORK:-fc_public}"
+33
View File
@@ -33,3 +33,36 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
OIDC_RS_CLIENT_ID=people
OIDC_RS_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RS_AUTH_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RS_PRIVATE_KEY_STR="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3boG1kwEGUYL+
U58RPrVToIsF9jHB64S6WJIIInPmAclBciXFb6BWG11mbRIgo8ha3WVnC/tGHbXb
ndiKdrH2vKHOsDhV9AmgHgNgWaUK9L0uuKEb/xMLePYWsYlgzcQJx8RZY7RQyWqE
20WfzFxeuCE7QMb6VXSOgwQMnJsKocguIh3VCI9RIBq3B1kdgW35AD63YKOygmGx
qjcWwbjhKLvkF7LpBdlyAEzOKqg4T5uCcHMfksMW2+foTJx70RrZM/KHU+Zysuw7
uhhVsgPBG+CsqBSjHQhs7jzymqxtQAfe1FkrCRxOq5Pv2Efr7kgtVSkJJiX3KutM
vnWuEypxAgMBAAECggEAGqKS9pbrN+vnmb7yMsqYgVVnQn0aggZNHlLkl4ZLLnuV
aemlhur7zO0JzajqUC+AFQOfaQxiFu8S/FoJ+qccFdATrcPEVmTKbgPVqSyzLKlX
fByGll5eOVT95NMwN8yBGgt2HSW/ZditXS/KxxahVgamGqjAC9MTSutGz/8Ae1U+
DNDBJCc6RAqu3T02tV9A2pSpVC1rSktDMpLUTscnsfxpaEQATd9DJUcHEvIwoX8q
GJpycPEhNhdPXqpln5SoMHcf/zS5ssF/Mce0lJJXYyE0LnEk9X12jMWyBqmLqXUY
cKLyynaFbis0DpQppwKx2y8GpL76k+Ci4dOHIvFknQKBgQDj/2WRMcWOvfBrggzj
FHpcme2gSo5A5c0CVyI+Xkf1Zab6UR6T7GiImEoj9tq0+o2WEix9rwoypgMBq8rz
/rrJAPSZjgv6z71k4EnO2FIB5R03vQmoBRCN8VlgvLM0xv52zyjV4Wx66Q4MDjyH
EgkpHyB0FzRZh0UzhnE/pYSetQKBgQDN9eLB1nA4CBSr1vMGNfQyfBQl3vpO9EP4
VSS3KnUqCIjJeLu682Ylu7SFxcJAfzUpy5S43hEvcuJsagsVKfmCAGcYZs9/xq3I
vzYyhaEOS5ezNxLSh4+yCNBPlmrmDyoazag0t8H8YQFBN6BVcxbATHqdWGUhIhYN
eEpEMOh2TQKBgGBr7kRNTENlyHtu8IxIaMcowfn8DdUcWmsW9oBx1vTNHKTYEZp1
bG/4F8LF7xCCtcY1wWMV17Y7xyG5yYcOv2eqY8dc72wO1wYGZLB5g5URlB2ycJcC
LVIaM7ZZl2BGl+8fBSIOx5XjYfFvQ+HLmtwtMchm19jVAEseHF7SXRfRAoGAK15j
aT2mU6Yf9C9G7T/fM+I8u9zACHAW/+ut14PxN/CkHQh3P16RW9CyqpiB1uLyZuKf
Zm4cYElotDuAKey0xVMgYlsDxnwni+X3m5vX1hLE1s/5/qrc7zg75QZfbCI1U3+K
s88d4e7rPLhh4pxhZgy0pP1ADkIHMr7ppIJH8OECgYEApNfbgsJVPAMzucUhJoJZ
OmZHbyCtJvs4b+zxnmhmSbopifNCgS4zjXH9qC7tsUph1WE6L2KXvtApHGD5H4GQ
IH5em4M/pHIcsqCi1qggBMbdvzHBUtC3R4sK0CpEFHlN+Y59aGazidcN2FPupNJv
MbyqKyC6DAzv4jEEhHaN7oY=
-----END PRIVATE KEY-----
"
+118
View File
@@ -0,0 +1,118 @@
"""Resource Server authentication class"""
from base64 import b64decode
from requests.exceptions import HTTPError
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed
from ..models import User
from .clients import AuthorizationServerClient, ResourceServerClient
from .utils import get_settings
class ResourceServerAuthentication(BaseAuthentication):
"""Token-based authentication for Resource Server (RS).
Authenticate by passing the token received from the OIDC Provider (OP).
The Resource Server will introspect the token, while the OIDC Provider validates
its integrity and permissions.
"""
@staticmethod
def _decode_authorization_header(request):
"""Get token and received_secret passed by the Service Provider (SP)."""
authorization_header = get_authorization_header(request).split()
if (
not authorization_header
or authorization_header[0].lower() != "Bearer".lower().encode()
):
msg = "Invalid token header. No credentials provided."
raise AuthenticationFailed(msg)
if len(authorization_header) == 1:
msg = "Invalid token header. No credentials provided."
raise AuthenticationFailed(msg)
if len(authorization_header) > 2:
msg = "Invalid token header. Token string should not contain spaces."
raise AuthenticationFailed(msg)
decoded_bearer = b64decode(authorization_header[1]).decode("utf-8")
# FIXME: inherited from France Connect mocked RS, bad practice, a bearer token should respect format specified in the RFC
authorization_data = decoded_bearer.split(":")
if len(authorization_data) != 2:
msg = "Token should contain a token and a received_secret."
raise AuthenticationFailed(msg)
token, received_secret = authorization_data
return (token, received_secret)
@staticmethod
def authenticate_service_provider(received_secret):
"""Authenticate the Service Provider (SP) with a shared secret.
This method is temporary, and inspired by Agent Connect mocked resource servers.
"""
if received_secret != get_settings("OIDC_RS_AUTH_SECRET"):
raise AuthenticationFailed("Invalid authentication secret")
@staticmethod
def _get_user(introspection_response):
"""Retrieve the user associated with the given token introspection response."""
sub = introspection_response.get("sub", None)
if sub is None:
raise AuthenticationFailed(
"Introspection response lacks a subject identifier (sub)."
)
user = User.objects.filter(identities__sub=sub).distinct().first()
if user is None:
raise AuthenticationFailed(
"No user found for the given subject identifier (sub)."
)
return sub
def authenticate(self, request):
"""Authenticate the request using a token issued by the OIDC Provider (OP)"""
access_token, received_secret = self._decode_authorization_header(request)
self.authenticate_service_provider(received_secret)
authorization_server = AuthorizationServerClient(
endpoint_introspection=get_settings("OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT"),
endpoint_jwks=get_settings("OIDC_OP_TOKEN_JWKS_ENDPOINT"),
url=get_settings("OIDC_OP_URL"),
verify_ssl=get_settings("OIDC_VERIFY_SSL", True),
timeout=get_settings("OIDC_TIMEOUT", False),
proxy=get_settings("OIDC_PROXY", True),
)
resource_server = ResourceServerClient(
client_id=get_settings("OIDC_RS_CLIENT_ID"),
client_secret=get_settings("OIDC_RS_CLIENT_SECRET"),
encryption_encoding=get_settings("OIDC_RS_ENCRYPTION_ENCODING"),
encryption_algorithm=get_settings("OIDC_RS_ENCRYPTION_ALGO"),
signing_algorithm=get_settings("OIDC_RS_SIGNING_ALGO"),
authorization_server=authorization_server,
)
try:
introspection_response = resource_server.verify(access_token)
except HTTPError as err:
raise AuthenticationFailed(
"Could not fetch introspection response"
) from err
except ValueError as err:
raise AuthenticationFailed("introspection response is not active") from err
user = self._get_user(introspection_response)
return (user, introspection_response)
+219
View File
@@ -0,0 +1,219 @@
"""Resource Server client classes"""
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
import requests
from joserfc import jwe as jose_jwe
from joserfc import jwt as jose_jwt
from joserfc.errors import InvalidClaimError
from joserfc.jwk import KeySet
from requests.exceptions import HTTPError
from rest_framework.exceptions import AuthenticationFailed
from .utils import import_private_key_from_settings
class AuthorizationServerClient:
"""Client for interacting with an OAuth 2.0 authorization server.
An authorization server issues access tokens to client applications after authenticating
and obtaining authorization from the resource owner. It also provides endpoints for token
introspection and JSON Web Key Sets (JWKS) to validate and decode tokens.
This client facilitates communication with the authorization server, including:
- Fetching token introspection responses.
- Retrieving JSON Web Key Sets (JWKS) for token validation.
- Setting appropriate headers for secure communication as recommended by RFC drafts.
"""
def __init__(
self, endpoint_introspection, endpoint_jwks, url, verify_ssl, timeout, proxy
):
self.url = url
self._endpoint_introspection = endpoint_introspection
self._endpoint_jwks = endpoint_jwks
self._verify_ssl = verify_ssl
self._timeout = timeout
self._proxy = proxy
@property
def _introspection_headers(self):
"""Get HTTP header for the introspection request.
Notify the authorization server that we expect a signed and encrypted response
by setting the appropriate 'Accept' header. This follows the recommendation from
the draft RFC (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-introspection-response-12).
"""
return {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/token-introspection+jwt",
}
def fetch_introspection(self, client_id, client_secret, token):
"""Retrieve introspection response about a token."""
response = requests.post(
self._endpoint_introspection,
data={
"client_id": client_id,
"client_secret": client_secret,
"token": token,
},
headers=self._introspection_headers,
verify=self._verify_ssl,
timeout=self._timeout,
proxies=self._proxy,
)
response.raise_for_status()
return response.text
def fetch_jwks(self):
"""Fetch the Json Web Key Set from the jwks endpoint."""
response = requests.get(
self._endpoint_jwks,
verify=self._verify_ssl,
timeout=self._timeout,
proxies=self._proxy,
)
response.raise_for_status()
return response.json()
def retrieve_public_keys(self):
"""Retrieve the public keys from the jwks endpoint."""
try:
jwks = self.fetch_jwks()
except HTTPError as err:
msg = "Failed to retrieve JWKs from the Authorization Server."
raise AuthenticationFailed(msg) from err
try:
public_keys = KeySet.import_key_set(jwks)
except ValueError as err:
msg = (
"Failed to create a public key set from the Authorization Server jwks."
)
raise AuthenticationFailed(msg) from err
return public_keys
class ResourceServerClient:
"""Client for interacting with an OAuth 2.0 resource server.
In the context of OAuth 2.0, a resource server is a server that hosts protected resources and
is capable of accepting and responding to protected resource requests using access tokens.
The resource server verifies the validity of the access tokens issued by the authorization
server to ensure secure access to the resources.
"""
def __init__(
self,
client_id,
client_secret,
encryption_encoding,
encryption_algorithm,
signing_algorithm,
authorization_server,
):
self._client_id = client_id
self._client_secret = client_secret
self._encryption_encoding = encryption_encoding
self._encryption_algorithm = encryption_algorithm
self._signing_algorithm = signing_algorithm
self._authorization_server = authorization_server
def verify(self, token):
"""Verify the token to determine if it's still valid and active.
This method follows the specifications outlined in RFC 7662
(https://www.rfc-editor.org/info/rfc7662) and the draft RFC
(https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-introspection-response-12).
In our eGovernment applications, the standard RFC 7662 does not provide sufficient security.
Its introspection response is a plain JSON object. Therefore, we use the draft RFC that extends
RFC 7662 by returning a signed and encrypted JWT for stronger assurance that the authorization
server issued the token introspection response.
"""
jwe = self._authorization_server.introspect_token(
self._client_id,
self._client_secret,
token,
)
private_key = import_private_key_from_settings()
jws = self._decrypt(jwe, private_key=private_key)
jwt = self._decode(
jws,
public_key_set=self._authorization_server.retrieve_public_keys(),
)
self._validate_claims(jwt)
introspection_response = jwt.claims.get("token_introspection")
active = introspection_response.get("active", None)
if not active:
raise ValueError("Instrospection response is not active.")
return introspection_response
def _decrypt(self, encrypted_token, private_key):
"""Decrypt the token encrypted by the Authorization Server (AS).
Resource Server (RS)'s public key is used for encryption, and its private
key is used for decryption. The RS's public key is exposed to the AS via a JWK endpoint.
Encryption Algorithm and Encoding should be configured to match between the AS
and the RS.
"""
try:
decrypted_token = jose_jwe.decrypt_compact(
encrypted_token,
private_key,
algorithms=[self._encryption_encoding, self._encryption_algorithm],
)
except ValueError as err:
msg = "Token decryption failed."
raise SuspiciousOperation(msg) from err
return decrypted_token
def _decode(self, encoded_token, public_key_set):
"""Decode the token signed by the Authorization Server (AS).
AS's private key is used for signing, and its public key is used for decoding.
The AS public key is exposed via a JWK endpoint.
Signing Algorithm should be configured to match between the AS and the RS.
"""
try:
token = jose_jwt.decode(
encoded_token.plaintext,
public_key_set,
algorithms=[self._signing_algorithm],
)
except ValueError as err:
msg = "Token decoding failed."
raise SuspiciousOperation(msg) from err
return token
def _validate_claims(self, token):
"""Validate the claims of the token to ensure authentication security.
By validating these claims, we ensure that the token was issued by a
trusted authorization server and is intended for this specific
resource server. This prevents various types of attacks, such as
token substitution or misuse of tokens issued for different clients.
"""
claims_requests = jose_jwt.JWTClaimsRegistry(
iss={"essential": True, "value": self._authorization_server.url},
aud={"essential": True, "value": self._client_id},
token_introspection={"essential": True},
)
try:
claims_requests.validate(token.claims)
except InvalidClaimError as err:
msg = "Failed to validate token's claims."
raise SuspiciousOperation(msg) from err
+11
View File
@@ -0,0 +1,11 @@
"""Resource Server URL Configuration"""
from django.urls import path
from .views import DataView, JWKSView
urlpatterns = [
path("jwks", JWKSView.as_view()),
# FIXME: temporary route
path("data", DataView.as_view()),
]
+61
View File
@@ -0,0 +1,61 @@
"""Resource Server utils functions"""
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from joserfc.jwk import JWKRegistry
def import_private_key_from_settings():
"""Import the private key used by the resource server when interacting with the OIDC provider.
This private key is crucial; its public components are exposed in the JWK endpoints,
while its private component is used for decrypting the introspection token retrieved
from the OIDC provider.
By default, we recommend using RSAKey for asymmetric encryption,
known for its strong security features.
Note:
- The function requires the 'OIDC_RS_PRIVATE_KEY_STR' setting to be configured.
- The 'OIDC_RS_ENCRYPTION_KEY_TYPE' and 'OIDC_RS_ENCRYPTION_ALGO' settings can be customized
based on the chosen key type.
Raises:
ImproperlyConfigured: If the private key setting is missing, empty, or incorrect.
Returns:
joserfc.jwk.JWK: The imported private key as a JWK object.
"""
private_key_str = settings.OIDC_RS_PRIVATE_KEY_STR
if not private_key_str:
raise ImproperlyConfigured(
"OIDC_RS_PRIVATE_KEY_STR setting is missing or empty."
)
private_key_pem = private_key_str.encode()
try:
private_key = JWKRegistry.import_key(
private_key_pem,
key_type=settings.OIDC_RS_ENCRYPTION_KEY_TYPE,
parameters={"alg": settings.OIDC_RS_ENCRYPTION_ALGO, "use": "enc"},
)
except ValueError as err:
raise ImproperlyConfigured("OIDC_RS_PRIVATE_KEY_STR setting is wrong.") from err
return private_key
def get_settings(attr, *args):
"""Get the value of a setting from Django settings."""
try:
if args:
return getattr(settings, attr, args[0])
return getattr(settings, attr)
except AttributeError as err:
# FIXME: lint error C0209
msg = "Setting {0} not found".format(attr)
raise ImproperlyConfigured(msg) from err
+54
View File
@@ -0,0 +1,54 @@
"""Resource Server views"""
from joserfc.jwk import KeySet
from rest_framework.response import Response
from rest_framework.views import APIView
from .auth import ResourceServerAuthentication
from .utils import import_private_key_from_settings
class JWKSView(APIView):
"""
API endpoint for retrieving a JSON Web Key (JWK).
Returns:
Response: JSON response containing the JWK data.
"""
authentication_classes = [] # disable authentication
permission_classes = [] # disable permission
def get(self, request):
"""Handle GET requests to retrieve the JSON Web Key (JWK).
Returns:
Response: JSON response containing the JWK data.
"""
private_key = import_private_key_from_settings()
jwk = KeySet([private_key]).as_dict()
return Response(jwk)
# FIXME: temporary view
class DataView(APIView):
"""Temporary view to test resource server authentication."""
authentication_classes = [ResourceServerAuthentication]
permission_classes = [] # disable permission
def get(self, request):
"""Temporary route to test resource server authentication."""
token = request.auth
if 'groups' in token.get('scope'):
# TODO - return a proper error
Response({'error scope "groups" not requested to the authorization server'})
slugs = [team.slug for team in request.user.teams.all()]
# TODO - use SCIM specification to exchange data
return Response({
"groups": slugs
})
+2
View File
@@ -7,6 +7,7 @@ from mozilla_django_oidc.urls import urlpatterns as oidc_urls
from rest_framework.routers import DefaultRouter
from core.api import viewsets
from core.resource_server.urls import urlpatterns as resource_server_urls
# - Main endpoints
router = DefaultRouter()
@@ -35,6 +36,7 @@ urlpatterns = [
[
*router.urls,
*oidc_urls,
*resource_server_urls,
re_path(
r"^teams/(?P<team_id>[0-9a-z-]*)/",
include(team_related_router.urls),
+69 -26
View File
@@ -301,34 +301,9 @@ class Base(Configuration):
default=True,
environ_name="OIDC_CREATE_USER",
)
OIDC_RP_SIGN_ALGO = values.Value(
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
)
OIDC_RP_CLIENT_ID = values.Value(
"people", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
)
OIDC_RP_CLIENT_SECRET = values.Value(
environ_name="OIDC_RP_CLIENT_SECRET",
environ_prefix=None,
)
OIDC_OP_JWKS_ENDPOINT = values.Value(
environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
)
OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
)
OIDC_OP_TOKEN_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
)
OIDC_OP_USER_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
)
OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
{}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
)
OIDC_RP_SCOPES = values.Value(
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
)
LOGIN_REDIRECT_URL = values.Value(
None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None
)
@@ -347,13 +322,81 @@ class Base(Configuration):
OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue(
default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None
)
USER_OIDC_FIELDS_TO_NAME = values.ListValue(
default=["first_name", "last_name"],
environ_name="USER_OIDC_FIELDS_TO_NAME",
environ_prefix=None,
)
# OIDC - Resource Provider (RP) also named Service Provider (SP)
OIDC_RP_SCOPES = values.Value(
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
)
OIDC_RP_SIGN_ALGO = values.Value(
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
)
OIDC_RP_CLIENT_ID = values.Value(
"people", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
)
OIDC_RP_CLIENT_SECRET = values.Value(
environ_name="OIDC_RP_CLIENT_SECRET",
environ_prefix=None,
)
# OIDC - OIDC Provider (OP)
OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT", environ_prefix=None
)
OIDC_OP_JWKS_ENDPOINT = values.Value(
environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
)
OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
)
OIDC_OP_TOKEN_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
)
OIDC_OP_USER_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
)
# TODO: refactor this settings, should be factorized with all endpoints
OIDC_OP_URL = values.Value(None, environ_name="OIDC_OP_URL", environ_prefix=None)
# OIDC - Resource Server (RS)
OIDC_RS_CLIENT_ID = values.Value(
"people", environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None
)
OIDC_RS_CLIENT_SECRET = values.Value(
environ_name="OIDC_RS_CLIENT_SECRET",
environ_prefix=None,
)
OIDC_RS_AUTH_SECRET = values.Value(
None, environ_name="OIDC_RS_AUTH_SECRET", environ_prefix=None
)
OIDC_RS_PRIVATE_KEY_STR = values.Value(
default=None,
environ_name="OIDC_RS_PRIVATE_KEY_STR",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_ALGO = values.Value(
default="RSA-OAEP",
environ_name="OIDC_RS_ENCRYPTION_ALGO",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
default="A256GCM",
environ_name="OIDC_RS_ENCRYPTION_ENCODING",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value(
default="RSA",
environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE",
environ_prefix=None,
)
OIDC_RS_SIGNING_ALG0 = values.Value(
default="ES256", environ_name="OIDC_RS_SIGNING_ALG0", environ_prefix=None
)
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
+1
View File
@@ -47,6 +47,7 @@ dependencies = [
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.1.18",
"PyJWT==2.8.0",
"joserfc==0.9.0",
"requests==2.31.0",
"sentry-sdk==1.45.0",
"url-normalize==1.4.3",