Compare commits
13 Commits
main
...
al-data-provider
| Author | SHA1 | Date | |
|---|---|---|---|
| e502368c8b | |||
| e4da9b23cd | |||
| cc157904d0 | |||
| b06e30f436 | |||
| d124b57d16 | |||
| bc182fe313 | |||
| be364d9227 | |||
| 681065dd85 | |||
| a1065e55e0 | |||
| 6b15954d05 | |||
| 23cf6c31f7 | |||
| 5b2988491a | |||
| 669633285c |
@@ -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,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-----
|
||||
"
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user