Skip to main content

Implementing authorization in Laravel with Cryptr

Cryptr - Laravel illustration

Laravel Authorization Getting Started: Secure an API

Laravel >= 8.06min

Learn how to use Cryptr to implement authorization in Laravel

In this tutorial, we are going to create an API that will allow you to read the data, i.e. see the list of courses in our example. Security will be enhanced so that only Cryptr users can access the list of courses by checking if the token is valid by following the Cryptr security procedure.

Let's get started! πŸ˜‰

1. Configuration​

Create a Laravel application​

πŸ› οΈοΈ To create your laravel application, type this command in your terminal:

composer create-project laravel/laravel cryptr-laravel-api-sample

This creates a new folder called cryptr-laravel-api-sample and configures your application in that folder

Note

Composer is a dependency manager in PHP. With it, you can declare the dependent libraries your project needs and they will be installed in your project by this tool. See Composer's getting started doc for more information.

πŸ› οΈοΈ Once it’s finished, go to your project with command cd cryptr-laravel-api-sample.

You can start the application by running php artisan serve

2. Application keys​

Create an application with Cryptr​

πŸ› οΈοΈ In order to start, you need to create a free Cryptr account if you don't have one yet.

Rendering Rendering

πŸ› οΈοΈ You can then create your application by following the onboarding steps.

Rendering

Once you've completed all the funnel, after the processing steps, you arrive on the homepage of the back-office where you have access to various guides that will help you. They will depend on the choices of technology you have made in the funnel.

Rendering

πŸ› οΈοΈ You can get your environment configuration by first clicking on Β« applications Β» in the left menu, then by clicking on the application you will have created. A modal will appear and this is where you may copy/paste your environment variables.

Rendering Rendering

Add your Cryptr credentials​

πŸ›  Complete the environment file with the variables that you get in the Cryptr back-office, in the modal that is displayed when you select your application. Don't forget to replace YOUR_CLIENT_ID & YOUR_DOMAIN

.env
CRYPTR_BASE_URL=https://auth.cryptr.eu
TENANT_DOMAIN=YOUR_DOMAIN
DEFAULT_REDIRECT_URI=http://localhost:8080
CLIENT_ID=YOUR_CLIENT_ID
NOTE

If you are from the EU, you must add https://auth.cryptr.eu/ in the CRYPTR_BASE_URL variable, and if you are from the US, you must add https://auth.cryptr.us/ in the same variable.

Restart the terminal

Make sure to restart your shell/terminal when you modify the environment file

3. Valid access tokens​

Install dependencies​

πŸ›  Update composer setup by adding PHP-JWT to encode and decode JSON Web Tokens (JWT) in PHP:

composer require firebase/php-jwt

πŸ›  Open up composer.json and add Cryptr\\": "cryptr/ in autoloader psr-4:

composer.json
"autoload": {
"psr-4": {
"App\\": "app/",
// Add here
"Cryptr\\": "cryptr/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},

With the autoloader, we can define a namespace prefix and the directory mapped to that prefix. Everything in the cryptr folder is a namespace. Anything the composer asks for that has a namespace starting with "Cryptr" can be found in the "cryptr" directory.

πŸ›  Don’t forget to run composer dump-autoload

Create a sample resource model​

πŸ›  First, create a basic model in order to have a data structure:

php artisan make:model Course

πŸ›  Next, add protected $fillable = ['title', 'date', 'desc', 'img']; before use HasFactory

app/Models/Course.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Course extends Model
{
// Add protected $fillable here
protected $fillable = ['title', 'date', 'desc', 'img'];

use HasFactory;
}

Create a sample resource controller​

πŸ›  First, create an API resource controller which does not include the edit creation methods by adding --api and add --model to use a model instance:

php artisan make:controller CourseController --api --model=Course
Note

The purpose of the controller is to receive a request (which has already been selected by a route) and to define the appropriate response.

πŸ›  Next, create an Api folder and move generated file in it with this command:

mkdir app/Http/Controllers/Api && mv app/Http/Controllers/CourseController.php "$_"

πŸ› οΈοΈ Open up app/Http/Controllers/Api/CourseController.php and replace the contents with this:

app/Http/Controllers/Api/CourseController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Course;
use Illuminate\Http\Request;

class CourseController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return Course::all()->toJson();
}

/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$course = Course::create($request->all());

if ($course) {
return response() -> json([
'data' => $course
], 200);
} else {
return response() -> json([
'error' => 'unprocessable course'
], 422);
}
}

