mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-18 06:23:21 +00:00
✨(backend) introduce a token exchange endpoint for iframe embeds
Some integrators render our videoconference inside an iframe, where our cookie-based authentication does not work: our cookies are SameSite=Lax/Strict, so the iframe drops them. We looked at what Jitsi offers: a shared secret used to sign JWTs that authenticate users coming from external services. Since we already expose an external API where third parties authenticate as a given user, it was simpler for us to add an exchange mechanism on top of that. Flow: * Through the external API, mint a short-lived, single-use exchange code for a user. * The third party hands that code to the frontend as a URL fragment. * The frontend exchanges the code for a longer-lived JWT that can be used to query the regular API viewsets. Known limitations and follow-ups: * At some point it would be nice to shorten the JWT lifetime and add a refresh mechanism. This will be handled in a follow-up PR when actually needed. * CSP rules to control which origins are allowed to embed the app in an iframe still need to be added. * This alternative authentication cannot easily be scoped to a subset of endpoints without adding a lot of complexity, so it is accepted globally on the API for now.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""User access JWT authentication for the Meet core API.
|
||||
|
||||
Allows an embedded frontend (e.g. rendered in an iframe, where third-party
|
||||
session cookies are blocked) to authenticate requests on the core API with
|
||||
a JWT, obtained by exchanging a single-use transit code (see
|
||||
core.services.transit_code and the users exchange-access-token endpoint)
|
||||
and passed as a Bearer header. The JWT itself never appears in any URL.
|
||||
|
||||
Similar to lib-jitsi-meet's token authentication, the token is bound to a
|
||||
user, not to a resource: once authenticated, the request is treated
|
||||
exactly like a session-authenticated one, and the existing role-based
|
||||
permissions apply unchanged.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import exceptions
|
||||
|
||||
from core.external_api.authentication import BaseJWTAuthentication
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USER_ACCESS_TOKEN_TYPE_CLAIM = "user_access" # noqa: S105
|
||||
|
||||
|
||||
class UserAccessJWTAuthentication(BaseJWTAuthentication):
|
||||
"""JWT authentication for user access tokens.
|
||||
|
||||
Validates user access tokens issued by the users exchange-access-token
|
||||
endpoint and authenticates the user they were issued for. A bearer
|
||||
token that does not verify against the user access token secret is
|
||||
deferred to the next authentication backend; a token that does verify
|
||||
but carries wrong claims is rejected.
|
||||
|
||||
When the feature is disabled (USER_ACCESS_TOKEN_ENABLED=False), the
|
||||
backend is entirely inert: `BaseJWTAuthentication.authenticate`
|
||||
returns None before reading the Authorization header, deferring every
|
||||
request to the next authentication backend.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the backend with user access token settings."""
|
||||
super().__init__(
|
||||
secret_key=settings.USER_ACCESS_TOKEN_SECRET_KEY,
|
||||
algorithm=settings.USER_ACCESS_TOKEN_ALG,
|
||||
issuer=settings.USER_ACCESS_TOKEN_ISSUER,
|
||||
audience=settings.USER_ACCESS_TOKEN_AUDIENCE,
|
||||
expiration_seconds=settings.USER_ACCESS_TOKEN_TTL,
|
||||
token_type=settings.USER_ACCESS_TOKEN_TYPE,
|
||||
is_enabled=settings.USER_ACCESS_TOKEN_ENABLED,
|
||||
)
|
||||
|
||||
def validate_payload(self, payload):
|
||||
"""Validate the token type and the issuance-audit claim.
|
||||
|
||||
Raises:
|
||||
AuthenticationFailed: If the token verified against the user
|
||||
access token secret but does not carry the expected claims.
|
||||
"""
|
||||
if payload.get("token_type") != USER_ACCESS_TOKEN_TYPE_CLAIM:
|
||||
logger.warning("Wrong 'token_type' in user access token payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token type.")
|
||||
|
||||
if not payload.get("client_id"):
|
||||
logger.warning("Missing 'client_id' in user access token payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token claims.")
|
||||
Reference in New Issue
Block a user