Skip to main content

Overview

The Rodando Passenger app implements a dual authentication flow that adapts to platform:
  • Mobile apps use refresh tokens stored in secure storage
  • Web apps use HttpOnly cookies for enhanced security
Both flows share the same AuthService and AuthFacade, ensuring consistent behavior across platforms.

Core Services

AuthService

The AuthService handles HTTP communication with the authentication API. Location: src/app/core/services/http/auth.service.ts

Key Methods

login
(payload: LoginPayload, httpOptions?) => Observable<LoginResponse>
Authenticates a user with email/phone and password.Payload Structure:
interface LoginPayload {
  email?: string;
  phoneNumber?: string;
  password: string;
  appAudience: 'driver_app' | 'passenger_app' | 'admin_panel' | 'api_client';
  expectedUserType: 'passenger' | 'driver' | 'admin';
  location?: Location;
}
Response (Mobile):
interface LoginResponseMobile {
  accessToken: string;
  refreshToken: string;  // Stored in secure storage
  accessTokenExpiresAt: number;  // Epoch milliseconds
  sessionType: 'mobile_app';
}
Response (Web):
interface LoginResponseWeb {
  accessToken: string;
  accessTokenExpiresAt: number;
  sessionType: 'web' | 'api_client';
  // refreshToken sent as HttpOnly cookie
}
refresh
(refreshToken?: string, useCookie?: boolean) => Observable<RefreshResponse>
Refreshes the access token using either a refresh token (mobile) or cookie (web).Mobile Flow:
authService.refresh(refreshToken, false).subscribe(res => {
  // res.accessToken: new access token
  // res.refreshToken: new refresh token (rotated)
});
Web Flow:
authService.refresh(undefined, true).subscribe(res => {
  // res.accessToken: new access token
  // Cookie automatically sent with withCredentials: true
});
me
(useCookie: boolean) => Observable<UserProfile>
Fetches the current user’s profile from /users/profile.Returns:
interface UserProfile {
  id: string;
  name: string;
  email?: string | null;
  phoneNumber?: string | null;
  userType: 'passenger' | 'driver' | 'admin';
  profilePictureUrl?: string | null;
  currentLocation?: {
    type: 'Point';
    coordinates: [number, number];  // [lng, lat]
  };
}
logoutWeb
() => Observable<void>
Logs out a web user by clearing the HttpOnly refresh cookie on the server.
logoutMobile
(refreshToken: string) => Observable<void>
Logs out a mobile user by invalidating the refresh token on the server.

State Management

AuthFacade

The AuthFacade orchestrates authentication flows, token management, and automatic refresh scheduling. Location: src/app/store/auth/auth.facade.ts

Login Flow

src/app/pages/login.component.ts
import { AuthFacade } from '@/app/store/auth/auth.facade';

export class LoginComponent {
  private authFacade = inject(AuthFacade);
  
  onLogin(email: string, password: string) {
    const payload: LoginPayload = {
      email,
      password,
      appAudience: 'passenger_app',
      expectedUserType: 'passenger'
    };
    
    this.authFacade.login(payload).subscribe({
      next: (user) => {
        console.log('Logged in:', user);
        // AuthFacade automatically:
        // - Stores accessToken in memory
        // - Stores refreshToken in secure storage
        // - Schedules auto-refresh before expiration
        // - Fetches user profile
      },
      error: (err: ApiError) => {
        console.error('Login failed:', err.message);
      }
    });
  }
}

Automatic Token Refresh

The AuthFacade automatically schedules token refresh 30 seconds before expiration:
// Automatically called by AuthFacade
private scheduleAutoRefresh(expiresAt: number): void {
  const now = Date.now();
  const ttl = expiresAt - now;
  const offset = Math.min(30_000, Math.floor(ttl / 2));
  const msUntilRefresh = ttl - offset;
  
  this.refreshTimerId = setTimeout(() => {
    this.performRefresh().pipe(take(1)).subscribe();
  }, msUntilRefresh);
}
The auto-refresh mechanism ensures users stay authenticated without manual intervention. If refresh fails (e.g., refresh token expired), the user is automatically logged out.

Session Restoration

On app startup, the AuthFacade attempts to restore the session:
src/app/app.initializer.ts
import { AuthFacade } from '@/app/store/auth/auth.facade';

export function initializeAuth(authFacade: AuthFacade) {
  return async () => {
    try {
      await authFacade.restoreSession();
      // If successful:
      // - Restores sessionType from localStorage
      // - Performs silent refresh if needed
      // - Fetches user profile
      // - Schedules auto-refresh
    } catch (err) {
      console.warn('Session restoration failed:', err);
      // User will see login screen
    }
  };
}

Logout

authFacade.logoutMobileFlow().subscribe(() => {
  // AuthFacade automatically:
  // - Calls /auth/logout with refreshToken
  // - Clears secure storage
  // - Clears in-memory state
  // - Cancels auto-refresh timer
  // - Navigates to /auth/login
});

Token Storage

Mobile: Secure Storage

Refresh tokens are stored using SecureStorageService (typically backed by iOS Keychain or Android Keystore):
// Store refresh token
await firstValueFrom(
  this.secureStorage.save('refreshToken', refreshToken)
);

// Retrieve refresh token
const token = await firstValueFrom(
  this.secureStorage.load('refreshToken')
);

// Remove refresh token
await firstValueFrom(
  this.secureStorage.remove('refreshToken')
);

Web: HttpOnly Cookies

Web apps rely on HttpOnly cookies set by the server. The client never has direct access to the refresh token:
// All requests with withCredentials: true
this.http.post('/auth/refresh', {}, { withCredentials: true });
Never store refresh tokens in localStorage or sessionStorage on web platforms. HttpOnly cookies prevent XSS attacks from stealing refresh tokens.

Error Handling

The AuthService normalizes all errors to ApiError:
interface ApiError {
  message: string;
  status?: number;
  code?: string;  // 'INVALID_CREDENTIALS', 'EMAIL_NOT_VERIFIED', etc.
  validation?: Record<string, string[]>;  // Field-level errors
  raw?: any;
  url?: string | null;
}
Example: Handling validation errors
this.authFacade.login(payload).subscribe({
  error: (err: ApiError) => {
    if (err.code === 'INVALID_CREDENTIALS') {
      this.showError('Invalid email or password');
    } else if (err.validation?.email) {
      this.showError(err.validation.email[0]);
    } else {
      this.showError(err.message);
    }
  }
});

Best Practices

Use AuthFacade, not AuthService

Always interact with AuthFacade instead of calling AuthService directly. The facade handles token persistence, scheduling, and state management.

Check authentication in guards

Use Angular route guards to protect authenticated routes:
export const authGuard: CanActivateFn = () => {
  const authStore = inject(AuthStore);
  const router = inject(Router);
  
  if (!authStore.accessToken() || !authStore.user()) {
    return router.createUrlTree(['/auth/login']);
  }
  return true;
};

Handle token expiration gracefully

The auto-refresh mechanism handles expiration, but always handle refresh failures:
// In HTTP interceptor
if (error.status === 401) {
  // Redirect to login
  authFacade.clearAll();
}
The AuthFacade integrates with PassengerLocationReporter to automatically start/stop location tracking on login/logout.

Build docs developers (and LLMs) love