Skip to content

Instantly share code, notes, and snippets.

@odan
Last active August 16, 2022 18:49
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save odan/2885ccd0d2f3a3df41bf5c3d6e9b4999 to your computer and use it in GitHub Desktop.
Save odan/2885ccd0d2f3a3df41bf5c3d6e9b4999 to your computer and use it in GitHub Desktop.
@odan
Copy link
Author

odan commented Dec 30, 2019

@outro99 Both tutorials are based on the slim/http package. If you use slim/psr7 then you can replace the method signature with the specific psr7 implementation (\Slim\Psr7\Request) or the more generic Psr7 interface.

@outro99
Copy link

outro99 commented Jan 6, 2020

@outro99 Both tutorials are based on the slim/http package. If you use slim/psr7 then you can replace the method signature with the specific psr7 implementation (\Slim\Psr7\Request) or the more generic Psr7 interface.

@odan thank you for everything! I am actively following your updates for skeleton app as well, loving it.

May I ask, is there a possibility that with the release of 1.0.0. skeleton app we might see also an opt-in pre-included docker installation sequence with nginx & mysql (and/or mariaDB?) and other resources that are needed for it.

Actually I would love also skeleton app to include much more detailed Authorization examples that better show how to initialize user and create different views (which I know they are in construction as stated) and would love if the skeleton would come already with slim4-oauth2 pre-included as a feature that can be enabled, with examples how to set cookies for it also inside your given application and correctly authorize and/or register user within db/manage roles without giving away stateless principle.

I know this is much. Just sharing my opinions for this skeleton to be the ultimate power. I will appreciate your answer as well!

Take care!

@rtpHarry
Copy link

rtpHarry commented Jan 26, 2020

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 namespace use at the top is missing use Slim\Factory\AppFactory;.

@rtpHarry
Copy link

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:

composer

Used this library: https://github.com/kreait/firebase-tokens-php

composer require kreait/firebase-tokens

settings.php

// Firebase Settings
$settings['firebase'] = [
    // The project ID
    'project_id' => 'some-id-here',
];

src/Auth/FirebaseAuth.php

<?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;
    }
}

config/Container.php

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);
    },

creating a token

skipped this part as I only wanted to verify existing user tokens passed in from my dashboard

src/Middleware/FirebaseMiddleware.php

<?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);
    }
}

config/routes.php

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.

filtering your api by the email address

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.

@odan
Copy link
Author

odan commented Jan 26, 2020

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

@rtpHarry
Copy link

@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 :)

@mkraha
Copy link

mkraha commented Apr 17, 2020

@odan Do you have idea to create new tutorial with tuupola/slim-basic-auth and tuupola/slim-jwt-auth ?

@odan
Copy link
Author

odan commented Apr 17, 2020

@mkraha I think the documentation of tuupola/slim-basic-auth and tuupola/slim-jwt-auth is good enough.

@Webudvikler-TechCollege
Copy link

Webudvikler-TechCollege commented May 18, 2020

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.

@odan
Copy link
Author

odan commented May 18, 2020

Hi @Webudvikler-TechCollege

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();
},

@Webudvikler-TechCollege

@odan, thank you so much. It worked for me. And thank you for the article! It has been a really great help!

@samuelgfeller
Copy link

samuelgfeller commented Nov 18, 2020

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?

@odan
Copy link
Author

odan commented Nov 18, 2020

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.

@samuelgfeller
Copy link

@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.

@odan
Copy link
Author

odan commented Nov 18, 2020

@samuelgfeller Yes, thanks. I will add it to the article.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment