feat(llmo-3954): add IMS service principal token for CDN routing API calls#2175
feat(llmo-3954): add IMS service principal token for CDN routing API calls#2175
Conversation
…calls Adds getServicePrincipalToken() to support/utils.js using ImsClient client_credentials grant (IMS v3 token endpoint). The CDN routing function updateEdgeOptimizeCDNRouting now tries the SP token first and falls back to the user promise token if SP credentials are not configured in env. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Pull request overview
Adds an IMS service-principal (client_credentials) token acquisition helper and updates the LLMO Edge Optimize CDN routing flow to prefer a backend service token over a user-derived token (with fallback).
Changes:
- Add
getServicePrincipalToken(context)insrc/support/utils.jsusingImsClient.getServiceAccessTokenV3(). - Update
updateEdgeOptimizeCDNRoutingto try a service-principal token first, then fall back to exchanging the provided promise token.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/support/utils.js |
Introduces a new helper to fetch an IMS service-principal access token via IMS v3. |
src/controllers/llmo/llmo.js |
Updates CDN routing auth to prefer the service-principal token and fall back to the existing promise-token exchange. |
| imsUserToken = await getServicePrincipalToken(context); | ||
| log.info('IMS service principal token obtained successfully'); | ||
| } catch (spTokenError) { | ||
| log.warn(`Service principal token unavailable for site ${siteId}: ${spTokenError.message}; falling back to promise token`); |
There was a problem hiding this comment.
updateEdgeOptimizeCDNRouting still rejects requests when x-promise-token is missing (earlier in the function), so the new service-principal path can never be used without that header. If the intent is “SP token first; only require promise token for fallback”, move/conditionalize the x-promise-token validation so SP-token flows can succeed without it.
| log.warn(`Service principal token unavailable for site ${siteId}: ${spTokenError.message}; falling back to promise token`); | |
| log.warn(`Service principal token unavailable for site ${siteId}: ${spTokenError.message}; falling back to promise token`); | |
| if (!hasText(promiseToken)) { | |
| log.warn(`No promise token provided for site ${siteId}; unable to use fallback authentication`); | |
| return createResponse({ message: 'Authentication failed with upstream IMS service' }, 401); | |
| } |
| log.debug(`Getting IMS service principal token for site ${siteId}`); | ||
| imsUserToken = await getServicePrincipalToken(context); | ||
| log.info('IMS service principal token obtained successfully'); | ||
| } catch (spTokenError) { | ||
| log.warn(`Service principal token unavailable for site ${siteId}: ${spTokenError.message}; falling back to promise token`); | ||
| try { | ||
| log.debug(`Getting IMS user token for site ${siteId}`); | ||
| imsUserToken = await exchangePromiseToken(context, promiseToken); | ||
| log.info('IMS user token obtained successfully (fallback)'); | ||
| } catch (tokenError) { | ||
| log.warn(`Fetching IMS user token for site ${siteId} failed: ${tokenError.status} ${tokenError.message}`); | ||
| return createResponse({ message: 'Authentication failed with upstream IMS service' }, 401); | ||
| } |
There was a problem hiding this comment.
This change introduces a new token acquisition path (getServicePrincipalToken) and changes the controller’s behavior, but no tests are updated/added in this PR. Please update test/controllers/llmo/llmo.test.js to stub/assert the SP-token path (including the “no x-promise-token needed” case) and the fallback-to-promise-token case.
| let imsUserToken; | ||
| try { | ||
| log.debug(`Getting IMS user token for site ${siteId}`); | ||
| imsUserToken = await exchangePromiseToken(context, promiseToken); | ||
| log.info('IMS user token obtained successfully'); | ||
| } catch (tokenError) { | ||
| log.warn(`Fetching IMS user token for site ${siteId} failed: ${tokenError.status} ${tokenError.message}`); | ||
| return createResponse({ message: 'Authentication failed with upstream IMS service' }, 401); | ||
| log.debug(`Getting IMS service principal token for site ${siteId}`); | ||
| imsUserToken = await getServicePrincipalToken(context); | ||
| log.info('IMS service principal token obtained successfully'); |
There was a problem hiding this comment.
imsUserToken now holds either a service-principal token or a user-exchanged token. Renaming this variable to something neutral (e.g., imsAccessToken) would avoid confusion in logs/debugging and reduce the chance of future misuse.
| } catch (spTokenError) { | ||
| log.warn(`Service principal token unavailable for site ${siteId}: ${spTokenError.message}; falling back to promise token`); | ||
| try { | ||
| log.debug(`Getting IMS user token for site ${siteId}`); | ||
| imsUserToken = await exchangePromiseToken(context, promiseToken); | ||
| log.info('IMS user token obtained successfully (fallback)'); | ||
| } catch (tokenError) { |
There was a problem hiding this comment.
The fallback currently triggers on any service-principal token error, even when IMS credentials are configured. If fallback is only intended when env vars are missing (per PR description), consider checking required env vars before calling IMS or narrowing the catch to “not configured” errors so misconfigurations/outages don’t get silently masked.
| export async function getServicePrincipalToken(context) { | ||
| const imsClient = ImsClient.createFrom(context); | ||
| const { access_token: accessToken } = await imsClient.getServiceAccessTokenV3(); | ||
| return accessToken; |
There was a problem hiding this comment.
getServicePrincipalToken relies on ImsClient.createFrom(context) throwing when required env vars are missing. To make fallback behavior deterministic (and reduce noisy warn logs), consider explicitly validating IMS_HOST, IMS_CLIENT_ID, IMS_CLIENT_CODE, and IMS_CLIENT_SECRET and throwing a clear “not configured” error when any are absent.
Summary
getServicePrincipalToken(context)tosupport/utils.jsusingImsClient.getServiceAccessTokenV3()(client_credentials grant)updateEdgeOptimizeCDNRoutingnow tries the SP token first; falls back to the user promise token ifIMS_CLIENT_ID/IMS_CLIENT_SECRETenv vars are not configuredRequired env vars
IMS_HOST,IMS_CLIENT_ID,IMS_CLIENT_CODE,IMS_CLIENT_SECRET(already available via Vault)Test plan
x-promise-tokenheader needed)Fixes LLMO-3954
🤖 Generated with Claude Code