07 — M2M Token Manager Deep Dive (Central-Manager)¶
Scope: The machine-to-machine (M2M) token manager in Central-Manager — what it is, how it works, where it's used, and how it relates to the user EVO token.
Validated: 2026-06-17 against
chain-management-centralization/central-managersource (SharedStoreUnitEvoTokenServiceImpl.java,SharedStoreUnitEvoTokenType.java,CentralManagerProperties.java) andhelm/pricer-central-solution/values.yaml.Accuracy note — corrected version of the earlier
M2M-Token-Manager-Deep-Dive.mddraft. The draft was largely accurate; fixes: the additional-audiences field is actuallyserverAllowedAudiences(notm2mClientAdditionalAudiences), the dev default audience values, and a stale Jira reference. Flagged inline ✏️. Pairs with 05 — Core Concepts → EVO Token.
1. What it is ✅¶
An M2M token manager obtains service-level JWTs from Auth0 with no human in the loop, using the OAuth2 client_credentials grant (client id + client secret → access token).
Human users → EVO tokens via login (password / SSO)
Services → M2M tokens via client_credentials (client_id + client_secret)
@Service SharedStoreUnitEvoTokenServiceImpl (package se.pricer.centralmanager.security). ✅
2. Why Central-Manager needs it ✅¶
When a human calls CM, the user's EVO token is forwarded. When CM acts on its own behalf (background jobs, forwarding to Store-Units, calling external renderer / DMS), there is no user token — so it mints a service token.
---
config:
layout: elk
---
flowchart TB
subgraph Human["👤 Human flow"]
H1["login → EVO JWT"] --> H2["BFF/CM forwards user token → R3Server"]
end
subgraph M2M["🤖 M2M flow"]
M1["CM needs to call a service"] --> M2["SharedStoreUnitEvoTokenService"]
M2 --> M3["client_credentials → Auth0"]
M3 --> M4["cache (Guava, 7h)"]
M4 --> M5["call downstream with service token"]
end
style Human fill:#e3f2fd,stroke:#1565c0,color:#000
style M2M fill:#e8f5e9,stroke:#2e7d32,color:#000
3. Implementation ✅ verified line-by-line¶
3.1 Service & token types¶
SharedStoreUnitEvoTokenType.java:
// TODO: When building the integration towards tenant configurator we agreed with the core team to
// have only one M2M token per system, not one per integration. So for now we re-use the DMS token
// for the tenant configurator integration. Ideally it should be renamed into something more generic.
public enum SharedStoreUnitEvoTokenType {
EXTERNAL_RENDERER,
DMS,
}
✅ Both the enum values and that TODO are verbatim in the code.
3.2 Token caching config ✏️ field name corrected¶
CentralManagerProperties.java (Auth0 → EVO block):
private String clientAudience = "https://dev.pricer-plaza.com"; // line 202
private String m2mClientAudience = "https://internal.m2m.dev.pricer-plaza.com"; // line 203
private List<String> serverAllowedAudiences = List.of( // line 204 ✏️
"https://m2m.dev.pricer-plaza.com",
"https://internal.m2m.dev.pricer-plaza.com");
private String dmsClientId; // line 215
private int m2mTokenCacheExpiryInSeconds = 60 * 60 * 7; // line 216 = 25200 (7h)
✏️ The earlier draft called the list
m2mClientAdditionalAudiences. The real field isserverAllowedAudiences(the resource-server's accepted audiences). There is alsoexternalRendererClientId/dmsClientIdfor the two token types.
3.3 Token fetch flow ✅¶
---
config:
layout: elk
---
sequenceDiagram
participant CM as Central-Manager
participant Cache as Guava LoadingCache
participant SM as GCP Secret Manager
participant Auth0
CM->>Cache: getSharedMachineToMachineToken(EXTERNAL_RENDERER)
alt cached & fresh
Cache-->>CM: cached JWT
else miss/expired
Cache->>CM: load → fetchEvoToken(type)
CM->>SM: getSecretString("auth0-" + clientId)
SM-->>CM: client_secret
CM->>Auth0: authenticateEvoM2m("client_credentials", audience, clientId, secret, tenantUuid)
Auth0-->>CM: { access_token, ... }
CM->>CM: expiresAt = jwt.decode(access_token).getExpiresAt()
CM->>Cache: store (expireAfterWrite 25200s)
end
Key code in SharedStoreUnitEvoTokenServiceImpl.java:
public static final String EVO_AUTH_0_CLIENT_SECRET_PREFIX = "auth0-"; // line 27
private SharedStoreUnitEvoToken fetchEvoToken(SharedStoreUnitEvoTokenType evoTokenType) {
String clientId = switch (evoTokenType) {
case EXTERNAL_RENDERER -> props.getAuth0().getEvo().getExternalRendererClientId();
case DMS -> props.getAuth0().getEvo().getDmsClientId();
};
String clientSecret =
secretManagerTemplate.getSecretString(EVO_AUTH_0_CLIENT_SECRET_PREFIX + clientId);
var response = auth0Retrofit.authenticateEvoM2m(
"client_credentials",
props.getAuth0().getEvo().getM2mClientAudience(),
clientId, clientSecret,
props.getTenantUuid()).execute();
if (response.isSuccessful()) {
String accessToken = response.body().getAccessToken();
Instant expiresAt = evoJwtDecoder.decode(accessToken).getExpiresAt();
return new SharedStoreUnitEvoToken(accessToken, expiresAt);
}
}
3.4 Cache (Guava) ✅¶
this.evoTokenCache = CacheBuilder.newBuilder()
.expireAfterWrite(props.getAuth0().getEvo().getM2mTokenCacheExpiryInSeconds(), TimeUnit.SECONDS)
.build(CacheLoader.from(this::fetchEvoToken)); // line 43-46
public SharedStoreUnitEvoToken getSharedMachineToMachineToken(SharedStoreUnitEvoTokenType type)
throws ExecutionException { return evoTokenCache.get(type); } // line 53
SharedStoreUnitEvoTokenType and reloads via fetchEvoToken on miss/expiry. ✅
4. Helm configuration ✏️ field name corrected¶
helm/pricer-central-solution/values.yaml (auth0.evo block):
auth0:
evo:
clientAudience: https://pricer-plaza.com # (dev: https://dev.pricer-plaza.com)
m2mClientAudience: https://pricer-plaza.com
serverAllowedAudiences: # ✏️ not 'm2mClientAdditionalAudiences'
- https://pricer-plaza.com
- https://m2m.pricer-plaza.com
- https://internal.m2m.pricer-plaza.com
domain: auth.iam.pricer-plaza.com # Auth0/EVO domain
externalRendererClientId:
dmsClientId:
# 25200 = 7 hours (evo m2m tokens expire in 8 hours)
m2mTokenCacheExpiryInSeconds: 25200
✅ The "tokens expire in 8 hours, cache 7 hours" relationship is documented in the values.yaml comment — the cache deliberately refreshes ~1h before Auth0 expiry to avoid races.
Environment is wired in central-manager-deployment.yaml (Spring relaxed-binding names, e.g. PRICER_CM_AUTH0_EVO_M2M_CLIENT_AUDIENCE, PRICER_CM_AUTH0_EVO_M2M_TOKEN_CACHE_EXPIRY_IN_SECONDS).
5. M2M token vs EVO user token ✅¶
| Aspect | EVO user token | M2M service token |
|---|---|---|
| Who | a human (associate/admin) | Central-Manager itself |
| Grant | login (auth code / SSO) | client_credentials |
| Identity | user (sub/role/tags) | service (client_id) |
| Tenant | from Auth0 claim | passed explicitly (props.getTenantUuid()) |
| Secret | user password | client secret in GCP Secret Manager (auth0-<clientId>) |
| Cache | none | Guava LoadingCache, 7h |
| Lifetime | per session | ~8h (cache refreshes at 7h) |
| Usage | BFF/CM forwards user token → R3Server | CM → external renderer / DMS / Store-Units |
---
config:
layout: elk
---
flowchart LR
subgraph U["👤 EVO user token"]
U1["login → JWT (sub/tenant/role/tags)"]
end
subgraph M["🤖 M2M service token"]
M1["client_credentials → JWT (client_id, tenantUuid)"]
M2["Guava cache 7h"]
end
style U fill:#e3f2fd,stroke:#1565c0,color:#000
style M fill:#e8f5e9,stroke:#2e7d32,color:#000
6. Where M2M tokens are used ✏️ Jira status refreshed¶
CM mints M2M tokens to call services on its own behalf. The two concrete token types in code are:
- EXTERNAL_RENDERER — calls to the external rendering service.
- DMS — Device Management Service integration (and, per the TODO, re-used for the tenant-configurator integration — one token per system, not per integration).
Other CM-initiated, server-to-server scenarios that rely on service auth include multi-store item operations forwarded to each Store-Unit's R3Server, and configuration/ECC data movement.
✏️ The earlier draft tied this to PLT-2573 "ECC Sync — On-Prem to Cloud Data Push", which is Closed as of 2026-06-17 (verify in Jira before citing it as active), and PLT-2353 "Pricer Server config export to DTOflow" (Backlog). Treat those epic links as status-dependent.
7. Summary ✅¶
SharedStoreUnitEvoTokenServiceImpl lets Central-Manager authenticate as a trusted service:
1. Fetches a JWT from Auth0 via client_credentials.
2. Caches it in a Guava LoadingCache for 7h (Auth0 token lives ~8h).
3. Reads secrets from GCP Secret Manager (auth0-<clientId>), never from config/env.
4. Two token types today — EXTERNAL_RENDERER, DMS (intended to be unified).
5. Tenant-scoped by passing tenantUuid to Auth0.