Skip to main content

Authentication

Comprehensive guide to authentication in AsaHome Cloud.

Overview

AsaHome Cloud uses JWT (JSON Web Tokens) for authentication with a dual-token system:

Token TypeLifetimeStoragePurpose
Access Token15 minutesClient memoryAPI authentication
Refresh Token30 daysDatabase (hashed)Obtain new access tokens

Authentication Flow

Login

Request

curl -X POST https://cloud.asahome.io/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "yourPassword123"
}'

Response

{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 900,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"role": "user"
}
}

Token Refresh

Access tokens expire after 15 minutes. Use the refresh token to obtain new tokens without re-authenticating.

Token Rotation

Each refresh invalidates the previous refresh token and issues a new one. This prevents token reuse attacks.

Request

curl -X POST https://cloud.asahome.io/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'

Response

{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 900
}

Logout

Logout invalidates the refresh token, preventing further token refreshes.

curl -X POST https://cloud.asahome.io/api/v1/auth/logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

JWT Token Structure

Access Token Payload

{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"role": "user",
"iat": 1700000000,
"exp": 1700000900
}
ClaimDescription
subUser ID (UUID)
emailUser email address
roleUser role (user or admin)
iatIssued at timestamp
expExpiration timestamp

Device Token Payload

Devices receive tokens with additional claims:

{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"deviceUuid": "device-uuid-here",
"type": "device",
"iat": 1700000000,
"exp": 1700086400
}

Using Tokens

REST API Requests

Include the access token in the Authorization header:

curl -X GET https://cloud.asahome.io/api/v1/devices \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json"

WebSocket Connections

Pass the token in the connection handshake:

import { io } from 'socket.io-client';

const socket = io('wss://cloud.asahome.io/tunnel', {
transports: ['websocket'],
auth: {
token: accessToken
}
});

socket.on('connect', () => {
console.log('Connected to tunnel');
});

socket.on('connect_error', (error) => {
if (error.message === 'unauthorized') {
// Token expired - refresh and reconnect
refreshTokenAndReconnect();
}
});

Flutter Implementation

Token Storage

Use secure storage for tokens:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthStorage {
final _storage = FlutterSecureStorage();

Future<void> saveTokens(String accessToken, String refreshToken) async {
await _storage.write(key: 'access_token', value: accessToken);
await _storage.write(key: 'refresh_token', value: refreshToken);
}

Future<String?> getAccessToken() async {
return await _storage.read(key: 'access_token');
}

Future<String?> getRefreshToken() async {
return await _storage.read(key: 'refresh_token');
}

Future<void> clearTokens() async {
await _storage.deleteAll();
}
}

HTTP Interceptor

Automatically refresh tokens on 401 responses:

class AuthInterceptor extends Interceptor {
final Dio dio;
final AuthStorage storage;

AuthInterceptor(this.dio, this.storage);


void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await storage.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}


void onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
final refreshToken = await storage.getRefreshToken();
if (refreshToken != null) {
try {
final newTokens = await _refreshTokens(refreshToken);
await storage.saveTokens(
newTokens['accessToken'],
newTokens['refreshToken'],
);
// Retry original request
final response = await dio.fetch(err.requestOptions);
handler.resolve(response);
return;
} catch (e) {
// Refresh failed - logout user
await storage.clearTokens();
}
}
}
handler.next(err);
}
}

Internal API Key Authentication

For service-to-service communication (e.g., Laravel to AsaHome Cloud), use internal API keys.

Configuration

# In .env
INTERNAL_API_KEYS=laravel-service-key-123,monitoring-key-456

Usage

curl -X POST https://cloud.asahome.io/api/v1/auth/sync-customer \
-H "X-Internal-Api-Key: laravel-service-key-123" \
-H "Content-Type: application/json" \
-d '{ "id": 123, "email": "customer@example.com", ... }'

Security Best Practices

Token Storage

PlatformRecommendation
Mobile (Flutter)flutter_secure_storage (Keychain/Keystore)
WebHttpOnly cookies or memory (not localStorage)
ServerEnvironment variables

Token Lifetime

  • Access Token (15m): Short lifetime limits exposure if compromised
  • Refresh Token (30d): Long enough for good UX, with rotation for security

Additional Protections

  1. HTTPS Only: All communication must use TLS
  2. Token Rotation: Each refresh invalidates the previous token
  3. Device Binding: Optionally bind tokens to device fingerprints
  4. Rate Limiting: Prevent brute force attacks on login

Error Responses

Invalid Credentials

{
"statusCode": 401,
"message": "Invalid email or password",
"error": "Unauthorized"
}

Token Expired

{
"statusCode": 401,
"message": "Token has expired",
"error": "Unauthorized"
}

Token Revoked

{
"statusCode": 401,
"message": "Token has been revoked",
"error": "Unauthorized"
}

Next Steps