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
| Field | Purpose | Required |
|---|---|---|
authorize_url | Supplier OAuth authorize endpoint | Yes |
token_url | Supplier token endpoint | Yes |
client_id / client_secret | OAuth credentials issued by supplier to LinkOStar | Yes |
scopes | Scope JSON array (e.g. ["openid","provisioning:write"]) | Yes |
userinfo_url | Returns sub claim — required for n:1 identification | Yes |
consent_text | User-facing consent text shown by the app | Yes |
revoke_url | Token revoke endpoint | Optional |
permission_query_url | Supplier-SSOT permission query. If set, supplier is SSOT for permission within that surface. | Optional |
hub_registration_url / device_registration_url | LinkOStar pushes new hub/device events here | Optional |
registration_shared_secret | HMAC-SHA256 secret used for both-way webhook signing | Optional |
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_url6. 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 32recommended. Same value on both sides. - redirect_uri: must be
https://api.linkostar.sandevaux.com/auth/supplier-link/callbackexactly. - 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.