ConnectMigrating from M2M

Migrating from M2M to Connect

If your integration currently uses Machine-to-Machine (client_credentials) tokens to access user data, this guide explains when and how to migrate to the Connect flow (authorization_code + PKCE).

When to migrate

ScenarioKeep M2MMigrate to Connect
Service-to-service calls with no user context (cron jobs, internal pipelines)YesNo
Reading/writing data on behalf of a specific userNoYes
Admin operations (provisioning, bulk exports)YesNo
User-facing app (web, mobile, SPA)NoYes
Partner integration that accesses user health dataNoYes

Rule of thumb: if your code runs because a user did something, use Connect. If it runs on a schedule or in response to an internal event, keep M2M.

What changes

Before (M2M)

# Get a service token (no user involved)
curl -X POST https://identity.platform.loop.health/v1/oauth/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$SERVICE_CLIENT_ID" \
  -d "client_secret=$SERVICE_CLIENT_SECRET" \
  -d "scope=admin:clinical"
 
# Call with admin-level access — no scope boundaries per user
curl https://clinical.platform.loop.health/v1/biomarkers?user_id=user_01HXY \
  -H "Authorization: Bearer $SERVICE_TOKEN"

After (Connect)

# User grants specific scopes via the consent screen
# Your app receives an authorization code, exchanges it:
curl -X POST https://identity.platform.loop.health/v1/oauth/token \
  -d "grant_type=authorization_code" \
  -d "code=$AUTH_CODE" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "code_verifier=$VERIFIER"
 
# Call with user-scoped access — can only see what the user allowed
curl https://clinical.platform.loop.health/v1/biomarkers/me \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN"

Key differences

AspectM2M (client_credentials)Connect (authorization_code + PKCE)
User contextNone — calls act as the service itselfAlways tied to a specific user
Scopesadmin:* — full service accessread:* / write:* — limited to user consent
Token lifetimeConfigurable (often long-lived)1 hour access / 90-day refresh
ConsentNo user involvementUser sees consent screen, can revoke
Audit trailLogged as service actionLogged as user-authorized action with app attribution
Resource path/v1/resource?user_id=.../v1/resource/me (user inferred from token)
HIPAACovered by service BAARequires per-app BAA for PHI scopes
RevocationService credential rotationPer-user grant revocation

Migration steps

1. Register an OAuth client

If your integration only has M2M credentials, register a new OAuth client at developers.platform.loop.health. You will get a separate client_id for user-facing flows.

2. Identify the scopes you need

Map your current admin:* usage to user-facing scopes:

M2M scopeConnect scope(s)
admin:clinical (reading biomarkers)read:biomarkers
admin:clinical (starting protocols)write:protocols
admin:payments (viewing history)read:payments
admin:comms (reading inbox)read:inbox

See scopes.md for the complete list.

3. Implement the authorization flow

Follow the quickstart or one of the language-specific guides:

4. Update your API calls

Replace user_id query parameters with the /me convention:

- GET /v1/biomarkers?user_id=user_01HXY
- Authorization: Bearer $SERVICE_TOKEN
+ GET /v1/biomarkers/me
+ Authorization: Bearer $USER_ACCESS_TOKEN

Services infer the user from the access token’s sub claim.

5. Handle token refresh

M2M tokens are typically long-lived. Connect access tokens expire after 1 hour. Your app must handle 401 responses by refreshing:

async function callWithRefresh(url: string, tokens: Tokens): Promise<Response> {
  let res = await fetch(url, {
    headers: { Authorization: `Bearer ${tokens.accessToken}` },
  });
 
  if (res.status === 401) {
    const refreshed = await refreshTokens(tokens.refreshToken);
    tokens.accessToken = refreshed.access_token;
    tokens.refreshToken = refreshed.refresh_token;
    // Persist the new tokens
    await saveTokens(tokens);
 
    res = await fetch(url, {
      headers: { Authorization: `Bearer ${refreshed.access_token}` },
    });
  }
 
  return res;
}

6. Run both flows during transition

During migration, you can run both M2M and Connect in parallel:

  • Keep M2M for admin/batch operations that have no user context.
  • Use Connect for user-facing features.
  • Both token types are valid simultaneously; services distinguish them by the token prefix and claims.

7. Clean up

Once all user-facing paths use Connect:

  • Remove any admin:* scope usage from user-facing code paths.
  • Audit your M2M credentials — restrict them to only the admin scopes they genuinely need.
  • Update your service.yaml if it declares scopes.

Common pitfalls

“My cron job needs user data.” If a background job processes data for many users, it should still use M2M. Only migrate if the job runs in direct response to a user action and can carry the user’s token.

“I need to access multiple users’ data.” Connect tokens are per-user. If you need to aggregate across users, keep M2M for the aggregation layer and use Connect for individual user-facing views.

“The user’s refresh token expired.” Refresh tokens last 90 days from last use. If a user hasn’t used your app in 90 days, you’ll need to re-prompt for consent. This is by design — stale grants should not persist indefinitely.