Building an API that third-party applications can access securely requires OAuth2. Laravel Passport makes OAuth2 server implementation trivial, providing authorization code grants, personal access tokens, and client credentials. At ZIRA Software, Passport powers our platform APIs, enabling hundreds of third-party integrations securely.
Why Laravel Passport?
OAuth2 use cases:
- Third-party app integration ("Login with YourApp")
- Mobile app authentication
- Personal access tokens (like GitHub tokens)
- Server-to-server API access
- JavaScript SPAs
- Microservices authentication
Passport provides:
- Full OAuth2 server
- Authorization code grant
- Implicit grant (deprecated)
- Password grant
- Client credentials grant
- Personal access tokens
- Token scopes
- Token management UI
- Refresh tokens
Installation
Laravel 5.3+:
composer require laravel/passport
Run migrations:
php artisan migrate
Install Passport:
php artisan passport:install
This creates encryption keys and OAuth2 clients.
Setup
User model:
<?php namespace App;
use Laravel\Passport\HasApiTokens;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens;
}
AuthServiceProvider:
<?php namespace App\Providers;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
public function boot()
{
$this->registerPolicies();
Passport::routes();
// Token lifetimes
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}
}
config/auth.php:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport', // Changed from 'token'
'provider' => 'users',
],
],
Personal Access Tokens
Create tokens for users:
$user = User::find(1);
$token = $user->createToken('My Token');
echo $token->accessToken; // The token string
echo $token->token->id; // Token ID
With scopes:
$token = $user->createToken('My Token', ['place-orders', 'check-status']);
Token management UI:
Add to routes:
Passport::routes();
Visit /oauth/personal-access-tokens to manage tokens.
API usage:
curl -H "Authorization: Bearer YOUR_TOKEN_HERE" \
https://api.yourapp.com/api/user
Protected routes:
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Authorization Code Grant
For third-party apps accessing user data.
1. Create OAuth Client
php artisan passport:client
Choose "Authorization code grant" and provide:
- Name: My Application
- Redirect URL: https://myapp.com/callback
2. Authorization Request
Third-party app redirects user to:
https://yourapp.com/oauth/authorize?
client_id=CLIENT_ID&
redirect_uri=https://myapp.com/callback&
response_type=code&
scope=place-orders check-status
User sees consent screen and approves.
3. Authorization Callback
Your app redirects back with code:
https://myapp.com/callback?code=AUTH_CODE
4. Exchange Code for Token
Third-party app makes request:
curl -X POST https://yourapp.com/oauth/token \
-d "grant_type=authorization_code" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "redirect_uri=https://myapp.com/callback" \
-d "code=AUTH_CODE"
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh_token": "def50200e4da9b...",
"token_type": "Bearer",
"expires_in": 1296000
}
5. Access Protected Resources
curl -H "Authorization: Bearer ACCESS_TOKEN" \
https://yourapp.com/api/user
Custom Authorization Page
resources/views/vendor/passport/authorize.blade.php:
@extends('layouts.app')
@section('content')
<div class="container">
<h1>Authorization Request</h1>
<p><strong>{{ $client->name }}</strong> is requesting access to your account.</p>
<h3>This application will be able to:</h3>
<ul>
@foreach ($scopes as $scope)
<li>{{ $scope->description }}</li>
@endforeach
</ul>
<form method="POST" action="{{ route('passport.authorizations.approve') }}">
@csrf
<input type="hidden" name="state" value="{{ $request->state }}">
<input type="hidden" name="client_id" value="{{ $client->id }}">
<button type="submit" class="btn btn-success">Authorize</button>
</form>
<form method="POST" action="{{ route('passport.authorizations.deny') }}">
@csrf
@method('DELETE')
<input type="hidden" name="state" value="{{ $request->state }}">
<input type="hidden" name="client_id" value="{{ $client->id }}">
<button type="submit" class="btn btn-danger">Cancel</button>
</form>
</div>
@endsection
Scopes
Define scopes:
use Laravel\Passport\Passport;
Passport::tokensCan([
'place-orders' => 'Place orders',
'check-status' => 'Check order status',
'manage-account' => 'Manage account information',
]);
Passport::setDefaultScope([
'check-status',
]);
Check scopes in routes:
Route::middleware(['auth:api', 'scope:place-orders'])->post('/orders', function () {
// User has 'place-orders' scope
});
Route::middleware(['auth:api', 'scopes:check-status,place-orders'])->get('/orders', function () {
// User has both scopes
});
Check in controller:
if ($request->user()->tokenCan('place-orders')) {
// User has permission
}
Client Credentials Grant
For server-to-server API access (no user).
Create Client
php artisan passport:client --client
Request Token
curl -X POST https://yourapp.com/oauth/token \
-d "grant_type=client_credentials" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "scope=*"
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"expires_in": 31536000
}
Protected Routes
Route::middleware('client')->get('/stats', function (Request $request) {
// Authenticated via client credentials
return ['total_users' => User::count()];
});
Password Grant
For first-party apps (your own mobile/desktop apps).
Create Password Client
php artisan passport:client --password
Request Token
curl -X POST https://yourapp.com/oauth/token \
-d "grant_type=password" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "username=user@example.com" \
-d "password=secret" \
-d "scope=*"
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"refresh_token": "def50200e4da9b...",
"token_type": "Bearer",
"expires_in": 31536000
}
Request Token from App
JavaScript:
axios.post('/oauth/token', {
grant_type: 'password',
client_id: 'CLIENT_ID',
client_secret: 'CLIENT_SECRET',
username: email,
password: password,
scope: '*',
})
.then(response => {
localStorage.setItem('access_token', response.data.access_token);
localStorage.setItem('refresh_token', response.data.refresh_token);
});
Refresh Tokens
Get new access token:
curl -X POST https://yourapp.com/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=REFRESH_TOKEN" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "scope=*"
JavaScript:
function refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
axios.post('/oauth/token', {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'CLIENT_ID',
client_secret: 'CLIENT_SECRET',
scope: '*',
})
.then(response => {
localStorage.setItem('access_token', response.data.access_token);
localStorage.setItem('refresh_token', response.data.refresh_token);
});
}
Revoking Tokens
Revoke token:
$user->token()->revoke();
Revoke all tokens:
$user->tokens->each(function ($token) {
$token->revoke();
});
Frontend revocation:
axios.delete('/oauth/tokens/' + tokenId)
.then(response => {
console.log('Token revoked');
});
Token Management
Get user's tokens:
$tokens = $user->tokens;
foreach ($tokens as $token) {
echo $token->name;
echo $token->scopes;
echo $token->revoked;
echo $token->created_at;
echo $token->expires_at;
}
Check if token is valid:
if ($token->revoked || $token->expires_at->isPast()) {
// Token invalid
}
JavaScript SPA Authentication
Vue.js example:
// Login
methods: {
login() {
axios.post('/oauth/token', {
grant_type: 'password',
client_id: process.env.VUE_APP_CLIENT_ID,
client_secret: process.env.VUE_APP_CLIENT_SECRET,
username: this.email,
password: this.password,
scope: '*',
})
.then(response => {
this.$store.commit('setToken', response.data.access_token);
this.$router.push('/dashboard');
})
.catch(error => {
alert('Login failed');
});
}
}
// Configure axios to include token
axios.defaults.headers.common['Authorization'] = 'Bearer ' + store.state.token;
// API request
axios.get('/api/user')
.then(response => {
console.log(response.data);
});
Testing
tests/Feature/OAuthTest.php:
<?php
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class OAuthTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_authenticates_with_personal_access_token()
{
$user = factory(User::class)->create();
$token = $user->createToken('Test Token')->accessToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
])->json('GET', '/api/user');
$response->assertStatus(200);
$response->assertJson([
'id' => $user->id,
'email' => $user->email,
]);
}
/** @test */
public function it_checks_token_scopes()
{
$user = factory(User::class)->create();
$token = $user->createToken('Test Token', ['check-status'])->accessToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
])->json('POST', '/api/orders');
$response->assertStatus(403); // Missing 'place-orders' scope
}
}
Using Passport::actingAs():
public function test_user_can_view_orders()
{
Passport::actingAs(
factory(User::class)->create(),
['check-status', 'place-orders']
);
$response = $this->get('/api/orders');
$response->assertStatus(200);
}
Mobile App Flow
iOS/Android authentication:
- App shows login screen
- User enters credentials
- App requests token via password grant
- App stores access token securely (Keychain/KeyStore)
- App includes token in API requests
- App refreshes token when needed
Swift example:
func login(email: String, password: String, completion: @escaping (Bool) -> Void) {
let parameters = [
"grant_type": "password",
"client_id": "CLIENT_ID",
"client_secret": "CLIENT_SECRET",
"username": email,
"password": password,
"scope": "*"
]
Alamofire.request("https://api.yourapp.com/oauth/token",
method: .post,
parameters: parameters)
.responseJSON { response in
if let json = response.result.value as? [String: Any],
let accessToken = json["access_token"] as? String {
KeychainWrapper.standard.set(accessToken, forKey: "access_token")
completion(true)
} else {
completion(false)
}
}
}
Security Best Practices
- Use HTTPS only - OAuth2 requires secure connections
- Keep secrets secret - Never expose client secrets
- Short token lifetimes - Refresh frequently
- Validate redirect URIs - Prevent token hijacking
- Use scopes - Limit access appropriately
- Revoke tokens - When user logs out or changes password
- Rate limit - Prevent brute force attacks
Production Considerations
Store keys securely:
# Production environment
php artisan passport:keys --force
Use environment variables:
PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..."
Prune revoked tokens:
# Schedule in Kernel.php
$schedule->command('passport:purge')->daily();
Common Issues
"Unauthenticated" error:
- Check token in Authorization header
- Verify token hasn't expired
- Ensure api guard uses 'passport' driver
Scopes not working:
- Check middleware order
- Verify scope names match definitions
Tokens not refreshing:
- Check refresh token hasn't expired
- Verify client credentials
Alternatives
For simpler needs:
- Sanctum - SPA and mobile app authentication (lighter than Passport)
- JWT - Manual JWT implementation
Use Passport when:
- Third-party app integration required
- Full OAuth2 compliance needed
- Multiple grant types needed
Conclusion
Laravel Passport provides full OAuth2 server functionality with minimal configuration. From personal access tokens for simple APIs to authorization code grants for third-party integrations, Passport handles authentication elegantly. At ZIRA Software, Passport has enabled secure API access for hundreds of partner integrations.
Start with personal access tokens. Add OAuth2 flows as needed. Your API will be secure, scalable, and standards-compliant.
Building APIs that third-party applications will access? Contact ZIRA Software to discuss OAuth2 implementation, API security strategies, and scalable authentication architectures.