Skip to content

feat: add organization policy enforcement - #135

Open
kishore7snehil wants to merge 4 commits into
mainfrom
feat/org-policy-enforcement
Open

kishore7snehil wants to merge 4 commits into
mainfrom
feat/org-policy-enforcement

Conversation

@kishore7snehil

Copy link
Copy Markdown
Contributor

📋 Changes

This PR adds organization policy enforcement to auth0-api-python, letting an API require that incoming access tokens carry an org_id claim and optionally pin accepted tokens to a specific organization or allowlist.

✨ Features

  • Organization Policy: New organization_policy option on ApiClientOptions. "allow" accepts tokens with or without org_id and matches existing behavior. "required" rejects tokens without org_id.
  • Organization Pinning: New organization_id option that pins accepted tokens to a single organization or an allowlist. Valid only when the policy is "required".
  • Policy Enforcement: verify_access_token() now enforces the configured organization policy during verification.
  • Missing Organization Error: New MissingOrganizationError (subclasses VerifyAccessTokenError), raised when a token has no org_id and the policy is "required".
  • Organization Not Allowed Error: New OrganizationNotAllowedError (subclasses VerifyAccessTokenError), raised when a token's org_id is not in the organization_id allowlist.

🔧 API Changes

  • Added organization_policy (str, default "allow") to ApiClientOptions
  • Added organization_id (str or list of str, default None) to ApiClientOptions, valid only with the "required" policy
  • New errors: MissingOrganizationError, OrganizationNotAllowedError (both subclass VerifyAccessTokenError)
  • verify_access_token() now enforces the organization policy

📖 Documentation

  • Added docs/OrganizationPolicy.md describing the policy modes and the organization allowlist
  • Updated README.md with an organization policy section

🧪 Testing

  • This change adds test coverage
  • This change has been tested on the latest version of the platform/language

Contributor Checklist

Adds organization_policy ("allow"/"required") and organization_id
allowlist options. When required, verify_access_token now rejects
tokens missing org_id or carrying an org_id outside the allowlist.
Passing organization_id with policy "allow" raises ConfigurationError
at construction time.
Adds coverage for missing org_id under "required" policy, org_id
outside the allowlist, an allowed org_id succeeding, default "allow"
policy not requiring org_id, and the construction-time ConfigurationError
when organization_id is set without policy "required". Each test is
co-located with the existing tests for the surface it exercises.
@kishore7snehil
kishore7snehil force-pushed the feat/org-policy-enforcement branch from 19320d8 to 8f87351 Compare October 5, 2026 16:32
@kishore7snehil
kishore7snehil marked this pull request as ready for review October 6, 2026 06:41
@kishore7snehil
kishore7snehil requested a review from a team as a code owner October 6, 2026 06:41
return "invalid_token"


class MissingOrganizationError(VerifyAccessTokenError):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two new error classes are not added to __init__.py (__all__), so they do not get exported from the package.

But the docs show from auth0_api_python import MissingOrganizationError and OrganizationNotAllowedError, which will fail with ImportError.

Can we please export both from the package so the documented import works? The tests pass only because they import from the private errors submodule.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae99659.

Comment thread src/auth0_api_python/errors.py Outdated
"""Error raised when organization_policy is 'required' but the token has no org_id claim."""

def get_error_code(self) -> str:
return "missing_organization"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small concern on the error code here. missing_organization and organization_not_allowed (line 70) end up as the error value in the WWW-Authenticate header.

RFC 6750 registers only invalid_request, invalid_token and insufficient_scope for that field, and all our other verification failures already return invalid_token.

Can we keep invalid_token on the wire and put the org reason in the error_description? We can still keep these as separate classes for except handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae99659.

Comment thread src/auth0_api_python/api_client.py Outdated
raise ConfigurationError(
"organization_policy must be either 'required' or 'allow'"
)
if options.organization_id is not None and options.organization_policy != "required":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also validate the value of organization_id here itself?

Right now organization_id=[] or "" passes construction but then rejects every token later, and organization_id=123 passes here and then raises a raw TypeError at verify time which turns into a 500.

The domains option already does this kind of check at construction, so it will be good to stay consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae99659.

Comment thread src/auth0_api_python/api_client.py Outdated
raise VerifyAccessTokenError(f"Missing required claim: {rc}")

# Organization policy enforcement
org_id = claims.get("org_id")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

org_id is read on every call but it is used only in the required branch. Can we move this read inside the if so the default allow path stays clean?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae99659.

Comment thread src/auth0_api_python/api_client.py Outdated
allowed_orgs = [allowed_orgs]
if org_id not in allowed_orgs:
raise OrganizationNotAllowedError(
f"Organization '{org_id}' is not in the allowed list"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am nitpicking now :D

The token org_id goes into the error message and from there into the error_description of the WWW-Authenticate header as is. The value is read only after full verification so the risk is low, but can we keep a static text in the header and log the org_id separately?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae99659.

Comment thread src/auth0_api_python/config.py Outdated
client_id: Optional[str] = None,
client_secret: Optional[str] = None,
timeout: float = 10.0,
organization_policy: str = "allow",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since only required and allow are valid, can we type this as Literal["required", "allow"]?

That way a wrong value gets caught by the type checker and not only at runtime. The earlier shipped option also used a Literal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typed it as Literal["required", "allow"] in ae99659. No option on main uses Literal today, so this is the first one. It works on Python 3.9 since Literal is in typing from 3.8.

Comment thread .ruff.toml Outdated
Comment on lines +17 to +19
per-file-ignores = {
"tests/*" = ["S101", "S105", "S106"], # Allow assert and ignore hardcoded password warnings in test files
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This per-file-ignores is now an inline table spread across multiple lines with a trailing comma, which is not valid TOML 1.0 (strict parsers like tomllib reject it).

Current ruff still reads it so lint is passing, but it is a bit fragile across tools. Can we use the subtable form [lint.per-file-ignores] instead?

Also this ruff change looks unrelated to the feature, maybe it can go in a separate chore PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the .ruff.toml change from this PR. It now lives in #140, which uses the [lint] and [lint.per-file-ignores] tables.

Comment thread tests/test_api_client.py
assert "failed to parse token" in str(e.value).lower()


# ===== Organization Policy: verify_access_token Enforcement =====

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single string organization_id (all tests pass a list, so the str to list branch is never hit), required policy with no allowlist but a token that has an org_id (should pass), allow policy with a token that does carry an org_id, and rejection of an invalid organization_policy value like none at construction.

Can we add these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae99659.

@kishore7snehil
kishore7snehil force-pushed the feat/org-policy-enforcement branch from 8f87351 to f4b287d Compare October 8, 2026 04:07
Comment thread tests/test_api_client.py Fixed
…_token

- Export MissingOrganizationError and OrganizationNotAllowedError from the package root
- Keep invalid_token as the error code for organization failures
- Validate organization_id at construction and normalize it once
- Reject a non-string org_id claim and read it only under the required policy
- Keep the org_id out of the error message and log it instead
- Type organization_policy as Literal["required", "allow"]
- Add tests for the single-string allowlist, no allowlist, allow policy, invalid values and the response header
@kishore7snehil
kishore7snehil force-pushed the feat/org-policy-enforcement branch from f4b287d to ae99659 Compare October 8, 2026 04:22

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants