Skip to content

Commit 07469a3

Browse files
authored
feat(api): support azuread token v2 (keephq#3659)
1 parent e10ba35 commit 07469a3

File tree

2 files changed

+46
-15
lines changed

2 files changed

+46
-15
lines changed

ee/identitymanager/identity_managers/azuread/azuread_authverifier.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def _verify_bearer_token(
157157
self, token: str = Depends(oauth2_scheme)
158158
) -> AuthenticatedEntity:
159159
"""Verify the Azure AD JWT token and extract claims"""
160+
160161
try:
161162
# First decode without verification to get the key id (kid)
162163
unverified_headers = jwt.get_unverified_header(token)
@@ -170,15 +171,17 @@ def _verify_bearer_token(
170171
if not signing_key:
171172
raise HTTPException(status_code=401, detail="Invalid token signing key")
172173

173-
# Verify and decode the token
174+
# For v2.0 tokens, 'appid' doesn't exist — 'azp' is used instead.
175+
# Remove "appid" from the 'require' list so v2 tokens won't fail.
174176
options = {
175177
"verify_signature": True,
176-
"verify_aud": False, # We'll validate manually
178+
"verify_aud": False, # We'll validate manually below
177179
"verify_iat": True,
178180
"verify_exp": True,
179181
"verify_nbf": True,
180182
"verify_iss": True,
181-
"require": ["exp", "iat", "nbf", "iss", "sub", "appid"],
183+
# "require" the standard claims but NOT "appid"
184+
"require": ["exp", "iat", "nbf", "iss", "sub"],
182185
}
183186

184187
try:
@@ -190,13 +193,25 @@ def _verify_bearer_token(
190193
options=options,
191194
)
192195

193-
# Validate the appid claim instead of audience
194-
if payload.get("appid") != self.client_id:
196+
# In v1.0 tokens, client_id => 'appid'
197+
# In v2.0 tokens, client_id => 'azp'
198+
# We consider either one valid, so long as it matches self.client_id
199+
client_id_in_token = payload.get("appid") or payload.get("azp")
200+
201+
if not client_id_in_token:
195202
raise HTTPException(
196-
status_code=401, detail="Invalid token application ID"
203+
status_code=401, detail="No client ID (appid/azp) in token"
197204
)
198-
# validate aud
199-
if payload.get("aud") != f"api://{self.client_id}":
205+
206+
if client_id_in_token != self.client_id:
207+
raise HTTPException(
208+
status_code=401,
209+
detail="Invalid token application ID (appid/azp)",
210+
)
211+
212+
# Validate the audience
213+
expected_aud = f"api://{self.client_id}"
214+
if payload.get("aud") != expected_aud:
200215
raise HTTPException(
201216
status_code=401, detail="Invalid token audience"
202217
)
@@ -234,20 +249,34 @@ def _verify_bearer_token(
234249
# Map groups to role
235250
role_name = self.group_mapper.get_role_from_groups(groups)
236251
if not role_name:
252+
self.logger.warning(
253+
f"User {email} is not a member of any authorized groups for Keep",
254+
extra={
255+
"tenant_id": tenant_id,
256+
"groups": groups,
257+
},
258+
)
237259
raise HTTPException(
238260
status_code=403,
239-
detail="You are using Azure AD but the user is not a member of any authorized groups. You need to be a member of an authorized group to access Keep.",
261+
detail="User not a member of any authorized groups for Keep",
240262
)
241263

242-
# Validate role has required scopes
264+
# Validate role scopes
243265
role = get_role_by_role_name(role_name)
244266
if not role.has_scopes(self.scopes):
267+
self.logger.warning(
268+
f"Role {role_name} does not have required permissions",
269+
extra={
270+
"tenant_id": tenant_id,
271+
"role": role_name,
272+
},
273+
)
245274
raise HTTPException(
246275
status_code=403,
247276
detail=f"Role {role_name} does not have required permissions",
248277
)
249278

250-
# Auto-provision so we can list users
279+
# Auto-provisioning logic
251280
hashed_token = hashlib.sha256(token.encode()).hexdigest()
252281
if hashed_token not in self.saw_tokens and not user_exists(
253282
tenant_id, email
@@ -258,14 +287,16 @@ def _verify_bearer_token(
258287

259288
if hashed_token not in self.saw_tokens:
260289
update_user_last_sign_in(tenant_id, email)
261-
# Add token to seen tokens
262290
self.saw_tokens.add(hashed_token)
291+
263292
return AuthenticatedEntity(tenant_id, email, None, role_name)
264293

265294
except HTTPException:
295+
# Re-raise known HTTP errors
296+
self.logger.exception("Token validation failed (HTTPException)")
266297
raise
267-
except Exception as e:
268-
logger.error(f"Token validation failed: {str(e)}")
298+
except Exception:
299+
self.logger.exception("Token validation failed")
269300
raise HTTPException(status_code=401, detail="Invalid token")
270301

271302
def _authorize(self, authenticated_entity: AuthenticatedEntity) -> None:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "keep"
3-
version = "0.37.6"
3+
version = "0.37.7"
44
description = "Alerting. for developers, by developers."
55
authors = ["Keep Alerting LTD"]
66
packages = [{include = "keep"}]

0 commit comments

Comments
 (0)