Skip to content

Commit 1d2358f

Browse files
committed
status, README
1 parent 8eaedca commit 1d2358f

4 files changed

Lines changed: 58 additions & 15 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ OBP_OIDC_ISSUER_URL=http://localhost:9000/obp-oidc
188188
OBP_AUTHORIZATION_VIA="consent"
189189
OBP_OPEY_CONSUMER_KEY=<opey's consumer key (same as the OBP_CONSUMER_KEY in Opey)>
190190
```
191+
192+
> [!IMPORTANT] `OBP_AUTHORIZATION_VIA` must be set to `oauth` or `consent`. Any other value (including unset) causes the server to refuse to start — unauthenticated OBP-API access is not supported.
193+
191194
> [!NOTE] You will not be able to use the MCP server, set up in this way, with any other MCP clients, unless they are capable of creating valid consents on the fly.
192195
193196
In Opey's `mcp_servers.json` (inside the servers array):

src/mcp_server_obp/lifespan.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@
88

99
logger = logging.getLogger(__name__)
1010

11+
_VALID_OUTBOUND_AUTH = {"oauth", "consent"}
12+
13+
14+
def _validate_outbound_auth() -> None:
15+
"""Fail loudly at startup if OBP_AUTHORIZATION_VIA isn't oauth or consent.
16+
17+
The previous 'none' mode let call_obp_api fire unauthenticated requests
18+
against OBP-API, which severed the link between a chat session and an
19+
identified OBP user. Requiring an explicit mode here makes that
20+
impossible by construction.
21+
"""
22+
value = os.getenv("OBP_AUTHORIZATION_VIA", "").lower()
23+
if value not in _VALID_OUTBOUND_AUTH:
24+
raise RuntimeError(
25+
f"OBP_AUTHORIZATION_VIA must be one of {sorted(_VALID_OUTBOUND_AUTH)}; "
26+
f"got {value!r}. Refusing to start — unauthenticated OBP-API access is "
27+
"no longer supported."
28+
)
29+
1130

1231
def print_client_configs():
1332
"""Print copy-pasteable MCP client configuration snippets."""
@@ -90,6 +109,7 @@ async def lifespan(server) -> AsyncIterator[dict[str, Any]]:
90109
"""
91110
# Startup actions
92111
logger.info("Starting MCP server...")
112+
_validate_outbound_auth()
93113
print_client_configs()
94114

95115
# Check and update index on startup

src/mcp_server_obp/server.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ async def call_obp_api(
276276
method = endpoint.method # HttpMethod enum value
277277
request_headers = headers or {}
278278

279-
auth_method = os.getenv("OBP_AUTHORIZATION_VIA", "none").lower()
279+
auth_method = os.getenv("OBP_AUTHORIZATION_VIA", "").lower()
280280
match auth_method:
281281
case "oauth":
282282
logger.info("Using OAuth authorization method for OBP API call")
@@ -308,13 +308,26 @@ async def call_obp_api(
308308
if body_role not in required_roles:
309309
required_roles.append(body_role)
310310

311-
# If the endpoint requires no roles at all, treat it as public and skip
312-
# the consent prompt. OBP will reject the call if it actually needs auth,
313-
# which is a better UX than a meaningless "grant consent" dialog for e.g.
314-
# GET /root.
315-
if not required_roles:
311+
# Detect endpoints that need consent even without explicit role
312+
# requirements:
313+
# * account/view-scoped: path contains ACCOUNT_ID or VIEW_ID — gated by
314+
# account-access-to-a-view in OBP.
315+
# * user-scoped: path contains the `/my/` segment — identity-bound, the
316+
# response depends on which user is calling so we still need a
317+
# Consent-JWT for the user.
318+
path_segments = (endpoint.path or "").split("/")
319+
requires_view_access = "ACCOUNT_ID" in path_segments or "VIEW_ID" in path_segments
320+
is_user_scoped = "my" in path_segments
321+
account_id = (path_params or {}).get("ACCOUNT_ID") or (path_params or {}).get("account_id")
322+
view_id = (path_params or {}).get("VIEW_ID") or (path_params or {}).get("view_id")
323+
324+
# Endpoints with no role requirements AND no account/view/user scoping are
325+
# treated as public — skip the consent prompt (e.g. GET /root). OBP will
326+
# reject the call if it actually needs auth, which is a better UX than a
327+
# meaningless "grant consent" dialog.
328+
if not required_roles and not requires_view_access and not is_user_scoped:
316329
logger.info(
317-
f"Skipping consent for {endpoint.operation_id}: endpoint has no required roles"
330+
f"Skipping consent for {endpoint.operation_id}: no required roles, not account/view-scoped, not user-scoped"
318331
)
319332
else:
320333
return json.dumps({
@@ -325,6 +338,10 @@ async def call_obp_api(
325338
"path": endpoint.path,
326339
"required_roles": required_roles,
327340
"bank_id": bank_id,
341+
"account_id": account_id,
342+
"view_id": view_id,
343+
"requires_view_access": requires_view_access,
344+
"is_user_scoped": is_user_scoped,
328345
"message": f"User consent is required to call {endpoint.operation_id}. "
329346
f"Please approve and provide a Consent-JWT.",
330347
}, indent=2)
@@ -337,14 +354,14 @@ async def call_obp_api(
337354
else:
338355
logger.warning("OBP_OPEY_CONSUMER_KEY is not set in environment variables. Consent-based authorization will fail without it.")
339356

340-
case "none":
341-
# No authorization
342-
logger.info("No authorization method used for OBP API call")
343-
pass
344357
case _:
345-
logger.error(f"Invalid OBP_AUTHORIZATION_VIA value: {auth_method}")
358+
logger.error(
359+
f"Invalid OBP_AUTHORIZATION_VIA value: {auth_method!r}. "
360+
"Must be 'oauth' or 'consent'."
361+
)
346362
return json.dumps({
347-
"error": f"Internal server error."
363+
"error": "OBP-API calls are disabled: OBP_AUTHORIZATION_VIA "
364+
"must be set to 'oauth' or 'consent'."
348365
}, indent=2)
349366

350367
# Make the request

src/mcp_server_obp/status.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async def build_status() -> dict[str, Any]:
6969
obp_api_version = os.getenv("OBP_API_VERSION", "")
7070
auth_enabled = os.getenv("ENABLE_OAUTH", "false").lower() == "true"
7171
auth_provider = os.getenv("AUTH_PROVIDER", "none") if auth_enabled else "none"
72+
outbound_auth_via = os.getenv("OBP_AUTHORIZATION_VIA", "").lower() or None
7273

7374
issuers: list[dict[str, Any]] = []
7475
if auth_enabled:
@@ -130,6 +131,7 @@ async def build_status() -> dict[str, Any]:
130131
"auth": {
131132
"enabled": auth_enabled,
132133
"provider": auth_provider,
134+
"outbound_auth_via": outbound_auth_via,
133135
"issuers": issuers,
134136
},
135137
"index": {
@@ -269,8 +271,9 @@ def row(label: str, value: Any) -> str:
269271
<section>
270272
<h2>Authentication</h2>
271273
<table>
272-
{row("OAuth enabled", auth.get("enabled"))}
273-
{row("Provider", auth.get("provider"))}
274+
{row("OAuth enabled (inbound)", auth.get("enabled"))}
275+
{row("Inbound provider", auth.get("provider"))}
276+
{row("Outbound to OBP-API (OBP_AUTHORIZATION_VIA)", auth.get("outbound_auth_via"))}
274277
</table>
275278
{issuer_sections}
276279
</section>

0 commit comments

Comments
 (0)