Overview
The SUNAT Electronic Invoicing API uses Laravel Sanctum for API token authentication. Sanctum provides a lightweight authentication system for SPAs, mobile applications, and simple token-based APIs.
Sanctum tokens are stored in the personal_access_tokens table and can have specific abilities (permissions) assigned to them.
Authentication Flow
System Initialization
Before any users can authenticate, the system must be initialized with a super admin account.
Check System Status
GET /api/auth/system-info
Returns whether the system is initialized and ready for use. {
"system_initialized" : false ,
"user_count" : 0 ,
"roles_count" : 0 ,
"database_connected" : true
}
Initialize System
POST /api/auth/initialize
Creates the first super admin user and seeds roles/permissions. {
"name" : "Admin User" ,
"email" : "[email protected] " ,
"password" : "SecurePassword123!"
}
Response: {
"message" : "Sistema inicializado exitosamente" ,
"user" : {
"id" : 1 ,
"name" : "Admin User" ,
"email" : "[email protected] " ,
"role" : "Super Administrador"
},
"access_token" : "1|sunat_a1b2c3..." ,
"token_type" : "Bearer"
}
User Login
Endpoint: POST /api/auth/login
Request:
Response:
{
"message" : "Login exitoso" ,
"user" : {
"id" : 1 ,
"name" : "John Doe" ,
"email" : "[email protected] " ,
"role" : "Administrator" ,
"company_id" : 5 ,
"permissions" : [ "invoices.create" , "invoices.view" , "*" ]
},
"access_token" : "2|sunat_xyz123..." ,
"token_type" : "Bearer"
}
Implementation: app/Http/Controllers/Api/AuthController.php:80
app/Http/Controllers/Api/AuthController.php
public function login ( Request $request )
{
$request -> validate ([
'email' => 'required|email' ,
'password' => 'required' ,
]);
$user = User :: where ( 'email' , $request -> email ) -> first ();
if ( ! $user || ! Hash :: check ( $request -> password , $user -> password )) {
return response () -> json ([
'message' => 'Credenciales incorrectas' ,
'status' => 'error'
], 401 );
}
// Check if user is active
if ( ! $user -> active ) {
return response () -> json ([
'message' => 'Usuario inactivo' ,
], 401 );
}
// Check if user is locked
if ( $user -> isLocked ()) {
return response () -> json ([
'message' => 'Usuario bloqueado' ,
], 401 );
}
// Record successful login
$user -> recordSuccessfulLogin ( $request -> ip ());
// Create token with user's permissions
$abilities = $user -> role ? $user -> role -> getAllPermissions () : [ '*' ];
$token = $user -> createToken ( 'API_ACCESS_TOKEN' , $abilities ) -> plainTextToken ;
return response () -> json ([
'user' => [ ... ],
'access_token' => $token ,
'token_type' => 'Bearer'
]);
}
Token Usage
Include the token in the Authorization header for all authenticated requests:
curl -X GET https://api.example.com/api/invoices \
-H "Authorization: Bearer 2|sunat_xyz123..."
Endpoint: POST /api/auth/logout
Revokes the current access token.
public function logout ( Request $request )
{
$request -> user () -> currentAccessToken () -> delete ();
return response () -> json ([
'message' => 'Logout exitoso'
]);
}
User Model
Location: app/Models/User.php
User Attributes
protected $fillable = [
'name' ,
'email' ,
'password' ,
'role_id' , // Foreign key to roles table
'company_id' , // Foreign key to companies table
'user_type' , // 'system', 'user', 'api_client'
'allowed_ips' , // Array of allowed IP addresses
'permissions' , // Additional user-specific permissions
'last_login_at' , // Timestamp of last login
'last_login_ip' , // IP address of last login
'failed_login_attempts' , // Counter for security
'locked_until' , // Temporary account lock
'active' , // Boolean: is user active?
'force_password_change' , // Require password change on next login
];
User Types
Type Description systemSuper admin and system administrators userRegular company users api_clientProgrammatic API access only
User Roles and Permissions
Role Hierarchy
Location: app/Models/Role.php
Role :: create ([
'name' => 'super_admin' ,
'display_name' => 'Super Administrador' ,
'description' => 'Acceso total al sistema' ,
]);
Available Roles
Super Admin
Company Admin
Accountant
Operator
Name: super_adminPermissions: All (*)Description: Complete system access, can manage all companies and users.Key Methods: // Check if user is super admin
if ( $user -> hasRole ( 'super_admin' )) {
// Has all permissions
}
Name: adminPermissions:
companies.view
companies.update
users.create
users.view
invoices.*
boletas.*
Description: Company-level administrator, can manage company settings and users within their company.Name: accountantPermissions:
invoices.*
boletas.*
credit-notes.*
debit-notes.*
reports.view
Description: Can create and manage all types of electronic documents.Name: operatorPermissions:
invoices.create
invoices.view
boletas.create
boletas.view
Description: Basic document creation, cannot void or modify existing documents.
Permission Checking
Location: app/Models/User.php:93
public function hasPermission ( string $permission ) : bool
{
// Inactive users have no permissions
if ( ! $this -> active ) {
return false ;
}
// Check if locked
if ( $this -> isLocked ()) {
return false ;
}
// Super admin has all permissions
if ( $this -> role && $this -> role -> name === 'super_admin' ) {
return true ;
}
// Check user-specific permissions
if ( $this -> permissions && in_array ( $permission , $this -> permissions )) {
return true ;
}
// Check role permissions
if ( $this -> role && $this -> role -> hasPermission ( $permission )) {
return true ;
}
return false ;
}
Permission Examples
// Check single permission
if ( $user -> hasPermission ( 'invoices.create' )) {
// User can create invoices
}
// Check any of multiple permissions
if ( $user -> hasAnyPermission ([ 'invoices.create' , 'boletas.create' ])) {
// User can create invoices OR boletas
}
// Check all permissions
if ( $user -> hasAllPermissions ([ 'invoices.view' , 'invoices.create' ])) {
// User can both view AND create invoices
}
// Check role
if ( $user -> hasRole ( 'admin' )) {
// User is an admin
}
Security Features
Account Lockout
After 5 failed login attempts, accounts are locked for 30 minutes.
public function incrementFailedLoginAttempts () : void
{
$this -> increment ( 'failed_login_attempts' );
// Lock after 5 failed attempts
if ( $this -> failed_login_attempts >= 5 ) {
$this -> update ([
'locked_until' => now () -> addMinutes ( 30 )
]);
}
}
IP Whitelisting
Restrict user access to specific IP addresses or CIDR ranges.
public function isIpAllowed ( string $ip ) : bool
{
// If no restrictions, allow all
if ( ! $this -> allowed_ips ) {
return true ;
}
// Check exact IP match
if ( in_array ( $ip , $this -> allowed_ips )) {
return true ;
}
// Check CIDR ranges (e.g., "192.168.1.0/24")
foreach ( $this -> allowed_ips as $allowedIp ) {
if ( str_contains ( $allowedIp , '/' )) {
if ( $this -> ipInRange ( $ip , $allowedIp )) {
return true ;
}
}
}
return false ;
}
Token Expiration
Configuration: config/sanctum.php:50
'expiration' => env ( 'SANCTUM_EXPIRATION' , 1440 ), // 24 hours
'token_types' => [
'api' => [
'expiration' => 1440 , // 24 hours
'abilities' => [ '*' ],
],
'web' => [
'expiration' => 480 , // 8 hours
'abilities' => [ '*' ],
],
'mobile' => [
'expiration' => 10080 , // 7 days
'abilities' => [ '*' ],
],
'integration' => [
'expiration' => 43200 , // 30 days
'abilities' => [ 'invoices.create' , 'invoices.view' ],
],
],
Token Prefix
Sanctum tokens are prefixed with sunat_ for security scanning.
'token_prefix' => env ( 'SANCTUM_TOKEN_PREFIX' , 'sunat_' ),
Company Scoping
Users are scoped to their company. Super admins can access all companies.
public function canAccessCompany ( int $companyId ) : bool
{
// Super admin can access all companies
if ( $this -> hasRole ( 'super_admin' )) {
return true ;
}
// Check if it's the user's assigned company
return $this -> company_id === $companyId ;
}
Usage in Controllers:
public function index ( Request $request )
{
$user = $request -> user ();
// Super admin sees all invoices
if ( $user -> hasRole ( 'super_admin' )) {
$invoices = Invoice :: all ();
} else {
// Regular users only see their company's invoices
$invoices = Invoice :: where ( 'company_id' , $user -> company_id ) -> get ();
}
return response () -> json ( $invoices );
}
Creating Additional Users
Only super admins can create new users.
Endpoint: POST /api/auth/create-user
Request:
{
"name" : "Jane Smith" ,
"email" : "[email protected] " ,
"password" : "SecurePass123!" ,
"role_name" : "accountant" ,
"company_id" : 5 ,
"user_type" : "user"
}
Implementation: app/Http/Controllers/Api/AuthController.php:168
public function createUser ( Request $request )
{
if ( ! $request -> user () -> hasRole ( 'super_admin' )) {
return response () -> json ([
'message' => 'No tienes permisos para crear usuarios' ,
], 403 );
}
$request -> validate ([
'name' => 'required|string|max:255' ,
'email' => 'required|string|email|unique:users' ,
'password' => [ 'required' , Password :: min ( 8 )],
'role_name' => 'required|string|exists:roles,name' ,
'company_id' => 'nullable|integer|exists:companies,id' ,
'user_type' => 'required|in:system,user,api_client' ,
]);
$role = Role :: where ( 'name' , $request -> role_name ) -> first ();
$user = User :: create ([
'name' => $request -> name ,
'email' => $request -> email ,
'password' => Hash :: make ( $request -> password ),
'role_id' => $role -> id ,
'company_id' => $request -> company_id ,
'user_type' => $request -> user_type ,
'active' => true ,
]);
return response () -> json ([
'message' => 'Usuario creado exitosamente' ,
'user' => $user
]);
}
Token Abilities
Sanctum supports token-specific abilities (permissions).
// Create token with specific abilities
$token = $user -> createToken ( 'invoice-api' , [
'invoices.create' ,
'invoices.view'
]) -> plainTextToken ;
// Check token abilities in middleware
Route :: middleware ([ 'auth:sanctum' , 'ability:invoices.create' ])
-> post ( '/api/invoices' , [ InvoiceController :: class , 'store' ]);
Middleware
Authentication Middleware
// Require authentication
Route :: middleware ( 'auth:sanctum' ) -> group ( function () {
Route :: get ( '/api/invoices' , [ InvoiceController :: class , 'index' ]);
});
Ability Middleware
// Require specific ability
Route :: middleware ([ 'auth:sanctum' , 'ability:invoices.create' ])
-> post ( '/api/invoices' , [ InvoiceController :: class , 'store' ]);
// Require any of multiple abilities
Route :: middleware ([ 'auth:sanctum' , 'abilities:invoices.create,boletas.create' ])
-> post ( '/api/documents' , [ DocumentController :: class , 'store' ]);
Best Practices
Always use HTTPS to protect tokens in transit. SANCTUM_STATEFUL_DOMAINS=yourdomain.com
SESSION_SECURE_COOKIE=true
Implement token rotation for long-lived tokens. // Delete old token
$user -> tokens () -> delete ();
// Create new token
$newToken = $user -> createToken ( 'rotated-token' ) -> plainTextToken ;
Use Appropriate Expiration Times
Set shorter expiration for web tokens, longer for integration tokens. // Short-lived for web apps
$webToken = $user -> createToken ( 'web' , [ '*' ], now () -> addHours ( 8 ));
// Long-lived for integrations
$apiToken = $user -> createToken ( 'integration' , [ 'invoices.*' ], now () -> addDays ( 30 ));
Implement IP Whitelisting for API Clients
For programmatic access, restrict to known IP addresses. $user -> update ([
'allowed_ips' => [ '192.168.1.100' , '10.0.0.0/24' ]
]);
Next Steps
Certificate Setup Configure SUNAT digital certificates
Environments Understand Beta vs Production environments