Skip to content

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 api

Logout

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()

Next Steps