[UPDATED] Building RESTful APIs with Lumen and OAuth2
If you are thinking about building fast RESTful APIs in a quick and simple way, then, Lumen is here!.
Lumen eliminates all the unnecessary and heavy features in Laravel and load only components like Eloquent, middleware, authentication & authorization, …etc which keeps it light and fast. Lumen focuses on building Stateless APIs. Therefore, sessions and views are no longer included in the latest version of Lumen.
Index
- What Do We Mean by RESTful APIs?
- Why Using Lumen in Creating RESTful APIs?
- What We Are Building?
- Setting the Development Environment
- Creating the Models and Their Relationships
- Creating Migrations
- Inserting Fake Data
- Creating the Routes & Controllers
- Implementing the Controllers
- Using OAuth2 Server
- Conclusion
What Do We Mean by RESTful APIs?
First and foremost, What do we mean by building RESTful APIs?. You may have heard the term RESTful, but, don’t know what does it actually mean. So, it worth to know now before moving further.
REST is an architectural style for building APIs. It stands for “Representational State Transfer”. It means when we build an API, we build it in a way that HTTP methods and URIs mean something, and the API has to respond in a way that’s expected.
Why Using Lumen in Creating Restful APIs?
In short, It’s fast, light, and easy!
One Of the Fastest Micro-frameworks
Although all the candidates; Laravel, Slim, & Lumen are faster than you will ever need. With Lumen, your API will support from ~1200–1700 requests(if not more depending on your machine) in a range of 10 seconds with 10 simultaneous requests. It’s great speed makes it a perfect candidate for building a RESTful API.
The following command will instruct Apache Benchmark to run for 10 seconds with 10 concurrent requests.
ab -t 10 -c 10 http://lumen.app/
Use Laravel Features, While Remaining Light
When working on RESTful APIs, you won’t need all the features in a full stack framework. Lumen has the best features of Laravel in a light version, while remaining expressive, intuitive, and simple. Lumen removes all the parts that you probably won’t need.
Easier Than You Might Think
It’s easy to configure, understand, and upgrade. Since Lumen is powered by Laravel’s components, you can easily upgrade your Lumen application to the full Laravel framework.
What We Are Building?
We are going to build a simple application, indented for small projects, helps to understand creating RESTful APIs with Lumen and OAuth2, know how to authenticate and authorize, and more.
The RESTful API is for Posts and Comments, where Users can view, create, update, and delete. It provides authorization mechanism to authorize against access tokens using OAuth2.
You can find the final RESTful API on GitHub. You will need to check the GitHub repository as you are following along throughout this tutorial.
Although this tutorial focuses on building an API for Posts and Comments(just for demonstration purposes), however, you can follow the same steps to build an API for whatever you want.
Setting the Development Environment
Installing Lumen via Composer Create-Project
Lumen utilizes Composer to manage its dependencies.
composer create-project laravel/lumen lumen-api-oauth
Laravel Homestead
Laravel Homestead is an official, pre-packaged Vagrant box that provides you a wonderful development environment without requiring you to install PHP, HHVM, a web server, and any other server software on your local machine. Source
If you want to take the advantage of using Laravel Homestead, then follow the Installation Guide.
WAMP, LAMP, MAMP, XAMP Server
If you are using any of WAMP, LAMP, MAMP, XAMP Servers, then then don’t forget to create a database, probably a MySQL database.
Configure the .env File
Make a copy of the .env.example file, and rename it to .env. Then, set your application key to a random string with 32 characters long, edit database name, database username, and database password if needed.
If you are receiving Class ‘Memcached’ not found error, edit the .env file and change the following:
CACHE_DRIVER=array
QUEUE_DRIVER=arrayThanks to Ozal MEHMETi.
Configure the Bootstrap File
Go to bootstrap/app.php file, and uncomment $app->withEloquent(); and $app->withFacades();
Creating the Models and Their Relationships
Eloquent is the ORM of Lumen, which allows you to interact easily with the database. Eloquent Models allow you to query & insert data in your tables. Eloquent models extend Illuminate\Database\Eloquent\Model class.
We have three Models; User, Post, & Comment. So, let’s create them.
Defining Models
User
By default, the User Model comes with Lumen when installed Via Composer Create-Project, you just need to add Hash facade and edit the fillable & hidden attributes
// app/User.phpuse Illuminate\Auth\Authenticatable;
use Laravel\Lumen\Auth\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;use Illuminate\Support\Facades\Hash;class User extends Model implements AuthenticatableContract, AuthorizableContract { use Authenticatable, Authorizable; protected $fillable = ['id', 'name', 'email'];
protected $hidden = ['created_at', 'updated_at', 'password'];}
Post
// app/Post.phpuse Illuminate\Database\Eloquent\Model;class Post extends Model{ protected $fillable = ['id', 'user_id', 'title', 'content'];
protected $hidden = ['created_at', 'updated_at'];
}
Comment
// app/Comment.phpuse Illuminate\Database\Eloquent\Model;class Comment extends Model{ protected $fillable = ['id', 'post_id', 'user_id', 'content'];
protected $hidden = ['created_at', 'updated_at'];
}
Defining Relationships
Since Database tables are often related to each another, Eloquent makes it easy to work with these database tables relationships by defining relationships between model classes. For example, a post may have one or more comments, and every comment is related to a single post(One To Many).
Post
// app/Post.phpclass Post extends Model{ /**
* Define a one-to-many relationship with App\Comment
*/
public function comments(){
return $this->hasMany('App\Comment');
}
}
Comment
// app/Comment.phpclass Comment extends Model{ /**
* Define an inverse one-to-many relationship with App\Post.
*/
public function post(){
return $this->belongsTo('App\Post');
}
}
Creating Migrations
Migrations allow you to easily build, modify and share the application’s database schema. We will create three tables; users, posts, & comments.
Creating Migration Files
To create a migration file, use the make:migration Artisan command.
php artisan make:migration create_users_table --create=users
php artisan make:migration create_posts_table --create=posts
php artisan make:migration create_comments_table --create=comments
These commands will create migration files in your database/migrations folder.
Migration Structure
In each migration file, specifically, inside “up()” method, we can create a new table and define it’s columns.
Users
Every user has a unique, auto-incremented id, a name, an email, and a password.
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->nullableTimestamps();
});
}
Posts
A post has a an id, title, content, and a foreign key points to the user who created that post.
Whenever we delete or update a user, the database will cascade down affecting the referencing rows. This is used to force referential integrity at the database level.
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->string('content');
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
$table->nullableTimestamps();
});
}
Comments
It’s same idea as in posts table, where every comment has a content, and a foreign key points to the user who created that comment, and another foreign key points to the post related to that comment.
It doesn’t go without saying, If you deleted a user or a post, the database would cascade down deleting all the referencing comments.
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->increments('id');
$table->string('content'); $table->nullableTimestamps();
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); $table->integer('post_id')->unsigned();
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade')->onUpdate('cascade');
});
}
Timestamps
By default, $timestamps property of the Eloquent model is set to true. So, Eloquent expects created_at and updated_atcolumns to exist on our tables. “$table->nullableTimestamps()” will create created_at and updated_at columns automatically.
Running Migrations
Finally, It’s time to apply what we’ve defined so far. To run all migrations, use the migrate Artisan command.
php artisan migrate
After running this command, your database should have three tables, with their columns defined in the migration files.
Inserting Fake Data
Factories are useful to create and insert fake data for our Eloquent models classes, while the Seeder execute these factories. In fact both of them can insert data into the database.
We will use Faker library to generate fake data. It’s already installed with Lumen.
Model Factories
// database/factories/ModelFactory.php$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'title' => $faker->sentence(4),
'content' => $faker->paragraph(4),
'user_id' => mt_rand(1, 10)
];
});$factory->define(App\Comment::class, function (Faker\Generator $faker) {
return [
'content' => $faker->paragraph(1),
'post_id' => mt_rand(1, 50),
'user_id' => mt_rand(1, 10)
];
});$factory->define(App\User::class, function (Faker\Generator $faker) { $hasher = app()->make('hash'); return [
'name' => $faker->name,
'email' => $faker->email,
'password' => $hasher->make("secret")
];
});
To keep the database in a consistent state. A foreign key must have values that exist in the primary key it’s referring to. For example user_id column in posts table must have values from 1 to 10(assuming we inserted 10 or more Users).
Seeding the Database
A seeder class contains only one method by default; “run()”. This method is called when we run seeders. Seeders use model factories to generate & insert database records.
// database/seeds/DatabaseSeeder.php public function run(){ // Disable foreign key checking because truncate() will fail
DB::statement('SET FOREIGN_KEY_CHECKS = 0'); User::truncate();
Post::truncate();
Comment::truncate(); factory(User::class, 10)->create();
factory(Post::class, 50)->create();
factory(Comment::class, 100)->create(); // Enable it back
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
}
Don’t forget to include User, Post, and Comment classes so you can use them.
Running Seeders
Now, we are going to seed the database with fake data.
php artisan db:seed
Creating the Routes & Controllers
Routes link URIs to controller’s action method, while Controllers handle the request and make a response.
Routes
All the routes are defined in the routes/web.php file.
// Users
$app->get('/users/', 'UserController@index');
$app->post('/users/', 'UserController@store');
$app->get('/users/{user_id}', 'UserController@show');
$app->put('/users/{user_id}', 'UserController@update');
$app->delete('/users/{user_id}', 'UserController@destroy');// Posts
$app->get('/posts','PostController@index');
$app->post('/posts','PostController@store');
$app->get('/posts/{post_id}','PostController@show');
$app->put('/posts/{post_id}', 'PostController@update');
$app->delete('/posts/{post_id}', 'PostController@destroy');// Comments
$app->get('/comments', 'CommentController@index');
$app->get('/comments/{comment_id}', 'CommentController@show');// Comments of a Post
$app->get('/posts/{post_id}/comments', 'PostCommentController@index');
$app->post('/posts/{post_id}/comments', 'PostCommentController@store');
$app->put('/posts/{post_id}/comments/{comment_id}', 'PostCommentController@update');
$app->delete('/posts/{post_id}/comments/{comment_id}', 'PostCommentController@destroy');
Controllers
In this tutorial, we will create 4 controllers; UserController, PostController, CommentController, & PostCommentController. All controllers must extend the base controller class.
The final code for controllers won’t be included here as they are already available in the GitHub repository.
// app/Http/Controllers/UserController.php<?php namespace App\Http\Controllers;use App\User;use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;class UserController extends Controller{ public function index(){
} // ...
}
Implementing the Controllers
The API will be used to create, read, update, or delete data, right? So, we need to write the code that will take care of these operations.
Inside each controller, we will define action methods to create, read, update, and delete data. Here is an example of the UserController class.
// app/Http/Controllers/UserController.php public function index(){ $users = User::all();
return response()->json(['data' => $users], 200);
} public function store(Request $request){ $this->validateRequest($request); $user = User::create([
'email' => $request->get('email'),
'password'=> Hash::make($request->get('password'))
]); return response()->json(['data' => "The user with with id {$user->id} has been created"], 201);
} public function show($id){ $user = User::find($id); if(!$user){
return response()->json(['message' => "The user with {$id} doesn't exist"], 404);
} return response()->json(['data' => $user], 200);
} public function update(Request $request, $id){ $user = User::find($id); if(!$user){
return response()->json(['message' => "The user with {$id} doesn't exist"], 404);
} $this->validateRequest($request); $user->email = $request->get('email');
$user->password = Hash::make($request->get('password')); $user->save();
return response()->json(['data' => "The user with with id {$user->id} has been updated"], 200);
} public function destroy($id){ $user = User::find($id); if(!$user){
return response()->json(['message' => "The user with {$id} doesn't exist"], 404);
} $user->delete(); return response()->json(['data' => "The user with with id {$id} has been deleted"], 200);
}
public function validateRequest(Request $request){ $rules = [
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
]; $this->validate($request, $rules);
}
Again, the final code for controllers won’t be included here as they are already available in the GitHub repository.
Validating User Inputs
Whenever the client wants to store or update an existing User, the client needs to submit an email and a password. Of course, there are some validations we need to run against user inputs.
For example, we need to make sure the email field exists in the first place, and it’s a valid email, and also it has to be unique. All of these validations can be easily done with Lumen using “$this->validate()” method. It returns a JSON response with the relevant error messages if validation fails.
Don’t Repeat Yourself (DRY)
If you notice, there are some code snippets we have been using over and over again. For example, sending a JSON response with success and error message. So, it’s better the keep these snippets in the base controller class.
// app/Http/Controllers/Controller.php public function success($data, $code){
return response()->json(['data' => $data], $code);
} public function error($message, $code){
return response()->json(['message' => $message], $code);
}
Using OAuth2 Server
The most important part of this tutorial is how to install and use OAuth2 Server. This package adds authorization layer to your application by using access tokens.
About OAuth2 and How It Works
The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service.Source
Before diving deeper into this package, you need to have a good knowledge of the principles behind the OAuth2 specification.
If you need a refresher, or you want to learn about OAuth2, DigitalOcean has a good Introduction to OAuth 2.
Installing the OAuth2 Server
- Add “lucadegasperi/oauth2-server-laravel”: “^5.2” to ‘require’ in composer.json file.
- Run composer update to update and install(if not already installed) all the dependencies.
- Copy configuration folder vendor\lucadegasperi\oauth2-server-laravel\config into lumen-api-oauth folder.
- Copy all migration files in vendor\lucadegasperi\oauth2-server-laravel\database into database\migrations.
- Register service providers and middlewares
// bootstrap/app.php// Register Middleware$app->middleware([
\LucaDegasperi\OAuth2Server\Middleware\OAuthExceptionHandlerMiddleware::class
]);$app->routeMiddleware([
'oauth' =>
\LucaDegasperi\OAuth2Server\Middleware\OAuthMiddleware::class,
]);// bootstrap/app.php// Register Service Providers$app->register(\LucaDegasperi\OAuth2Server\Storage\FluentStorageServiceProvider::class);
$app->register(\LucaDegasperi\OAuth2Server\OAuth2ServerServiceProvider::class);
Configuring the OAuth2 Server
- Create a seeder class for OAuth called OAuthClientSeeder for example. This class will generate and insert data tooauth_clients table.
// database/seeds/OAuthClientSeeder.php
use Illuminate\Database\Seeder;
class OAuthClientSeeder extends Seeder {
public function run(){
DB::table('oauth_clients')->truncate();
for ($i=0; $i < 10; $i++){
DB::table('oauth_clients')->insert(
[ 'id' => "id$i",
'secret' => "secret$i",
'name' => "Test Client $i"
]
);
}
}
}
2. Make a call to OAuthClientSeeder in the DatabaseSeeder class.
// database/seeds/DatabaseSeeder.php
public function run(){
// ...
$this->call('OAuthClientSeeder');
// ...
}
3. Run composer dumpautoload to update the autoloader because of the new classes we’ve added recently.
4. Run Migrations and Seeders
php artisan migrate — seed
5. Define a route to respond to the incoming access token requests.
// app/Http/routes.php // Request Access Tokens
$app->post('/oauth/access_token', function() use ($app){
return response()->json($app->make('oauth2-server.authorizer')->issueAccessToken());
});
6. Choose and Enable the grant type suitable for us. In this tutorial, we will use Password Grant.
// config/oauth2.php 'grant_types' => [
'password' => [
'class' => '\League\OAuth2\Server\Grant\PasswordGrant',
'callback' => '\App\User@verify',
'access_token_ttl' => 3600
]
],
This grant needs the client to send the resource owner(user) email and password along with client id and client secret. So, we need to define a method where you check if the provided user is a valid one.
So, we will create a verify($email, $password) method in the User class.
// app/User.php public function verify($email, $password){ $user = User::where('email', $email)->first(); if($user && Hash::check($password, $user->password)){
return $user->id;
} return false;
}
Testing the OAuth2 Server
You can test the API using Postman, and make a POST request to: http://lumenapi.com/oauth/access_token.
In case of using any of WAMP, LAMP, MAMP, XAMP servers, make a POST request to http://localhost/lumen-api-oauth/public/oauth/access_token.
The client needs to submit 5 fields: username, password, client_id, client_secret, and grant_type.
The username field is the email in Users table.
The password field is secret.
The client_id field is the id in oauth_clients table.
The client_secret field is the secret in oauth_clients table.
The grant_type field is password.
The authorization server will then issue access tokens to the client after successfully authenticating the client credentials and presenting authorization grant(user credentials).
Protecting the Resources Using Access Tokens
After obtaining an access token, we need to restrict the access to controller action methods. Therefore, clients need to present the access token to get access to the resources.
For example, In the PostController class, we can assign oauth middleware to all methods which need authorization via access token except index & show methods. So, if the client needs to remove a post, the client must send a valid access token.
// app/Http/Controllers/PostController.php public function __construct(){
$this->middleware('oauth', ['except' => ['index', 'show']]);
} public function destroy($id){ $post = Post::find($id); if(!$post){
return $this->error("The post with {$id} doesn't exist", 404);
} // no need to delete the comments for the current post,
// since we used on delete cascase on update cascase.
// $post->comments()->delete();
$post->delete(); return $this->success("The post with with id {$id} has been deleted along with it's comments", 200);
}
Now, you can assign oauth middleware to action methods in UserController & PostCommentController classes.
// app/Http/Controllers/UserController.php
// app/Http/Controllers/PostCommentController.php public function __construct(){ $this->middleware('oauth', ['except' => ['index', 'show']]);]);
}
If you still receive invalid request error even if access token is sent, then, key and value pairs should be sent using www-url-encode instead of form-params.
The Current Authorized User Id
You might be asking How to get the id of the authorized user?. Every access token is related to a user, if you remember, we sent the user email and password to get an access token.
So, after the oauth middleware validates the access token sent by the client, you can access the authorized user id by calling:
\LucaDegasperi\OAuth2Server\Facades\Authorizer::getResourceOwnerId();
And because we are going to get the authorized user id multiple times, it’s better to keep this line of code in the base controller class.
// app/Http/Controllers/Controller.php
protected function getUserId(){
return \LucaDegasperi\OAuth2Server\Facades\Authorizer::getResourceOwnerId();
}
I See authorize Middleware, What Is This?!
If you have seen the following line of code in any Controller constructor in the GitHub repository, just uncomment it, Why? Because In this tutorial, we didn’t cover how to authorize against ownership, and non-admin Vs admin users.
// app/Http/Controllers/UserController.php
// app/Http/Controllers/PostCommentController.php
// app/Http/Controllers/PostController.php public function __construct(){ $this->middleware('oauth', ['except' => ['index', 'show']]);
// $this->middleware('authorize:' . __CLASS__, ['except' => [....]]);
}
Conclusion
Building a RESTful API couldn’t be easier with Lumen, even if you aren’t familiar with Laravel. Now, you should be able to install, configure, and build your own RESTful API with OAuth2. You can take what we have done further and tailor it according to your needs.