Authentication
Comprehensive guide to authentication in AsaHome Cloud.
Overview
AsaHome Cloud uses JWT (JSON Web Tokens) for authentication with a dual-token system:
| Token Type | Lifetime | Storage | Purpose |
|---|---|---|---|
| Access Token | 15 minutes | Client memory | API authentication |
| Refresh Token | 30 days | Database (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
}
| Claim | Description |
|---|---|
sub | User ID (UUID) |
email | User email address |
role | User role (user or admin) |
iat | Issued at timestamp |
exp | Expiration 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
| Platform | Recommendation |
|---|---|
| Mobile (Flutter) | flutter_secure_storage (Keychain/Keystore) |
| Web | HttpOnly cookies or memory (not localStorage) |
| Server | Environment 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
- HTTPS Only: All communication must use TLS
- Token Rotation: Each refresh invalidates the previous token
- Device Binding: Optionally bind tokens to device fingerprints
- 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
- WebSocket Tunnel - Real-time device communication
- API Reference: Auth - Detailed endpoint documentation
- Security Guide - Security best practices