diff --git a/api/app/settings/common.py b/api/app/settings/common.py index 0eb5a9c742e4..562db80758e8 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -904,11 +904,15 @@ "ROTATE_REFRESH_TOKEN": True, "PKCE_REQUIRED": True, "ALLOWED_CODE_CHALLENGE_METHODS": ["S256"], - "SCOPES": {"mcp": "MCP access"}, + "SCOPES": { + "mcp": "MCP access", + "scim": "SCIM provisioning access", + }, "DEFAULT_SCOPES": ["mcp"], "ALLOWED_GRANT_TYPES": [ "authorization_code", "refresh_token", + "client_credentials", ], } diff --git a/api/oauth2_metadata/views.py b/api/oauth2_metadata/views.py index 15b45d12826d..4915ee8bed03 100644 --- a/api/oauth2_metadata/views.py +++ b/api/oauth2_metadata/views.py @@ -29,6 +29,10 @@ def authorization_server_metadata(request: HttpRequest) -> JsonResponse: frontend_url: str = settings.FLAGSMITH_FRONTEND_URL.rstrip("/") oauth2_settings: dict[str, Any] = settings.OAUTH2_PROVIDER scopes: dict[str, str] = oauth2_settings.get("SCOPES", {}) + allowed_grant_types: list[str] = oauth2_settings.get( + "ALLOWED_GRANT_TYPES", + ["authorization_code", "refresh_token"], + ) metadata = { "issuer": api_url, @@ -39,7 +43,7 @@ def authorization_server_metadata(request: HttpRequest) -> JsonResponse: "introspection_endpoint": f"{api_url}/o/introspect/", "scopes_supported": list(scopes.keys()), "response_types_supported": ["code"], - "grant_types_supported": ["authorization_code", "refresh_token"], + "grant_types_supported": allowed_grant_types, "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": [ "client_secret_basic", diff --git a/api/tests/unit/oauth2_metadata/test_views.py b/api/tests/unit/oauth2_metadata/test_views.py index 5f371446251b..d824aa3e098d 100644 --- a/api/tests/unit/oauth2_metadata/test_views.py +++ b/api/tests/unit/oauth2_metadata/test_views.py @@ -37,7 +37,7 @@ def test_metadata_endpoint__unauthenticated__returns_200_with_rfc8414_json( assert data["revocation_endpoint"] == "https://api.flagsmith.com/o/revoke_token/" assert data["introspection_endpoint"] == "https://api.flagsmith.com/o/introspect/" assert data["response_types_supported"] == ["code"] - assert data["grant_types_supported"] == ["authorization_code", "refresh_token"] + assert data["grant_types_supported"] == ["authorization_code", "refresh_token","client_credentials"] assert data["code_challenge_methods_supported"] == ["S256"] assert "none" in data["token_endpoint_auth_methods_supported"] assert data["introspection_endpoint_auth_methods_supported"] == ["none"] @@ -108,3 +108,54 @@ def test_metadata_endpoint__post_request__returns_405() -> None: # Then assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + + +def test_metadata_endpoint__grant_types__derived_from_allowed_grant_types_setting( + client: Client, + settings: SettingsWrapper, +) -> None: + # Given + settings.OAUTH2_PROVIDER = { + **settings.OAUTH2_PROVIDER, + "ALLOWED_GRANT_TYPES": ["authorization_code", "client_credentials"], + } + + # When + response = client.get(reverse(METADATA_URL)) + + # Then + data = response.json() + assert data["grant_types_supported"] == ["authorization_code", "client_credentials"] + + +def test_metadata_endpoint__grant_types__include_client_credentials_by_default( + client: Client, + settings: SettingsWrapper, +) -> None: + # Given + # Use real settings which now include client_credentials + settings.FLAGSMITH_API_URL = "https://api.flagsmith.com" + settings.FLAGSMITH_FRONTEND_URL = "https://app.flagsmith.com" + + # When + response = client.get(reverse(METADATA_URL)) + + # Then + data = response.json() + assert "client_credentials" in data["grant_types_supported"] + + +def test_metadata_endpoint__scim_scope__present_in_scopes_supported( + client: Client, + settings: SettingsWrapper, +) -> None: + # Given + settings.FLAGSMITH_API_URL = "https://api.flagsmith.com" + settings.FLAGSMITH_FRONTEND_URL = "https://app.flagsmith.com" + + # When + response = client.get(reverse(METADATA_URL)) + + # Then + data = response.json() + assert "scim" in data["scopes_supported"]