Skip to content

Commit

Permalink
improved entraid auth (requires xlwings 0.31.3)
Browse files Browse the repository at this point in the history
  • Loading branch information
fzumstein committed May 23, 2024
1 parent dc0febb commit 36d72ca
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 30 deletions.
43 changes: 27 additions & 16 deletions app/auth/entraid.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import re
from typing import List, Optional

import httpx
import jwt
from cachetools import TTLCache, cached
from aiocache import Cache, cached
from fastapi import Depends, Header, status
from fastapi.exceptions import HTTPException
from pydantic import BaseModel
Expand All @@ -12,10 +13,20 @@

logger = logging.getLogger(__name__)

OPENID_CONNECT_DISCOVERY_DOCUMENT_URL = (
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"
)

# See https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
jwks_uri = "https://login.microsoftonline.com/common/discovery/v2.0/keys"
jwks_client = jwt.PyJWKClient(jwks_uri)

@cached(ttl=60 * 60, cache=Cache.MEMORY)
async def get_jwks_client_and_algorithms():
async with httpx.AsyncClient() as client:
response = await client.get(OPENID_CONNECT_DISCOVERY_DOCUMENT_URL)
data = response.json()
jwks_uri = data["jwks_uri"]
algorithms = data["id_token_signing_alg_values_supported"]
jwks_client = jwt.PyJWKClient(jwks_uri) # TODO: Makes a blocking request
return jwks_client, algorithms


class User(BaseModel):
Expand All @@ -25,8 +36,8 @@ class User(BaseModel):
roles: Optional[List[str]] = []


@cached(cache=TTLCache(maxsize=1024, ttl=60 * 60))
def validate_token(token: str):
@cached(ttl=60 * 60, cache=Cache.MEMORY)
async def validate_token(token: str):
"""Function that reads and validates the Entra ID access/id token.
Returns a user object."""
if not settings.entraid_tenant_id and not settings.entraid_client_id:
Expand All @@ -35,7 +46,7 @@ def validate_token(token: str):
if token.lower().startswith("error"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Auth error with Entra ID token: {token}",
detail=f"Auth error with Entra ID token: {token} See https://learn.microsoft.com/en-us/office/dev/add-ins/develop/troubleshoot-sso-in-office-add-ins#causes-and-handling-of-errors-from-getaccesstoken",
)
if token.lower().startswith("bearer"):
parts = token.split()
Expand All @@ -50,7 +61,7 @@ def validate_token(token: str):
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Auth error: Invalid token, must be in format 'Bearer xxxx'",
)

jwks_client, algorithms = await get_jwks_client_and_algorithms()
key = jwks_client.get_signing_key_from_jwt(token)
token_version = jwt.decode(token, options={"verify_signature": False}).get("ver")
# https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#token-formats
Expand All @@ -70,17 +81,17 @@ def validate_token(token: str):
try:
if settings.entraid_validate_issuer:
claims = jwt.decode(
token,
key.key,
algorithms=["RS256"],
jwt=token,
key=key.key,
algorithms=algorithms,
audience=audience,
issuer=issuer,
)
else:
claims = jwt.decode(
token,
key.key,
algorithms=["RS256"],
jwt=token,
key=key.key,
algorithms=algorithms,
audience=audience,
)
# External users have their own tenant_id
Expand Down Expand Up @@ -124,9 +135,9 @@ def authorize(user: User, roles: list = None):
return user


def authenticate(token: str = Header(default="", alias="Authorization")):
async def authenticate(token: str = Header(default="", alias="Authorization")):
"""Dependency, returns a user object"""
return validate_token(token)
return await validate_token(token)


class Authorizer:
Expand Down
6 changes: 3 additions & 3 deletions app/routers/socketio.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import xlwings as xw

from .. import custom_functions
from ..auth.entraid import authenticate
from ..auth.entraid import validate_token
from ..config import settings

logger = logging.getLogger(__name__)
Expand All @@ -21,14 +21,14 @@

@sio.on("connect")
async def connect(sid, environ, auth):
if settings.environment == "development":
if settings.environment == "dev":
from .. import hotreload

