LinkO'Star

Supplier OAuth Integration

LinkOStar provides a standard OAuth 2.1 client flow so that LinkOStar app users can link their accounts with a supplier (e.g. a self-hosted supplier), and hub/device permissions stay synchronised across the two systems through that link. This runbook covers the platform-admin registration steps and the contract the supplier must implement.

End-to-end flow

[Flutter app]                  [LinkOStar backend]              [Supplier OAuth]
   |                                                                |
   | 1. /app/supplier-link/authorize?supplier_uuid=...               |
   |    → state + PKCE generated + authorize URL returned            |
   |                                                                |
   | 2. open authorize URL in external browser ──────────────────-->|
   |                                                                |
   | 3. user signs in to supplier + consents                        |
   |                                                                |
   | 4. callback to LinkOStar redirect_uri  <───────────────────────|
   |                                                                |
   | 5. LinkOStar exchanges code → token ───────────────────────────>|
   |    + fetches userinfo                                          |
   |    + UPSERTs supplier_link (encrypted at rest)                 |
   |                                                                |
   | 6. deep link redirect → app returns to foreground              |
   |                                                                |

1. Fields supplier registers with LinkOStar

FieldPurposeRequired
authorize_urlSupplier OAuth authorize endpointYes
token_urlSupplier token endpointYes
client_id / client_secretOAuth credentials issued by supplier to LinkOStarYes
scopesScope JSON array (e.g. ["openid","provisioning:write"])Yes
userinfo_urlReturns sub claim — required for n:1 identificationYes
consent_textUser-facing consent text shown by the appYes
revoke_urlToken revoke endpointOptional
permission_query_urlSupplier-SSOT permission query. If set, supplier is SSOT for permission within that surface.Optional
hub_registration_url / device_registration_urlLinkOStar pushes new hub/device events hereOptional
registration_shared_secretHMAC-SHA256 secret used for both-way webhook signingOptional

2. LinkOStar-side values supplier must know

  • redirect_uri: https://api.linkostar.sandevaux.com/auth/supplier-link/callback
  • HMAC signature header: X-LinkOStar-Signature: sha256=<hex> — LinkOStar signs the entire body with HMAC-SHA256 using the shared secret. Supplier MUST verify with the same secret.
  • Idempotency header: X-LinkOStar-Idempotency-Key — use this to dedupe retries.

3. PUT /platform/supplier-oauth-configs

Platform admin endpoint. UPSERT (PK = supplier_uuid).

PUT /platform/supplier-oauth-configs
Authorization: Bearer <PLATFORM_ADMIN JWT>
Content-Type: application/json

{
  "supplierUuid": "11111111-...",
  "authorizeUrl": "https://supplier.example.com/oauth/authorize",
  "tokenUrl":     "https://supplier.example.com/oauth/token",
  "clientId":     "linkostar-app",
  "clientSecret": "...",                              // plaintext — server encrypts (AES-GCM)
  "scopes":       "[\"openid\",\"provisioning:write\"]",
  "consentText":  "This supplier collects this hub's data into its own servers.",
  "userinfoUrl":  "https://supplier.example.com/oauth/userinfo",
  "revokeUrl":    "https://supplier.example.com/oauth/revoke",
  "permissionQueryUrl":   "https://supplier.example.com/api/m2m/users/{sub}/permissions",
  "hubRegistrationUrl":   "https://supplier.example.com/api/m2m/linkostar/hub-registered",
  "deviceRegistrationUrl":"https://supplier.example.com/api/m2m/linkostar/device-registered",
  "registrationSharedSecret": "<32+ byte random; must match supplier>"
}

Response (secret hidden)

{
  "data": {
    "supplierUuid": "...",
    "authorizeUrl": "...",
    ...
    "registrationSharedSecretSet": true,    // value never returned, only presence
    "createdAt": "...",
    "updatedAt": "..."
  }
}

4. HMAC signature verification (supplier side)

When LinkOStar POSTs to hub_registration_url / device_registration_url, the entire body is signed with HMAC-SHA256. Supplier MUST verify with the same algorithm and reject mismatches (silently — return 401, never process).

# Node.js example (Express)
import crypto from 'node:crypto';

app.post('/api/m2m/linkostar/hub-registered', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.header('X-LinkOStar-Signature');
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.LINKOSTAR_SHARED_SECRET)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).end();
  }
  const payload = JSON.parse(req.body.toString('utf-8'));
  // payload: { hub_uuid, supplier_uuid, tenant_uuid, bundle_uuid, hub_identifier, hub_type, metadata }
  // Idempotency: dedupe by req.header('X-LinkOStar-Idempotency-Key')
  res.status(200).end();
});

5. Supplier → LinkOStar permission change webhook (optional)

When the supplier changes a user's permissions, it can notify LinkOStar to invalidate the cache. POST to /webhook/supplier-permission/{supplierUuid} with the same HMAC signature.

POST /webhook/supplier-permission/{supplierUuid}
X-LinkOStar-Signature: sha256=<hex>
Content-Type: application/json

{ "user_sub": "<supplier-side user sub>" }

# → LinkOStar invalidates that user's supplier permission cache
# → next permission lookup will re-fetch from permission_query_url

6. permission_query_url response shape

If registered, LinkOStar calls this on cache miss using the user's supplier-issued access_token. If the URL contains the {sub} placeholder it is substituted with the user's supplier sub.

GET https://supplier.example.com/api/m2m/users/{sub}/permissions
Authorization: Bearer <supplier-issued user access_token>

Response 200:
[
  {"resource_type": "HUB",    "resource_uuid": "550e8400-...", "permissions": ["owner"]},
  {"resource_type": "DEVICE", "resource_uuid": "660e8400-...", "permissions": ["operator"]}
]

# permissions enum: "owner" > "operator" > "viewer"
# Unknown values are dropped silently (LinkOStar enum must extend to add new ones).

Operational checklist

  • shared_secret: 32+ byte random. openssl rand -base64 32 recommended. Same value on both sides.
  • redirect_uri: must be https://api.linkostar.sandevaux.com/auth/supplier-link/callback exactly.
  • userinfo.sub: this is the n:1 identifier; it must be stable and unique. Changing it breaks every existing link.
  • PKCE: LinkOStar always sends S256. If your OAuth server forces plain, integration breaks.