/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
if (Course::where('id', $id)->exists()) {

return Course::find($id)->toJson();
} else {
return response()->json([
"error" => "course not found"
], 404);
}
}

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
if (Course::where('id', $id)->exists()) {
$course = Course::find($id);

$course->date = is_null($request->date) ? $course->date : $request->date;
$course->desc = is_null($request->desc) ? $course->desc : $request->desc;
$course->img = is_null($request->img) ? $course->img : $request->img;
$course->title = is_null($request->title) ? $course->title : $request->title;
$course->save();

return response()->json([
'data' => $course
], 200);
} else {
return response()->json([
"error" => "course not found"
], 404);
}
}

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
if(Course::where('id', $id)->exists()) {
$course = Course::find($id);
$course->delete();

return response()->json([
"message" => "records deleted"
], 202);
} else {
return response()->json([
"error" => "course not found"
], 404);
}
}
}

πŸ› οΈοΈ Open up routes/api.php and add use app\Http\Controllers\Api\CourseController;

routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// 1. Add this line:
use App\Http\Controllers\Api\CourseController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});

πŸ› οΈοΈ Next, remove the base route and add this code instead: Route::apiResource('courses', CourseController::class);

routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\CourseController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

// 2. Modify the route:
Route::apiResource('courses', CourseController::class);

πŸ› οΈοΈ Next, add /v1 to api in app/Providers/RouteServiceProvider.php

app/Providers/RouteServiceProvider.php
// ...
public function boot()
{
$this->configureRateLimiting();

$this->routes(function () {
// Add /v1 to api
Route::prefix('api/v1')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));

Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
// ...

πŸ› οΈοΈ Next, update course controller in app/Http/Controllers/Api/CourseController.php:

app/Http/Controllers/Api/CourseController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Course;
use Illuminate\Http\Request;

class CourseController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// Update here:
return response()->json(
[array(
"id" => 1,
"user_id" =>
"eba25511-afce-4c8e-8cab-f82822434648",
"title" => "learn git",
"tags" => ["colaborate", "git" ,"cli", "commit", "versionning"],
"img" => "https://carlchenet.com/wp-content/uploads/2019/04/git-logo.png",
"desc" => "Learn how to create, manage, fork, and collaborate on a project. Git stays a major part of all companies projects. Learning git is learning how to make your project better everyday",
"date" => '5 Nov',
"timestamp" => 1604577600000,
"teacher" => array(
"name" => "Max",
"picture" => "https://images.unsplash.com/photo-1558531304-a4773b7e3a9c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
)
)], 200);
}

πŸ› οΈοΈ Run the server with command php artisan serve and open insomnia or postman to make a GET request which should end with 200:

Rendering

JWT validation​

πŸ› οΈοΈ Create cryptr folder and go to the folder:

mkdir cryptr && cd cryptr

πŸ› οΈοΈ Create CryptrJwtVerifier.php with command touch CryptrJwtVerifier.php and copy paste this code:

cryptr/CryptrJwtVerifier.php
<?php
namespace Cryptr;

use \Firebase\JWT\JWK;
use \Firebase\JWT\JWT;
use \Illuminate\Support\Facades\Http;
use DateTime;
use Exception;
use Cryptr\JwtClaimsValidation;

class CryptrJwtVerifier
{
public function __construct($cryptrBaseUrl, $domain, $allowedOrigins, $allowedCLientIds)

{
$this->wellknownEndpoint = "$cryptrBaseUrl/t/$domain/.well-known";

# JWT
$this->jwks = NULL;

# Issuer
$this->tenantDomain = $domain;
$this->issuer = "$cryptrBaseUrl/t/$domain";

# Audiences
$this->allowedOrigins = $allowedOrigins;
$this->allowedClientIds = $allowedCLientIds;
}

public function getJwks()
{
try {
$keys = Http::get($this->wellknownEndpoint)['keys'];
$jwks = ['keys' => $keys];
$this->jwks = $jwks;

return $jwks;
} catch (Exception $e) {
echo 'Can not fetch JWKS : ', $e->getMessage(), "\n";
return [];
}
}

public function decode($jwt, $jwks)
{
$publicKeys = JWK::parseKeySet($jwks);
return JWT::decode($jwt, $publicKeys, array('RS256'));
}

public function validate($jwt, $jwks = NULL)
{
// 0. Prepare public keys
$jwks = $jwks ? $jwks : $this->jwks;
$publicKeys = JWK::parseKeySet($jwks);
// 1. Decode token
$decodedJwt = $this->decode($jwt, $jwks);

// 2. Assert claims
$jwtClaims = new JwtClaimsValidation($this->tenantDomain, $this->issuer, $this->allowedOrigins, $this->allowedClientIds);
$jwtClaims->isValid($decodedJwt);

return $decodedJwt;
}
}

