https://odan.github.io/2019/12/02/slim4-oauth2-jwt.html
If you like the article, please click on the ⭐ button.
https://odan.github.io/2019/12/02/slim4-oauth2-jwt.html
If you like the article, please click on the ⭐ button.
For my scenario I wanted to verify Firebase Auth users were valid requests to my api coming from an Angular dashboard.
I used this tutorial as a base, and did the following:
Used this library: https://github.com/kreait/firebase-tokens-php
composer require kreait/firebase-tokens
// Firebase Settings
$settings['firebase'] = [
// The project ID
'project_id' => 'some-id-here',
];
<?php
namespace App\Auth;
use Kreait\Firebase\JWT\Error\IdTokenVerificationFailed;
use Kreait\Firebase\JWT\IdTokenVerifier;
use InvalidArgumentException;
use Kreait\Firebase\JWT\Token;
final class FirebaseAuth
{
/**
* @var string The firebase project id
*/
private $projectId;
/**
* The constructor.
*
* @param string $projectId The firebase project id
*/
public function __construct(
string $projectId
) {
$this->projectId = $projectId;
}
/**
* Parse token.
*
* @param string $token The JWT
*
* @throws InvalidArgumentException
*
* @return Token The parsed token
*/
public function createParsedToken(string $token): Token
{
$verifier = IdTokenVerifier::createWithProjectId($this->projectId);
return $verifier->verifyIdToken($token);
}
/**
* Validate the access token.
*
* @param string $accessToken The JWT
*
* @return bool The status
*/
public function validateToken(string $accessToken): bool
{
$token = null;
// create + validate
try {
$token = $this->createParsedToken($accessToken);
} catch (IdTokenVerificationFailed $e) {
// signature is not valid
return false;
}
// user must have verified email to proceed
if($token->payload()["email_verified"] === false) {
return false;
}
return true;
}
}
Add this in like the tutorial does:
ResponseFactoryInterface::class => function (ContainerInterface $container) {
return $container->get(App::class)->getResponseFactory();
},
FirebaseAuth::class => function (ContainerInterface $container) {
$config = $container->get(Configuration::class);
$projectId = $config->getString('firebase.project_id');
return new FirebaseAuth($projectId);
},
skipped this part as I only wanted to verify existing user tokens passed in from my dashboard
<?php
namespace App\Middleware;
use App\Auth\FirebaseAuth;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Firebase middleware.
*/
final class FirebaseMiddleware implements MiddlewareInterface
{
/**
* @var FirebaseAuth
*/
private $firebaseAuth;
/**
* @var ResponseFactoryInterface
*/
private $responseFactory;
public function __construct(FirebaseAuth $firebaseAuth, ResponseFactoryInterface $responseFactory)
{
$this->firebaseAuth = $firebaseAuth;
$this->responseFactory = $responseFactory;
}
/**
* Invoke middleware.
*
* @param ServerRequestInterface $request The request
* @param RequestHandlerInterface $handler The handler
*
* @return ResponseInterface The response
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$authorization = explode(' ', (string)$request->getHeaderLine('Authorization'));
$token = $authorization[1] ?? '';
if (!$token || !$this->firebaseAuth->validateToken($token)) {
return $this->responseFactory->createResponse()
->withHeader('Content-Type', 'application/json')
->withStatus(401, 'Unauthorized');
}
// Append valid token
$parsedToken = $this->firebaseAuth->createParsedToken($token);
$request = $request->withAttribute('token', $parsedToken);
// Sample response for $parsedToken->payload();
// {
// "iss": "https://securetoken.google.com/{{PROJECTID}}",
// "aud": "{{PROJECTID}}",
// "auth_time": 1578755578,
// "user_id": "{{STRING OF LETTERS AND NUMBERS}}",
// "sub": "{{WAS SAME AS USER_ID}}",
// "iat": 1580038499,
// "exp": 1580042099,
// "email": "matthew@example.co.uk",
// "email_verified": true,
// "firebase": {
// "identities": {
// "email": [
// "matthew@example.co.uk"
// ]
// },
// "sign_in_provider": "password"
// }
// }
// Append the user id as request attribute
$request = $request->withAttribute('email', $parsedToken->payload()["email"]);
return $handler->handle($request);
}
}
Protect them just like the examples in the tutorial but replace it with the middleware class name:
$app->post('/users', \App\Action\UserCreateAction::class)
->add(\App\Middleware\FirebaseMiddleware::class);
One important note that is missed in the tutorial is that the OPTIONS preflight check for CORS support should not be authorised.
Inside an action you can then use an invoke method something like this:
public function __invoke(ServerRequest $request, Response $response): Response
{
// Collect input from the HTTP request
$data = (array) $request->getParsedBody();
// Get user email
$userEmail = $request->getAttribute("email");
// Invoke the Domain with inputs and retain the result
$devices = $this->deviceListReader->getDeviceList($userEmail);
// Transform the result into the JSON representation
foreach($devices as $device) {
$result[] = [
'id' => $device->id,
'client' => $device->client,
// etc
];
}
// Build the HTTP response
return $response->withJson($result)->withStatus(200);
}
Hopefully this will save a few hours for anyone wanting to implement it themselves.
@odan if you want to use any parts of this for a blog post please go ahead, thank you again for everything you have posted on here about Slim its been invaluable with my project.
Hi @rtpHarry Thanks for your feedback and your example about Firebase.
One important note that is missed in the tutorial is that the OPTIONS preflight check for CORS support should not be authorised.
You find more information about CORS in the Slim 4 documentation and here: https://odan.github.io/2019/11/24/slim4-cors.html
@odan, thank you, that is the tutorial I used to get CORS up and running for my system, it was great! My comment was something that came out of starting to merge your tutorials together. It doesn't mention, I don't think, that if the route is protected you shouldn't also protect the OPTIONS preflight check. It's basically the only piece of information I've had to research for myself with this project :)
@odan Do you have idea to create new tutorial with tuupola/slim-basic-auth and tuupola/slim-jwt-auth ?
@mkraha I think the documentation of tuupola/slim-basic-auth and tuupola/slim-jwt-auth is good enough.
Hi @odan
I followed your tutorial and I can generate tokens but I get the following error when trying to validate tokens on routes:
Message: Entry "App\Middleware\JwtMiddleware" cannot be resolved: Entry "Psr\Http\Message\ResponseFactoryInterface" cannot be resolved: the class is not instantiable Full definition: Object ( class = #NOT INSTANTIABLE# Psr\Http\Message\ResponseFactoryInterface lazy = false ) Full definition: Object ( class = App\Middleware\JwtMiddleware lazy = false __construct( $jwtAuth = get(App\Auth\JwtAuth) $responseFactory = get(Psr\Http\Message\ResponseFactoryInterface) ) )
My route looks like this:
$commentRoute->post('', \App\Action\BakeOnline\CommentCreateAction::class)->add(\App\Middleware\JwtMiddleware::class);
Do you have any suggestions on how to solve this problem?
Thank you in advance.
It looks like the container definition for ResponseFactoryInterface::class
is missing. You can find all the details in the article.
use Psr\Http\Message\ResponseFactoryInterface;
// ...
ResponseFactoryInterface::class => function (ContainerInterface $container) {
return $container->get(App::class)->getResponseFactory();
},
@odan, thank you so much. It worked for me. And thank you for the article! It has been a really great help!
Hello @odan
Thank you so much for this article. I have 2 questions the first isn't so important. I already decided that I will implement the way it's in your doc.
What are the pros / cons of using a library like tuupola/slim-jwt-auth versus an approach like in this article and what do you recommend in the end?
Do you know any projects where these JWT-Functions are being tested? Or do you have examples?
Hi @samuelgfeller My approach is more Middleware and Routing based while the tuupola/slim-jwt-auth
approach uses an array to configure the different routes. For me the array based protection is not so good to maintain in the long run, for example when you add or change route paths you may miss some routes and suddenly it's unprotected. I prefer to explicitly add the JwtAuthMiddleware
to specific routes or route groups in routes.php
. You can open the routes.php
file see what is protected. My approach also makes it easier to fetch users from the database (see TokenCreateAction) instead of loading it from a fixed array. I think you have to decide what's better for your specific use case.
@odan that's very pertinent! I think easily worth mentioning in the article. Below where you link to tuupola/slim-jwt-auth
or somewhere near.
@samuelgfeller Yes, thanks. I will add it to the article.
Great tutorials for Slim 4. So far I haven't even looked at the official docs, this blog has guided me through everything I've needed!
Small thing that I noticed, in the
container.php
snippet the namespaceuse
at the top is missinguse Slim\Factory\AppFactory;
.