Session Management
Manage user sessions, tokens, and authentication lifecycle.
Token Types
De. uses JWT-based authentication with two token types:
- Access Token: Short-lived (1 hour) for API requests
- Refresh Token: Long-lived (30 days) for obtaining new access tokens
Storage: Access tokens in memory, refresh tokens in httpOnly cookies (recommended)
Token Lifecycle
1
Initial Authentication
Obtain tokens on login
After successful authentication (phone, email, or OAuth):
json
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "refresh_abc123def456",
"expiresIn": 3600, // seconds
"tokenType": "Bearer"
}2
Use Access Token
Authenticate API requests
typescript
// Include in Authorization header
fetch('https://api.dedot.io/v1/resource', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})3
Refresh Tokens
Get new access token before expiry
typescript
POST https://auth.dedot.io/v1/auth/refresh
{
"refreshToken": "refresh_abc123def456"
}Response:
json
{
"success": true,
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600
}Auto-Refresh Implementation
React Hook
typescript
import { useState, useEffect, useCallback } from 'react'
function useAuth() {
const [accessToken, setAccessToken] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
// Refresh token before expiry
const refreshAccessToken = useCallback(async () => {
if (isRefreshing) return
setIsRefreshing(true)
try {
const refreshToken = localStorage.getItem('refreshToken')
if (!refreshToken) {
throw new Error('No refresh token available')
}
const response = await fetch('https://auth.dedot.io/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
})
const data = await response.json()
if (data.success) {
setAccessToken(data.accessToken)
// Schedule next refresh (5 minutes before expiry)
const refreshTime = (data.expiresIn - 300) * 1000
setTimeout(refreshAccessToken, refreshTime)
} else {
// Refresh failed, logout user
handleLogout()
}
} catch (error) {
console.error('Token refresh failed:', error)
handleLogout()
} finally {
setIsRefreshing(false)
}
}, [isRefreshing])
// Load token on mount
useEffect(() => {
const token = localStorage.getItem('accessToken')
if (token) {
setAccessToken(token)
// Check if token is expired
const payload = JSON.parse(atob(token.split('.')[1]))
const expiresAt = payload.exp * 1000
const now = Date.now()
if (expiresAt < now) {
// Token expired, refresh immediately
refreshAccessToken()
} else {
// Schedule refresh before expiry
const refreshTime = expiresAt - now - 300000 // 5 min before
setTimeout(refreshAccessToken, Math.max(0, refreshTime))
}
}
}, [refreshAccessToken])
const handleLogout = () => {
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
setAccessToken(null)
window.location.href = '/login'
}
return {
accessToken,
refreshAccessToken,
logout: handleLogout,
isAuthenticated: !!accessToken
}
}
// Usage
function App() {
const { accessToken, isAuthenticated, logout } = useAuth()
if (!isAuthenticated) {
return <LoginPage />
}
return (
<div>
<button onClick={logout}>Logout</button>
<Dashboard accessToken={accessToken!} />
</div>
)
}Axios Interceptor
typescript
import axios from 'axios'
const api = axios.create({
baseURL: 'https://api.dedot.io/v1'
})
let isRefreshing = false
let failedQueue: any[] = []
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error)
} else {
prom.resolve(token)
}
})
failedQueue = []
}
// Request interceptor
api.interceptors.request.use(
config => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// Response interceptor
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Queue request until refresh completes
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
}).then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`
return api(originalRequest)
}).catch(err => Promise.reject(err))
}
originalRequest._retry = true
isRefreshing = true
try {
const refreshToken = localStorage.getItem('refreshToken')
const response = await axios.post(
'https://auth.dedot.io/v1/auth/refresh',
{ refreshToken }
)
const { accessToken } = response.data
localStorage.setItem('accessToken', accessToken)
api.defaults.headers.common.Authorization = `Bearer ${accessToken}`
originalRequest.headers.Authorization = `Bearer ${accessToken}`
processQueue(null, accessToken)
return api(originalRequest)
} catch (err) {
processQueue(err, null)
// Refresh failed, logout
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
window.location.href = '/login'
return Promise.reject(err)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
}
)
export default apiLogout
End user session and invalidate tokens.
typescript
POST https://auth.dedot.io/v1/auth/logout
Headers:
Authorization: Bearer <access_token>
Body:
{
"refreshToken": "refresh_abc123def456"
}Response:
json
{
"success": true,
"message": "Logged out successfully"
}Logout Implementation
typescript
async function logout() {
try {
const accessToken = localStorage.getItem('accessToken')
const refreshToken = localStorage.getItem('refreshToken')
if (accessToken && refreshToken) {
await fetch('https://auth.dedot.io/v1/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify({ refreshToken })
})
}
} catch (error) {
console.error('Logout error:', error)
} finally {
// Clear local storage regardless
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
// Redirect to login
window.location.href = '/login'
}
}Session Validation
Verify if access token is still valid.
typescript
GET https://auth.dedot.io/v1/auth/validate
Headers:
Authorization: Bearer <access_token>Response:
json
{
"valid": true,
"user": {
"id": "usr_123456",
"email": "[email protected]",
"userType": "client"
},
"expiresAt": "2026-01-16T11:00:00Z"
}Validation Implementation
typescript
async function validateSession(): Promise<boolean> {
try {
const accessToken = localStorage.getItem('accessToken')
if (!accessToken) return false
const response = await fetch('https://auth.dedot.io/v1/auth/validate', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
const data = await response.json()
return data.valid
} catch (error) {
return false
}
}
// Use in protected routes
async function ProtectedRoute({ children }: { children: React.ReactNode }) {
const [isValid, setIsValid] = useState<boolean | null>(null)
useEffect(() => {
validateSession().then(setIsValid)
}, [])
if (isValid === null) {
return <div>Loading...</div>
}
if (!isValid) {
return <Navigate to="/login" />
}
return <>{children}</>
}Token Payload
Access tokens contain user information:
typescript
{
"sub": "usr_123456", // User ID
"email": "[email protected]",
"userType": "client",
"workspace": "wks_789",
"permissions": ["read", "write"],
"iat": 1705401600, // Issued at
"exp": 1705405200 // Expires at
}Decode Token
typescript
function decodeToken(token: string) {
try {
const payload = token.split('.')[1]
const decoded = JSON.parse(atob(payload))
return decoded
} catch (error) {
console.error('Invalid token:', error)
return null
}
}
// Usage
const token = localStorage.getItem('accessToken')
if (token) {
const payload = decodeToken(token)
console.log('User ID:', payload.sub)
console.log('Expires:', new Date(payload.exp * 1000))
}Security Best Practices
Do
- Store refresh tokens in httpOnly cookies
- Keep access tokens in memory (not localStorage)
- Implement auto-refresh before expiry
- Use short-lived access tokens (1 hour)
- Validate tokens on protected routes
- Clear tokens on logout
Don't
- Don't store tokens in localStorage if avoidable
- Don't share tokens across domains
- Don't log tokens or include in URLs
- Don't skip token validation
- Don't use expired tokens
- Don't trust client-side token validation alone
Multi-Device Sessions
List and manage active sessions across devices.
typescript
GET https://auth.dedot.io/v1/auth/sessions
Headers:
Authorization: Bearer <access_token>Response:
json
{
"sessions": [
{
"id": "sess_123",
"device": "Chrome on MacOS",
"ip": "192.168.1.100",
"lastActive": "2026-01-16T10:30:00Z",
"current": true
},
{
"id": "sess_456",
"device": "Safari on iPhone",
"ip": "192.168.1.101",
"lastActive": "2026-01-16T09:15:00Z",
"current": false
}
]
}Revoke Session
typescript
DELETE https://auth.dedot.io/v1/auth/sessions/{sessionId}
Headers:
Authorization: Bearer <access_token>Implementation
typescript
function SessionManager() {
const [sessions, setSessions] = useState([])
useEffect(() => {
fetchSessions()
}, [])
const fetchSessions = async () => {
const response = await fetch('https://auth.dedot.io/v1/auth/sessions', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
const data = await response.json()
setSessions(data.sessions)
}
const revokeSession = async (sessionId: string) => {
await fetch(`https://auth.dedot.io/v1/auth/sessions/${sessionId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
fetchSessions()
}
return (
<div>
<h3>Active Sessions</h3>
{sessions.map(session => (
<div key={session.id}>
<strong>{session.device}</strong>
{session.current && <span>(Current)</span>}
<p>Last active: {session.lastActive}</p>
{!session.current && (
<button onClick={() => revokeSession(session.id)}>
Revoke
</button>
)}
</div>
))}
</div>
)
}Token Expiry Handling
Frontend Token Manager
typescript
class TokenManager {
private accessToken: string | null = null
private refreshToken: string | null = null
private refreshTimer: NodeJS.Timeout | null = null
constructor() {
this.loadTokens()
}
private loadTokens() {
this.accessToken = localStorage.getItem('accessToken')
this.refreshToken = localStorage.getItem('refreshToken')
if (this.accessToken) {
this.scheduleRefresh()
}
}
private scheduleRefresh() {
if (!this.accessToken) return
const payload = this.decodeToken(this.accessToken)
if (!payload) return
const expiresAt = payload.exp * 1000
const now = Date.now()
const refreshTime = expiresAt - now - 300000 // 5 min before expiry
if (refreshTime > 0) {
this.refreshTimer = setTimeout(() => {
this.refresh()
}, refreshTime)
} else {
// Already expired, refresh immediately
this.refresh()
}
}
private async refresh() {
try {
const response = await fetch('https://auth.dedot.io/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
})
const data = await response.json()
if (data.success) {
this.setAccessToken(data.accessToken)
this.scheduleRefresh()
} else {
this.logout()
}
} catch (error) {
console.error('Token refresh failed:', error)
this.logout()
}
}
setAccessToken(token: string) {
this.accessToken = token
localStorage.setItem('accessToken', token)
}
setRefreshToken(token: string) {
this.refreshToken = token
localStorage.setItem('refreshToken', token)
}
getAccessToken(): string | null {
return this.accessToken
}
logout() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer)
}
this.accessToken = null
this.refreshToken = null
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
window.location.href = '/login'
}
private decodeToken(token: string) {
try {
const payload = token.split('.')[1]
return JSON.parse(atob(payload))
} catch {
return null
}
}
}
// Create singleton instance
export const tokenManager = new TokenManager()