πŸ› οΈοΈ Create JwtClaimsValidation.php with command touch JwtClaimsValidation.php and copy paste this code:

cryptr/JwtClaimsValidation.php
<?php
namespace Cryptr;

use DateTime;
use Exception;


class JwtClaimsValidation
{
public function __construct($tenantDomain, $issuer, $allowedOrigins, $allowedClientIds)
{
# Issuer
$this->tenantDomain = $tenantDomain;
$this->issuer = $issuer;

# Audiences
$this->allowedOrigins = $allowedOrigins;
$this->allowedClientIds = $allowedClientIds;
}

public function validateResourceOwner($decodedToken, $userId)
{
if ($decodedToken->sub != $userId) {
throw new Exception('The resource owner identifier (cryptr user id) of the JWT claim (sub) is not compliant');
}
return true;
}

public function validateScopes($decodedToken, $authorizedScopes)
{
if (array_intersect($decodedToken->scp, $authorizedScopes) != $decodedToken->scp){
throw new Exception('The scopes of the JWT claim (scp) resource are not compliants');
};
return true;
}

private function currentTime() {
return new DateTime();
}

public function validateExpiration($decodedToken) {
$expiration = DateTime::createFromFormat( 'U', $decodedToken->exp );

if ($expiration < $this->currentTime()){
throw new Exception('The expiration of the JWT claim (exp) should be greater than current time');
}

return true;
}

public function validateIssuedAt($decodedToken) {
$issuedAt = DateTime::createFromFormat( 'U', $decodedToken->iat );

if ($this->currentTime() < $issuedAt){
throw new Exception('The issuedAt of the JWT claim (iat) should be lower than current time');
};

return true;
}

public function validateNotBefore($decodedToken) {
$notBefore = DateTime::createFromFormat( 'U', $decodedToken->nbf );

if ($this->currentTime() < $notBefore){
throw new Exception('The notBefore of the JWT claim (iat) should be lower than current time');
};

return true;
}

public function validateIssuer($decodedToken) {
if ($decodedToken->iss != $this->issuer){
throw new Exception('The JWT (iss) claim issuer must conform to issuer from config');
};

return true;
}

public function validateAudience($decodedToken) {
if (!in_array($decodedToken->aud, $this->allowedOrigins)){
throw new Exception('The JWT (aud) claim audience must conform to audience from config');
};

return true;
}

public function isValid($decodedToken)
{
// exp (Expiration Time)
return $this->validateExpiration($decodedToken) &&
// iat (Issued At)
$this->validateIssuedAt($decodedToken) &&
// nbf (Not before)
// $this->validateNotBefore($decodedToken)&&
// iss (Issuer)
$this->validateIssuer($decodedToken) &&
// aud (Audience)
$this->validateAudience($decodedToken);
}
}

These are tools that will make it possible to retrieve the token (token of the user session) before retrieving the response to the request, and verify the token thanks to the validations claims.

4. Protect API Endpoints​

CryptrGuard middleware​

πŸ› οΈοΈ Return in root folder and create CryptrGuard middleware:

php artisan make:middleware CryptrGuard
Note

Middleware is responsible for filtering HTTP requests that arrive in the application, as well as those that leave it. In our case, it is verifying the authentication of a user so they can access certain resources.

πŸ› οΈοΈ Open up app/Http/Middleware/CryptrGuard.php and replace it with the following code:

app/Http/Middleware/CryptrGuard.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Cryptr\CryptrJwtVerifier;
use Illuminate\Support\Facades\Log;

