From fae208bcf7e82ab1ec893c916af34e0b9a4b38d1 Mon Sep 17 00:00:00 2001 From: Shekar V Date: Tue, 5 May 2026 14:48:59 -0400 Subject: [PATCH 1/2] Added EntraInternalAuthenticator Wrapper around OIDCAuthenticator that extracts username from Entra token --- tiled/authenticators.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tiled/authenticators.py b/tiled/authenticators.py index 3fac1c1fa..96027229e 100644 --- a/tiled/authenticators.py +++ b/tiled/authenticators.py @@ -255,6 +255,34 @@ async def authenticate(self, request: Request) -> Optional[UserSessionState]: return UserSessionState(verified_body["sub"], {}) +class EntraInternalAuthenticator(OIDCAuthenticator): + def decode_token( + self, id_token: str, access_token: Optional[str] = None + ) -> dict[str, Any]: + claims = super().decode_token(id_token, access_token) + + claims["entra_username"] = ( + claims.get("nameID") + or claims.get("preferred_username") + or claims.get("upn") + or claims.get("email") + ) + + # Make a copy of the entra app user id + # in case its needed later + claims["entra_userid"] = claims.get("user") + + if user := claims.get("entra_username"): + user = user.strip() + if "\\" in user: + user = user.rsplit("\\", 1)[-1] + elif "@" in user: + user = user.split("@", 1)[0] + claims["user"] = user + + return claims + + class ProxiedOIDCAuthenticator(OIDCAuthenticator): configuration_schema = """ $schema": http://json-schema.org/draft-07/schema# From b13e08c80618752c81da7aba1a9b6f70aa4e6cea Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Tue, 5 May 2026 15:43:04 -0400 Subject: [PATCH 2/2] TEMP: Hack to get things working for testing --- tiled/authenticators.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tiled/authenticators.py b/tiled/authenticators.py index 96027229e..0253ec4ff 100644 --- a/tiled/authenticators.py +++ b/tiled/authenticators.py @@ -282,6 +282,44 @@ def decode_token( return claims + async def authenticate(self, request: Request) -> Optional[UserSessionState]: + code = request.query_params.get("code") + if not code: + logger.warning( + "Authentication failed: No authorization code parameter provided." + ) + return None + # A proxy in the middle may make the request into something like + # 'http://localhost:8000/...' so we fix the first part but keep + # the original URI path. + redirect_uri = f"{get_root_url(request)}{request.url.path}" + response = await exchange_code( + self.token_endpoint, + code, + self._client_id, + self._client_secret.get_secret_value(), + redirect_uri, + ) + response_body = response.json() + if response.is_error: + logger.error("Authentication error: %r", response_body) + return None + response_body = response.json() + id_token = response_body["id_token"] + access_token = response_body["access_token"] + try: + verified_body = self.decode_token(id_token, access_token) + except JWTError: + logger.exception( + "Authentication error. Unverified token: %r", + jwt.get_unverified_claims(id_token), + ) + return None + # HACK! + # We should probably capture both "sub" and "user" (meaning display name) + # in UserSessionState, not override sub here. + return UserSessionState(verified_body["user"], {}) + class ProxiedOIDCAuthenticator(OIDCAuthenticator): configuration_schema = """