Skip to content

Trip Access Authentication

Passwordless authentication for passengers to access their trip details via personalized magic links. Allows travel agents to share trip information directly with passengers, independent of the client authentication system.

+-------------------+ +-------------------+ +-------------------+
| Filament Admin | | Laravel Backend | | Astro Frontend |
| (Backoffice) | | | | (Trip Page) |
+-------------------+ +-------------------+ +-------------------+
| | |
| "Share Trip with Pax" | |
|----------------------------->| |
| | |
| | Magic Link Email |
| |----------------------------->|
| | |
| | Verify Token |
| |<-----------------------------|
| | |
| | Session + Redirect |
| |----------------------------->|
| | |
AspectClient AuthTrip Access
Target userClients (booking owners)Passengers (travelers)
ScopeAll client bookingsSingle booking only
Token storageclients + SanctumSanctum personal_access_tokens (polymorphic)
Token ownerClient modelPassenger model
Duration7 days90 days (default)
Use caseClient portalTrip itinerary page
1. Agent clicks "Share Trip with Pax" in Filament booking view
|
v
2. Agent selects passengers (checkbox list)
- Only passengers with email addresses shown
- All selected by default
- Lead passenger marked with badge
|
v
3. For each selected passenger:
- TripAccessTokenService creates Sanctum token
- Previous tokens for same booking revoked
- TripAccessNotification queued
- Email sent with HMAC-signed magic link
|
v
4. Passenger clicks email link -> /auth/trip?token={encoded}
|
v
5. POST /api/trip/verify
- Decodes base64 payload (token, booking_reference, passenger_id, signature)
- Validates HMAC signature (prevents tampering)
- Finds valid Sanctum token via TripAccessTokenService
- Verifies booking_reference and passenger_id match token abilities
- Marks token as used
- Returns booking details + access_token
|
v
6. Astro stores in server session:
- bookingReference: string
- accessToken: string (Sanctum token for subsequent calls)
- expiresAt: timestamp
- passengerName: string (for personalized greeting)
|
v
7. Redirect to /{market}/trip/{reference}
- Page shows personalized greeting
- Trip details rendered from session

The magic link payload includes an HMAC-SHA256 signature to prevent tampering:

$payload = [
'token' => $plainTextToken,
'booking_reference' => $booking->booking_reference,
'passenger_id' => $passenger->id,
];
$signature = hash_hmac('sha256', json_encode($payload), config('app.key'));

The signature is verified before any database lookup, preventing timing attacks.

PropertyValuePurpose
Storagepersonal_access_tokens tableStandard Sanctum polymorphic storage
TokenablePassenger modelToken owner is the passenger
Nametrip-access:{booking_reference}Identifies token purpose and booking
Abilitiestrip-access, booking:{id}, passenger:{id}Scoped permissions
Expiry90 days defaultConfigurable via DEFAULT_VALIDITY_DAYS
Single-useNoToken can be reused until expiry

Each token is created with abilities that encode the access context:

$abilities = [
'trip-access', // Identifies as trip access token
'booking:' . $bookingId, // Links to specific booking
'passenger:' . $passengerId, // Links to specific passenger
];

Trip access tokens use Laravel Sanctum’s personal_access_tokens table (polymorphic):

-- Sanctum's personal_access_tokens table
CREATE TABLE personal_access_tokens (
id BIGINT PRIMARY KEY,
tokenable_type VARCHAR(255), -- 'App\Models\Passenger' for trip access
tokenable_id BIGINT, -- Passenger ID
name VARCHAR(255), -- 'trip-access:{booking_reference}'
token VARCHAR(64) UNIQUE, -- Hashed token
abilities JSON, -- ['trip-access', 'booking:123', 'passenger:456']
last_used_at TIMESTAMP NULL,
expires_at TIMESTAMP NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
INDEX(tokenable_type, tokenable_id)
);

The service wraps Sanctum token operations with booking context:

