|
18 | 18 | Verifier, |
19 | 19 | auth0_ai_agents, |
20 | 20 | entra_agent_id, |
| 21 | + workos, |
21 | 22 | ) |
22 | 23 |
|
23 | 24 | # ---------- Entra Agent ID profile ------------------------------------------- |
@@ -227,6 +228,115 @@ def patched_init( |
227 | 228 | assert result.agent_id == "agent-bot42" |
228 | 229 |
|
229 | 230 |
|
| 231 | +# ---------- WorkOS profile --------------------------------------------------- |
| 232 | + |
| 233 | + |
| 234 | +def test_workos_returns_obo_adapter() -> None: |
| 235 | + adapter = workos( |
| 236 | + client_id="client_01HXYZ1234567890", |
| 237 | + audience="https://api.example.com", |
| 238 | + ) |
| 239 | + assert isinstance(adapter, OAuthOBOAdapter) |
| 240 | + assert adapter.name == "oauth-obo" |
| 241 | + |
| 242 | + |
| 243 | +def test_workos_issuer_url_pattern() -> None: |
| 244 | + adapter = workos(client_id="client_01HXYZ", audience="aud") |
| 245 | + assert ( |
| 246 | + adapter.expected_issuer |
| 247 | + == "https://api.workos.com/user_management/client_01HXYZ" |
| 248 | + ) |
| 249 | + |
| 250 | + |
| 251 | +def test_workos_jwks_url_uses_sso_path_not_user_management() -> None: |
| 252 | + """WorkOS quirk: JWKS lives under /sso/jwks/ even for AuthKit tokens.""" |
| 253 | + adapter = workos(client_id="client_01HXYZ", audience="aud") |
| 254 | + # We can't directly read the URL without exposing internal state, |
| 255 | + # but the JWKSKeyProvider was constructed — verifying the type |
| 256 | + # is sufficient; the end-to-end test below proves the URL. |
| 257 | + assert isinstance(adapter.key_provider, JWKSKeyProvider) |
| 258 | + |
| 259 | + |
| 260 | +def test_workos_custom_domain() -> None: |
| 261 | + adapter = workos( |
| 262 | + client_id="client_x", |
| 263 | + audience="aud", |
| 264 | + domain="auth.example.com", |
| 265 | + ) |
| 266 | + assert ( |
| 267 | + adapter.expected_issuer |
| 268 | + == "https://auth.example.com/user_management/client_x" |
| 269 | + ) |
| 270 | + |
| 271 | + |
| 272 | +def test_workos_strips_protocol_from_custom_domain() -> None: |
| 273 | + adapter = workos( |
| 274 | + client_id="client_x", |
| 275 | + audience="aud", |
| 276 | + domain="https://auth.example.com/", |
| 277 | + ) |
| 278 | + assert ( |
| 279 | + adapter.expected_issuer |
| 280 | + == "https://auth.example.com/user_management/client_x" |
| 281 | + ) |
| 282 | + |
| 283 | + |
| 284 | +def test_workos_requires_client_id() -> None: |
| 285 | + with pytest.raises(ValueError, match="client_id"): |
| 286 | + workos(client_id="", audience="aud") |
| 287 | + |
| 288 | + |
| 289 | +def test_workos_requires_domain() -> None: |
| 290 | + with pytest.raises(ValueError, match="domain"): |
| 291 | + workos(client_id="client_x", audience="aud", domain="") |
| 292 | + |
| 293 | + |
| 294 | +async def test_workos_end_to_end_with_mocked_jwks( |
| 295 | + rsa_keypair: tuple[RSAPrivateKey, Any], monkeypatch: pytest.MonkeyPatch |
| 296 | +) -> None: |
| 297 | + """A token issued under WorkOS's iss / aud / kid pattern verifies cleanly.""" |
| 298 | + private_key, public_key = rsa_keypair |
| 299 | + client_id = "client_01HXYZ1234567890" |
| 300 | + |
| 301 | + mock_client = _serve_jwks_via_mock(public_key, kid="workos-key-1") |
| 302 | + original_init = JWKSKeyProvider.__init__ |
| 303 | + |
| 304 | + def patched_init( |
| 305 | + self: JWKSKeyProvider, jwks_url: str, **kwargs: Any |
| 306 | + ) -> None: |
| 307 | + # Sanity check: WorkOS JWKS URL must use the sso/jwks path, |
| 308 | + # not user_management/jwks. |
| 309 | + assert "/sso/jwks/" in jwks_url, jwks_url |
| 310 | + assert client_id in jwks_url |
| 311 | + kwargs["http_client"] = mock_client |
| 312 | + original_init(self, jwks_url, **kwargs) |
| 313 | + |
| 314 | + monkeypatch.setattr(JWKSKeyProvider, "__init__", patched_init) |
| 315 | + |
| 316 | + adapter = workos(client_id=client_id, audience="https://api.myapp.com") |
| 317 | + |
| 318 | + now = int(time.time()) |
| 319 | + token = jwt.encode( |
| 320 | + { |
| 321 | + "iss": f"https://api.workos.com/user_management/{client_id}", |
| 322 | + "aud": "https://api.myapp.com", |
| 323 | + "sub": "user_01HABC", |
| 324 | + "act": {"sub": "agent_workos_bot"}, |
| 325 | + "iat": now, |
| 326 | + "exp": now + 3600, |
| 327 | + "scope": "read:user", |
| 328 | + }, |
| 329 | + private_key, |
| 330 | + algorithm="RS256", |
| 331 | + headers={"kid": "workos-key-1"}, |
| 332 | + ) |
| 333 | + |
| 334 | + verifier = Verifier(adapters=[adapter]) |
| 335 | + result = await verifier.verify(token) |
| 336 | + assert result.principal_id == "user_01HABC" |
| 337 | + assert result.agent_id == "agent_workos_bot" |
| 338 | + |
| 339 | + |
230 | 340 | async def test_auth0_rejects_token_missing_trailing_slash_in_iss( |
231 | 341 | rsa_keypair: tuple[RSAPrivateKey, Any], monkeypatch: pytest.MonkeyPatch |
232 | 342 | ) -> None: |
|
0 commit comments