OAuth-In app devices scope gets corrupted server-side on save (asterisk becomes $, entries duplicated)

Any OAuth-In app’s stored scope gets corrupted server-side the moment it includes a devices-resource wildcard scope (r:devices:* and/or x:devices:*). Each entry is silently duplicated, and the duplicate has its * mangled to $. This makes /oauth/authorize fail with error=unsupported_response_type for any affected app, permanently blocking the OAuth flow.

Steps to reproduce

  1. Create a brand-new API_ONLY app (smartthings apps:create).

  2. Set its OAuth scope to include a devices resource wildcard, e.g.:

    { "scope": ["r:devices:*", "x:devices:*"], "redirectUris": ["https://example.com/callback"] }
    

    via smartthings apps:oauth:update <appId> -i scope.json

  3. Read the scope back: smartthings apps:oauth <appId>

Actual result

{

"scope": ["r:devices:$", "x:devices:*", "r:devices:*", "x:devices:$"]

}

(order may vary, but every wildcard devices entry ends up duplicated, one copy with * replaced by $.)

Expected result

{ "scope": ["r:devices:*", "x:devices:*"] }

unchanged from what was submitted.

Isolation already done (to rule out client-side causes)

  • Reproduced with a single r:devices:* scope alone — not just the read+execute pair.
  • Reproduced on multiple brand-new apps created fresh (not a stale/pre-existing app state).
  • Reproduced whether the scope is set via a separate apps:oauth:update call or embedded directly in the apps:create payload — same result either way.
  • Confirmed the corruption is not a client/tooling issue: using smartthings apps:oauth:update --dry-run, the exact outgoing request body is clean (["r:devices:*", "x:devices:*"], no duplication). The corruption only appears in what the server stores/returns afterward — it happens server-side, after a clean request is received.
  • Does not happen with non-devices resources — r:locations:* alone is stored and returned clean.
  • Tried the device-specific scope form (r:devices:{deviceId}) as a workaround instead of the wildcard — rejected outright by validation with 422 NotValidValue / Invalid scope definition, so OAuth-In apps apparently only accept the wildcard form for devices scopes, and the wildcard is exactly what gets corrupted. There is no scope syntax that both passes validation and avoids this.

Impact

This fully blocks building any OAuth-In (not just PAT-based) integration that needs device read/control access — /oauth/authorize cannot complete for an affected app, so the authorization step itself never succeeds. We’ve been stuck on PAT-based access as a fallback, which Samsung’s own 24h PAT expiry (since Dec 2024) makes impractical for a real always-connected integration.

Happy to share exact appIds used in testing, or run further isolation on request.