use App\Services\TripAccessTokenService;
$tokenService = app(TripAccessTokenService::class);
// Create token for passenger
$tokenData = $tokenService->createForPassenger($booking, $passenger);
// Returns: ['token' => NewAccessToken, 'plain_text' => 'id|token']
// Find valid token
$token = $tokenService->findValidToken($plainTextToken);
// Returns: PersonalAccessToken or null
// Get booking ID from token abilities
$bookingId = $tokenService->getBookingIdFromToken($token);
// Get passenger from token
$passenger = $tokenService->getPassengerFromToken($token);
// Revoke tokens for specific booking
$tokenService->revokeTokensForBooking($passenger, $booking);
// Revoke all trip tokens for passenger
$tokenService->revokeAllTokens($passenger);
POST /api/trip/verify

Rate Limit: 10 requests/minute

Request:

{
"token": "base64_encoded_payload"
}

Response (success):

{
"success": true,
"data": {
"booking": { /* BookingDetailsResource */ },
"passenger": {
"id": 4,
"name": "John Doe"
},
"access_token": "1|abc123def456...",
"expires_at": "2026-03-29T15:41:27.000000Z"
}
}

Error codes: invalid_token, expired_token

Source: backend/app/Http/Controllers/Api/Trip/AccessController.php:26

GET /api/trip/show?token={access_token}

Rate Limit: 60 requests/minute

Request: Sanctum token as query parameter.

Response: Fresh booking details for the authenticated passenger.

Source: backend/app/Http/Controllers/Api/Trip/AccessController.php:110

Located in ViewBooking.php, the action:

  1. Shows checkbox list of passengers with email addresses
  2. Displays passenger name, lead badge, and email
  3. Pre-selects all passengers
  4. Allows bulk select/deselect
  5. Sends emails to selected passengers only
Action::make('share_trip_with_pax')
->label('Share Trip with Pax')
->icon(Heroicon::OutlinedEnvelope)
->color('gray')
->form(fn (): array => $this->getShareTripForm())
->action(fn (array $data) => $this->shareWithSelectedPassengers($data['passengers'] ?? []))

Source: backend/app/Filament/Resources/Bookings/Pages/ViewBooking.php:28

The trip page supports two access methods:

MethodURL PatternUse Case
Admin Preview/{market}/trip/{ref}?preview={signed_token}Short-lived (60 min) signed URL for backoffice preview
Passenger Session/{market}/trip/{ref}Long-lived session from magic link verification
// Trip page access check
if (previewToken) {
// Admin preview via signed URL
booking = await getBookingByPreviewToken(previewToken, clientIp);
} else if (tripAccess && tripAccess.bookingReference === reference) {
// Passenger session access
const result = await getTripDetails(tripAccess.accessToken);
booking = result.data?.booking;
}

Source: frontend/src/pages/[market]/trip/[reference].astro:46

The TripAccessNotification sends a branded email with:

  • Personalized greeting (passenger name)
  • Trip summary (destination, dates)
  • CTA button linking to /auth/trip?token={encoded}
  • Expiry information

Source: backend/app/Notifications/TripAccessNotification.php

EndpointLimitPurpose
POST /api/trip/verify10/minPrevent brute force
GET /api/trip/show60/minNormal usage
FilePurpose
backend/app/Services/TripAccessTokenService.phpToken management service
backend/app/Models/Passenger.phpUses HasApiTokens trait for Sanctum
backend/app/Http/Controllers/Api/Trip/AccessController.phpAPI endpoints
backend/app/Notifications/TripAccessNotification.phpEmail notification
backend/app/Filament/Resources/Bookings/Pages/ViewBooking.phpAdmin action
frontend/src/pages/auth/trip.astroMagic link verification page
frontend/src/pages/[market]/trip/[reference].astroTrip itinerary page
frontend/src/features/trip-access/services/tripAccessApi.tsFrontend API client
backend/routes/api.phpRoute definitions (trip prefix)
  • HMAC verification: Payload signed with app.key, prevents tampering
  • Sanctum validation: Token must exist and not be expired
  • Abilities verification: Token abilities must contain correct booking/passenger IDs
  • Activity logging: Token usage tracked via last_used_at
  • Token rotation: Re-sharing revokes previous tokens for same booking/passenger
  • Single booking: Each token grants access to one booking only
  • No modification: Read-only access to trip details
  • No cascading: Compromised token doesn’t expose other bookings
  • Isolated tokens: Tokens for different bookings are independent
  • Trip page at /{market}/trip/{reference} is not publicly accessible
  • Requires either valid preview token OR active session from magic link
  • Session tied to specific booking reference