Skip to main content
The Rodando Driver app implements a comprehensive authentication system that supports both web and mobile session types, with different token management strategies for each platform.

Authentication Flow

The authentication process follows a standard OAuth2-like flow with JWT tokens and refresh token rotation:
1

User Login

The driver enters credentials (email/password) through the login form.
2

Token Issuance

The backend validates credentials and returns:
  • accessToken - Short-lived JWT for API requests
  • refreshToken - Long-lived token (mobile only)
  • sessionType - Either 'web' or 'mobile'
  • accessTokenExpiresAt - Unix timestamp for token expiry
3

Token Storage

Tokens are stored based on session type:
  • Web: refreshToken stored in HttpOnly cookies
  • Mobile: Both tokens stored in localStorage
4

Authenticated Requests

The accessToken is attached to all API requests via the Authorization header.

Session Types

The app supports two distinct session types with different security models:
Mobile sessions store the refresh token locally and send it in request bodies.
interface RefreshResponseMobile {
  accessToken: string;
  refreshToken: string;  // New refresh token (rotation)
  accessTokenExpiresAt: number;
}
Characteristics:
  • Refresh tokens sent in request body
  • Token rotation on every refresh
  • Stored in localStorage
  • Longer token lifetime

Login Implementation

The login process is handled by the AuthService with automatic response validation:
login(payload: LoginPayload, httpOptions?: { withCredentials?: boolean }): Observable<LoginResponse> {
  const url = `${this.baseUrl}/auth/login`;
  return this.http.post<ApiResponse<LoginResponse>>(url, payload, httpOptions).pipe(
    map(resp => this.unwrap<ApiResponse<LoginResponse>, LoginResponse>(resp, url)),
    // Validate required fields
    map(res => {
      if (!res?.accessToken || !res?.sessionType) {
        throw new Error('Login response malformed: missing accessToken or sessionType');
      }
      return res;
    }),
    catchError(err => this.handleErrorAsApiError(err, url))
  );
}
Login Payload:
interface LoginPayload {
  email: string;
  password: string;
  audience?: 'driver_app' | 'passenger_app' | 'admin_panel';
}

Token Refresh

The unified refresh() method handles both web and mobile token refresh automatically:
// Refresh using HttpOnly cookie
refresh(refreshToken?: string, useCookie = true): Observable<RefreshResponse> {
  const url = `${this.baseUrl}/auth/refresh`;

  if (useCookie) {
    // WEB/API_CLIENT (cookie HttpOnly)
    return this.http.post<ApiResponse<RefreshResponseWeb>>(
      url, 
      {}, 
      { withCredentials: true }
    ).pipe(
      map(resp => this.unwrap<ApiResponse<RefreshResponseWeb>, RefreshResponseWeb>(resp, url)),
      map(res => this.validateRefreshWebResponse(res, url)),
      catchError(err => this.handleErrorAsApiError(err, url))
    );
  }
}
The refresh method automatically validates that accessTokenExpiresAt is present and that the response contains all required fields before returning.

Logout

Logout is handled differently for each session type:
logoutWeb(): Observable<void> {
  const url = `${this.baseUrl}/auth/logout`;
  return this.http.post<void>(url, {}, { withCredentials: true }).pipe(
    catchError(err => this.handleErrorAsApiError(err, url))
  );
}
Web logout clears the HttpOnly cookie on the server side.

User Profile

Retrieve the authenticated driver’s profile information:
me(useCookie: boolean = true): Observable<UserProfile> {
  const url = `${this.baseUrl}/users/profile`;
  const options = useCookie ? { withCredentials: true } : {};
  return this.http
    .get<{ success: boolean; message?: string; data?: UserProfile }>(url, options)
    .pipe(
      map(res => {
        if (!res || typeof res.data !== 'object' || res.data === null) {
          throw new Error('Profile response malformed');
        }
        return res.data as UserProfile;
      }),
      catchError(err => this.handleErrorAsApiError(err, url))
    );
}
UserProfile Interface:
interface UserProfile {
  id: string | null;
  name: string | null;
  email: string | null;
  phoneNumber?: string | null;
  profilePictureUrl?: string | null;
  createdAt?: string;
}

Error Handling

The authentication service includes comprehensive error normalization:
private handleErrorAsApiError(err: any, requestUrl?: string) {
  return from(normalizeAnyError(err, requestUrl)).pipe(
    mergeMap((apiErr: ApiError) => throwError(() => apiErr))
  );
}
ApiError Structure:
interface ApiError {
  status?: number;      // HTTP status code
  message: string;      // Human-readable error message
  code?: string;        // Application error code
  validation?: any;     // Validation errors
  raw?: any;           // Raw error response
  url?: string | null; // Request URL
}
Network Errors: When a network error occurs (status 0 or ProgressEvent), the error handler returns a user-friendly message: “Network error: please check your connection.”

Response Validation

All authentication responses are validated to ensure data integrity:
private validateRefreshWebResponse(
  res: RefreshResponseWeb | null | undefined, 
  requestUrl: string
): RefreshResponseWeb {
  if (!res || typeof res.accessToken !== 'string') {
    throw new Error(`Respuesta de refresh (web) malformada: ${requestUrl}`);
  }
  if (typeof (res as any).accessTokenExpiresAt !== 'number') {
    throw new Error(`Missing accessTokenExpiresAt in refresh (web): ${requestUrl}`);
  }
  return res;
}

Security Best Practices

HttpOnly Cookies

Web sessions use HttpOnly cookies to prevent XSS attacks from accessing refresh tokens.

Token Rotation

Mobile sessions implement refresh token rotation - a new refresh token is issued with each refresh request.

Short-Lived Access Tokens

Access tokens expire quickly (typically 15-30 minutes) to minimize the impact of token theft.

Error Normalization

All errors are normalized to prevent sensitive information leakage in error messages.

Build docs developers (and LLMs) love