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]
};
}
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 );
}
});
}
}
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
// - Server sets HttpOnly cookie with refreshToken
// - 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
});
authFacade . logoutWebFlow (). subscribe (() => {
// AuthFacade automatically:
// - Calls /auth/logout (clears HttpOnly cookie)
// - 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.