logging.getLogger("watchfiles").setLevel(logging.ERROR)
await hotreload.start_browser_reload_watcher(
sio=sio, directory=settings.base_dir
)
await xw.server.sio_connect(sid, environ, auth, sio, authenticate)
await xw.server.sio_connect(sid, environ, auth, sio, authenticate=validate_token)


@sio.on("disconnect")
Expand Down
4 changes: 2 additions & 2 deletions requirements-win.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in -o requirements-win.txt --unsafe-package pywin32 --python-platform windows
aiocache==0.12.2
# via -r requirements.in
annotated-types==0.7.0
# via pydantic
anyio==4.3.0
Expand All @@ -9,8 +11,6 @@ anyio==4.3.0
# watchfiles
bidict==0.23.1
# via python-socketio
cachetools==5.3.3
# via -r requirements.in
certifi==2024.2.2
# via
# httpcore
Expand Down
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
aiocache
azure-functions; sys_platform != 'win32'
cachetools
fastapi
gunicorn; sys_platform != 'win32'
httptools
Expand Down
41 changes: 33 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in -o requirements.txt --unsafe-package appscript --unsafe-package psutil
aiocache==0.12.2
# via -r requirements.in
annotated-types==0.7.0
# via pydantic
anyio==4.3.0
Expand All @@ -8,9 +10,9 @@ anyio==4.3.0
# starlette
# watchfiles
azure-functions==1.19.0
# via -r requirements.in
bidict==0.23.1
# via python-socketio
cachetools==5.3.3
certifi==2024.2.2
# via
# httpcore
Expand All @@ -28,9 +30,11 @@ dnspython==2.6.1
email-validator==2.1.1
# via fastapi
fastapi==0.111.0
# via -r requirements.in
fastapi-cli==0.0.4
# via fastapi
gunicorn==22.0.0
# via -r requirements.in
h11==0.14.0
# via
# httpcore
Expand All @@ -39,19 +43,25 @@ h11==0.14.0
httpcore==1.0.5
# via httpx
httptools==0.6.1
# via uvicorn
# via
# -r requirements.in
# uvicorn
httpx==0.27.0
# via fastapi
# via
# -r requirements.in
# fastapi
idna==3.7
# via
# anyio
# email-validator
# httpx
jinja2==3.1.4
# via
# -r requirements.in
# fastapi
# jinja2-fragments
jinja2-fragments==1.3.0
# via -r requirements.in
lxml==5.2.2
# via appscript
markdown-it-py==3.0.0
Expand All @@ -63,10 +73,13 @@ mdurl==0.1.2
numpy==1.26.4
# via pandas
orjson==3.10.3
# via fastapi
# via
# -r requirements.in
# fastapi
packaging==24.0
# via gunicorn
pandas==2.2.2
# via -r requirements.in
pycparser==2.22
# via cffi
pydantic==2.7.1
Expand All @@ -76,9 +89,11 @@ pydantic==2.7.1
pydantic-core==2.18.2
# via pydantic
pydantic-settings==2.2.1
# via -r requirements.in
pygments==2.18.0
# via rich
pyjwt==2.8.0
# via -r requirements.in
python-dateutil==2.9.0.post0
# via pandas
python-dotenv==1.0.1
Expand All @@ -88,8 +103,11 @@ python-dotenv==1.0.1
python-engineio==4.9.1
# via python-socketio
python-multipart==0.0.9
# via fastapi
# via
# -r requirements.in
# fastapi
python-socketio==5.11.2
# via -r requirements.in
pytz==2024.1
# via pandas
pyyaml==6.0.1
Expand Down Expand Up @@ -121,16 +139,23 @@ tzdata==2024.1
ujson==5.10.0
# via fastapi
uvicorn==0.29.0
# via fastapi
# via
# -r requirements.in
# fastapi
uvloop==0.19.0
# via uvicorn
# via
# -r requirements.in
# uvicorn
watchfiles==0.21.0
# via uvicorn
# via
# -r requirements.in
# uvicorn
websockets==12.0
# via uvicorn
wsproto==1.2.0
# via simple-websocket
xlwings==0.31.2
# via -r requirements.in

# The following packages were excluded from the output:
# appscript
Expand Down

0 comments on commit 36d72ca

Please sign in to comment.