Skip to content

Authentication

Homecast supports three authentication models. Choose based on your use case.

ModelFormatUse caseLifetime
JWTeyJ...Web and mobile app sessions7 days
Access Tokenhc_...Programmatic API scriptsUntil revoked or expired
OAuth 2.1Bearer tokenThird-party integrations, MCP1 hour (refreshable)

All three are sent the same way:

http
Authorization: Bearer <token>

JWT sessions

First-party sessions for the web and mobile apps. Obtained via login or signup GraphQL mutations.

Claims

ClaimTypeDescription
substringUser ID (UUID)
emailstringUser email address
iatdatetimeIssued at
expdatetimeExpiration (issued + 7 days)
home_permissionsobject | nullHome access scope (only for OAuth-issued tokens)

Configuration

SettingValue
AlgorithmHS256
Expiry168 hours (7 days)

Login

graphql
mutation {
  login(email: "user@example.com", password: "password") {
    success
    token
    user {
      id
      email
      name
    }
    error
  }
}

Response:

json
{
  "data": {
    "login": {
      "success": true,
      "token": "eyJhbGciOiJIUzI1NiIs...",
      "user": { "id": "uuid", "email": "user@example.com", "name": "Alice" },
      "error": null
    }
  }
}

Signup

graphql
mutation {
  signup(email: "user@example.com", password: "password", name: "Alice") {
    success
    token
    user { id email name }
    error
  }
}

Access tokens

Programmatic tokens for scripts and automations. Prefixed with hc_.

Create a token

graphql
mutation {
  createAccessToken(
    name: "My Script"
    homePermissions: "{\"home-uuid\": \"control\"}"
  ) {
    success
    token
    accessToken {
      id
      name
      tokenPrefix
    }
    error
  }
}

The full token value (starting with hc_) is only returned on creation. Store it immediately.

Home permissions

The homePermissions field is a JSON string mapping home UUIDs to access levels:

json
{
  "550e8400-e29b-41d4-a716-446655440000": "control",
  "6ba7b810-9dad-11d1-80b4-00c04fd430c8": "view"
}
LevelCan read stateCan control devices
controlYesYes
viewYesNo

If homePermissions is null or omitted, the token has full access to all homes.

Token properties

PropertyDescription
idToken UUID
nameUser-defined label
tokenPrefixFirst characters of token (for identification)
lastUsedAtLast time the token was used
expiresAtExpiration date (null = never expires)
revokedAtRevocation date (null = active)
createdAtCreation timestamp

List and revoke

graphql
# List all tokens
{ accessTokens { id name tokenPrefix lastUsedAt expiresAt createdAt } }

# Revoke a token
mutation { revokeAccessToken(tokenId: "token-uuid") { success error } }

OAuth 2.1

Full OAuth 2.1 with PKCE for third-party integrations and MCP clients.

Discovery

EndpointPurpose
GET /.well-known/oauth-authorization-serverServer metadata (RFC 8414)
GET /.well-known/openid-configurationOpenID Connect Discovery
GET /.well-known/oauth-protected-resourceResource metadata (RFC 8707)

Server metadata response:

json
{
  "issuer": "https://api.homecast.cloud",
  "authorization_endpoint": "https://api.homecast.cloud/oauth/authorize",
  "token_endpoint": "https://api.homecast.cloud/oauth/token",
  "registration_endpoint": "https://api.homecast.cloud/oauth/register",
  "scopes_supported": ["mcp:read", "mcp:write", "mcp:admin"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"]
}

Scopes

ScopeDescription
mcp:readRead device state
mcp:writeRead and control devices
mcp:adminFull access including management

Flow

1. Dynamic client registration

http
POST /oauth/register
Content-Type: application/json

{
  "redirect_uris": ["https://your-app.com/callback"],
  "client_name": "My Integration",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Response:

json
{
  "client_id": "generated-uuid",
  "client_secret": "generated-secret",
  "client_id_issued_at": 1708099200,
  "client_secret_expires_at": 0,
  "redirect_uris": ["https://your-app.com/callback"],
  "client_name": "My Integration",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

2. Authorization request (with PKCE)

GET /oauth/authorize?
  client_id={client_id}&
  redirect_uri=https://your-app.com/callback&
  response_type=code&
  code_challenge={challenge}&
  code_challenge_method=S256&
  scope=mcp:write&
  state={random_state}

The user logs in, selects which homes to grant access to, and approves. Homecast redirects back:

https://your-app.com/callback?code={auth_code}&iss=https://api.homecast.cloud&state={random_state}

3. Token exchange

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code={auth_code}&
redirect_uri=https://your-app.com/callback&
code_verifier={verifier}

Response:

json
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "refresh_token_value",
  "scope": "mcp:write"
}

4. Refresh

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token={refresh_token}

Returns a new access token and a rotated refresh token.

Token lifetimes

TokenLifetime
Authorization code10 minutes
Access token1 hour
Refresh token30 days

Refresh tokens use rotation — each refresh returns a new refresh token and invalidates the old one.

Managing authorized apps

graphql
# List apps with OAuth access
{ authorizedApps { clientId clientName scope createdAt } }

# Revoke an app's access
mutation { revokeAuthorizedApp(clientId: "client-uuid") { success error } }

Error responses

StatusBodyMeaning
401{"error": "invalid_token"}Token is expired, revoked, or malformed
403{"error": "insufficient_permissions"}Token doesn't have access to the requested home or action
401{"error": "invalid_client"}OAuth client credentials are invalid
400{"error": "invalid_grant"}Authorization code or refresh token is invalid/expired