fix(iam): migrate userinfo/get-account to canonical /v1/iam/oauth/userinfo (HIP-0111) (#38)

Co-authored-by: Zach Kelling <[email protected]>
This commit is contained in:
2026-06-24 19:16:18 -07:00
committed by GitHub
parent 7489cdc308
commit e1a383635f
7 changed files with 32 additions and 14 deletions
+2 -1
View File
@@ -10,6 +10,7 @@ import httpx
import jwt import jwt
from hanzo_iam.models import ( from hanzo_iam.models import (
OIDC_USERINFO_PATH,
Application, Application,
JWTClaims, JWTClaims,
Organization, Organization,
@@ -415,7 +416,7 @@ class AsyncIAMClient:
""" """
http = await self._get_http() http = await self._get_http()
response = await http.get( response = await http.get(
"/api/userinfo", OIDC_USERINFO_PATH,
headers={"Authorization": f"Bearer {access_token}"}, headers={"Authorization": f"Bearer {access_token}"},
) )
response.raise_for_status() response.raise_for_status()
+3 -2
View File
@@ -15,6 +15,7 @@ import httpx
import jwt import jwt
from .models import ( from .models import (
OIDC_USERINFO_PATH,
Application, Application,
IAMConfig, IAMConfig,
JWTClaims, JWTClaims,
@@ -423,7 +424,7 @@ class IAMClient:
UserInfo with user profile data. UserInfo with user profile data.
""" """
response = self.http.get( response = self.http.get(
"/api/userinfo", OIDC_USERINFO_PATH,
headers={"Authorization": f"Bearer {access_token}"}, headers={"Authorization": f"Bearer {access_token}"},
) )
response.raise_for_status() response.raise_for_status()
@@ -1056,7 +1057,7 @@ class AsyncIAMClient:
"""Get user info from userinfo endpoint.""" """Get user info from userinfo endpoint."""
http = await self._get_http() http = await self._get_http()
response = await http.get( response = await http.get(
"/api/userinfo", OIDC_USERINFO_PATH,
headers={"Authorization": f"Bearer {access_token}"}, headers={"Authorization": f"Bearer {access_token}"},
) )
response.raise_for_status() response.raise_for_status()
+4 -2
View File
@@ -7,6 +7,8 @@ from typing import ClassVar
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from hanzo_iam.models import OIDC_USERINFO_PATH
class IAMConfig(BaseModel): class IAMConfig(BaseModel):
"""Configuration for Hanzo IAM client. """Configuration for Hanzo IAM client.
@@ -85,8 +87,8 @@ class IAMConfig(BaseModel):
@property @property
def userinfo_endpoint(self) -> str: def userinfo_endpoint(self) -> str:
"""OIDC UserInfo endpoint URL.""" """OIDC UserInfo endpoint URL (HIP-0111 canonical path)."""
return f"{self.server_url}/api/userinfo" return f"{self.server_url}{OIDC_USERINFO_PATH}"
@property @property
def api_base(self) -> str: def api_base(self) -> str:
+8 -2
View File
@@ -34,7 +34,13 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import PyJWKClient from jwt import PyJWKClient
from hanzo_iam.models import IAMConfig, JWTClaims, Organization, UserInfo from hanzo_iam.models import (
OIDC_USERINFO_PATH,
IAMConfig,
JWTClaims,
Organization,
UserInfo,
)
# Global state # Global state
_config: IAMConfig | None = None _config: IAMConfig | None = None
@@ -232,7 +238,7 @@ async def _fetch_user_info(token: str) -> UserInfo:
HTTPException: If request fails HTTPException: If request fails
""" """
config = get_config() config = get_config()
userinfo_url = f"{config.server_url}/api/userinfo" userinfo_url = f"{config.server_url}{OIDC_USERINFO_PATH}"
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
response = await client.get( response = await client.get(
+4
View File
@@ -8,6 +8,10 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
# Canonical OIDC UserInfo path (HIP-0111). One way; no legacy /api/ prefix,
# no Casdoor /api/get-account. Relative to the IAM server_url root.
OIDC_USERINFO_PATH = "/v1/iam/oauth/userinfo"
class Organization(str, Enum): class Organization(str, Enum):
"""Hanzo IAM organizations with their identity domains.""" """Hanzo IAM organizations with their identity domains."""
+7 -3
View File
@@ -201,13 +201,17 @@ def configure(url: str, client_id: str, client_secret: str, org: str):
os.environ["IAM_CLIENT_ID"] = client_id os.environ["IAM_CLIENT_ID"] = client_id
os.environ["IAM_CLIENT_SECRET"] = client_secret os.environ["IAM_CLIENT_SECRET"] = client_secret
os.environ["IAM_URL"] = url os.environ["IAM_URL"] = url
data = _iam_request("GET", "/api/get-account", auth_params=True) # HIP-0111: canonical OIDC userinfo. Returns flat standard claims
if data.get("status") != "error": # (sub/email/...) with no Casdoor {status,msg} envelope — a 200
# response is itself the success signal; any failure raises via
# raise_for_status in _iam_request.
data = _iam_request("GET", "/v1/iam/oauth/userinfo", auth_params=True)
if "error" not in data:
console.print("[green]✓[/green] Connected to IAM successfully") console.print("[green]✓[/green] Connected to IAM successfully")
else: else:
console.print( console.print(
"[yellow]⚠[/yellow] Connected but got error: " "[yellow]⚠[/yellow] Connected but got error: "
+ data.get("msg", "unknown") + data.get("error_description", data.get("error", "unknown"))
) )
except SystemExit: except SystemExit:
console.print( console.print(
+4 -4
View File
@@ -117,7 +117,7 @@ class IAMResource(SyncAPIResource):
) -> object: ) -> object:
"""Get the current authenticated account.""" """Get the current authenticated account."""
return self._get( return self._get(
"/iam/api/get-account", "/v1/iam/oauth/userinfo",
options=make_request_options( options=make_request_options(
extra_headers=extra_headers, extra_headers=extra_headers,
extra_query=extra_query, extra_query=extra_query,
@@ -137,7 +137,7 @@ class IAMResource(SyncAPIResource):
) -> object: ) -> object:
"""Get OIDC userinfo for the current session.""" """Get OIDC userinfo for the current session."""
return self._get( return self._get(
"/iam/api/userinfo", "/v1/iam/oauth/userinfo",
options=make_request_options( options=make_request_options(
extra_headers=extra_headers, extra_headers=extra_headers,
extra_query=extra_query, extra_query=extra_query,
@@ -1495,7 +1495,7 @@ class AsyncIAMResource(AsyncAPIResource):
) -> object: ) -> object:
"""Get the current authenticated account.""" """Get the current authenticated account."""
return await self._get( return await self._get(
"/iam/api/get-account", "/v1/iam/oauth/userinfo",
options=make_request_options( options=make_request_options(
extra_headers=extra_headers, extra_headers=extra_headers,
extra_query=extra_query, extra_query=extra_query,
@@ -1515,7 +1515,7 @@ class AsyncIAMResource(AsyncAPIResource):
) -> object: ) -> object:
"""Get OIDC userinfo for the current session.""" """Get OIDC userinfo for the current session."""
return await self._get( return await self._get(
"/iam/api/userinfo", "/v1/iam/oauth/userinfo",
options=make_request_options( options=make_request_options(
extra_headers=extra_headers, extra_headers=extra_headers,
extra_query=extra_query, extra_query=extra_query,