class CryptrGuard
{
// To handle Public Keys in cache to avoid HTTP requests :
//
// const JWKS_KEY = "cryptr_jwks";
// const JWKS_TTL_SECONDS = 900;

public function handle(Request $request, Closure $next)
{
$requestOrigin = $request->headers->get('origin');
// 0. Config
$cryptrBaseUrl = env('CRYPTR_BASE_URL');
$domain = env('TENANT_DOMAIN');
$allowedOrigins = explode(',',env('DEFAULT_REDIRECT_URI'));
$allowedClientIds = explode(',',env('CLIENT_ID'));

// 1. Handle CORS
$allowOriginHeader = in_array($requestOrigin, $allowedOrigins) ? $requestOrigin : array_values($allowedOrigins)[0];

$headers = [
'Access-Control-Allow-Origin' => $allowOriginHeader,
'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Headers'=> 'Content-Type, Authorization, Origin'
];

if($request->getMethod() == "OPTIONS") {
// The client-side application can set only headers allowed in Access-Control-Allow-Headers
return Response::make('OK', 200, $headers);
}

// 2. JWT Validation
$cryptrJwtVerifier = new CryptrJwtVerifier($cryptrBaseUrl, $domain, $allowedOrigins, $allowedClientIds);

try {
// 2.1 Fetch public keys set to validate token
//
// To handle Public Keys in cache to avoid HTTP requests, uncomment the following lines :
//
// $jwks = Cache::get(JWKS_KEY);
//
// if (!$jwks) {
// $jwks = $cryptrJwtVerifier->getJwks();
// Cache::add(JWKS_KEY, $jwks, JWKS_TTL_SECONDS);
// }
// ...

$jwks = $cryptrJwtVerifier->getJwks();
// 2.2 Validate token with
$decoded = $cryptrJwtVerifier->validate($request->bearerToken(), $jwks);

// 1.bis Add headers to response
$response = $next($request);
foreach($headers as $key => $value){
$response->header($key, $value);
}
return $response;

// ...

} catch (\Exception $exception) {
$errMsg = $exception->getMessage();
$errLine = $exception->getLine();
error_log("Can not handle: $errMsg:$errLine");
return response('Unauthorized', 401)->header('Content-Type', 'text/plain');
}
}
}

In this middleware:

  • Check the header, if it does not contain http://localhost:4000 the request is blocked
  • Retrieve the client's key to validate the token of the request
  • Accept request and process it, otherwise return 401

Configure the middleware​

πŸ› οΈοΈ Open up app/Http/Kernel.php and register cryptr-guard middleware 'cryptr-guard' => [\App\Http\Middleware\CryptrGuard::class] to Kernel.php in $middlewareGroups

app/Http/Kernel.php
// ...

protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
// Add cryptr guard:
'cryptr-guard' => [\App\Http\Middleware\CryptrGuard::class]
];

// ...

πŸ› οΈοΈ Open up routes/api.php to protect individual API endpoints by applying the jwt middleware, wrap route with Route::prefix('/')->middleware('cryptr-guard')->group(function () {

routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\CourseController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

// Apply cryptr-guard middleware:
Route::prefix('/')->middleware('cryptr-guard')->group(function () {
Route::apiResource('courses', CourseController::class);
});

For all actions, the routes are secured by respecting the criteria of Cryptr

It is now time to try this on an application. For this purpose, we have an example app on Vue.

Test with a Cryptr Vue app​

πŸ›  Run your code with php artisan serve

πŸ› οΈοΈ Clone our cryptr-vue-sample:

git clone --branch 07-backend-courses-api https://github.com/cryptr-examples/cryptr-vue2-sample.git
cd cryptr-vue2-sample

πŸ›  Install the Vue project dependencies with yarn

πŸ› οΈοΈ Add .env.local file with your variables. Don't forget to replace YOUR_CLIENT_ID & YOUR_DOMAIN:

.env.local
VUE_APP_AUDIENCE=http://localhost:8080
VUE_APP_CLIENT_ID=YOUR_CLIENT_ID
VUE_APP_CRYPTR_BASE_URL=YOUR_BASE_URL
VUE_APP_DEFAULT_LOCALE=fr
VUE_APP_DEFAULT_REDIRECT_URI=http://localhost:8080
VUE_APP_TENANT_DOMAIN=YOUR_DOMAIN
VUE_APP_CRYPTR_TELEMETRY=FALSE

πŸ› οΈοΈ Run vue server with yarn serve and try to connect. Your Vue application redirects you to your sign form page, where you can sign in or sign up with an email.

SANDBOX EMAIL

You can log in with a sandbox email and we send you a magic link which should directly arrive in your personal inbox. Your sandbox email is based on your account's email. The email's structure is as follows: my-user-to-test@sandbox.my-admin-name. For example testemail@sandbox.lucas

Once you're connected, click on "Protected route". You can now view the list of the courses.

Congratulations if you made it to the end!

I hope this was helpful, and thanks for reading! πŸ™‚