tuupola / slim-jwt-auth

PSR-7 and PSR-15 JWT Authentication Middleware
https://appelsiini.net/projects/slim-jwt-auth
MIT License
827 stars 141 forks source link

Token not found / apache / php #14

Closed zeced closed 8 years ago

zeced commented 8 years ago

On my Slim setup (v3) with php 5.6.14, token can't be found, i need to get it via apache_request_headers() and set $_SERVER['HTTP_AUTHORIZATION'] in index.php. I've tried different .htaccess directives like the one provided here without success.

Would it be possible to check if token can be found in 'apache_request_headers' ?

/**
 * Fetch the access token
 *
 * @return string|null Base64 encoded JSON Web Token or null if not found.
 */
public function fetchToken(RequestInterface $request) 
{

   /* If using PHP in CGI mode and non standard environment */
    $server_params = $request->getServerParams();
    if (isset($server_params[ $this->options["environment"]])) 
    {
        $message = "Using token from environent";
        $header = $server_params[ $this->options["environment"]];
    }

    $header = $request->getHeader("Authorization");
    if (isset($header[0])) 
    {
        $message = "Using token from request header";
        $header = isset($header[0]) ? $header[0] : "";
    }

    /* FIX for apache */
    if (function_exists('apache_request_headers')) 
    {
        $headers = apache_request_headers();
        $header = isset($headers['Authorization']) ? $headers['Authorization'] : "";
    }

    if (preg_match("/Bearer\s+(.*)$/i", $header, $matches)) 
    {
        $this->log(LogLevel::DEBUG, $message);
        return $matches[1];
    }

    /* Bearer not found, try a cookie. */
    $cookie_params = $request->getCookieParams();

    if (isset($cookie_params[ $this->options["cookie"]])) 
    {
        $this->log(LogLevel::DEBUG, "Using token from cookie");
        $this->log(LogLevel::DEBUG, $cookie_params[ $this->options["cookie"]]);
        return $cookie_params[ $this->options["cookie"]];
    };

    /* If everything fails log and return false. */
    $this->message = "Token not found";
    $this->log(LogLevel::WARNING, $this->message);
    return false;
}
tuupola commented 8 years ago

How does your .htaccess file look like?

zeced commented 8 years ago

Like this :

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [QSA,L]
</IfModule>

And call to middleware is:

$app->add(new \Slim\Middleware\JwtAuthentication([
    "relaxed" => ['192.168.98.13'], // only for testing
    "secret" => getenv('TOKEN_SECRET'),
    "logger" => $c->get('logger'),
    "callback" => function ($request, $response, $arguments) use ($app) {
        $app->jwt = $arguments["decoded"];
    },
    "rules" => [
        new \Slim\Middleware\JwtAuthentication\RequestPathRule([
            "path" => "/",
            "passthrough" => ["/users/login", "/ping"]
        ]),
        new \Slim\Middleware\JwtAuthentication\RequestMethodRule([
            "passthrough" => ["OPTIONS"]
        ])
    ]
])
);

Thanks for support :-)

ronaldo-systemar commented 8 years ago

Slim 3 don't keep $app instance anymore, you should save your JWT in container.

zeced commented 8 years ago

@ronaldo-systemar I've seen that i'm currently using an abstract class to get the SLIM instance through controllers but i will certainly pass the token through a DI container to be more "SLIM compliant". My main problem is that without apache_request_headers() the response's code is always 401.

mateuslopes commented 8 years ago

I am also having a hard time trying to get my decoded JWT token inside my routes and controllers with Slim 3.1. I understand the concept that i have to inject it through the DI, but, i don't know exactly how to do it. The JwtAuthentication->fetchToken method needs the Request object to fetch the token, so i tried to get the Request object inside my container injection. But, the only way I could do it was returning a Closure from the DI container, and then passing my Request object as an argument of this closure to fetch and decode the token.

For now, i did it like this:

$container['jwt_token'] = function (Container $c) {
    $jwtAuth = new JwtAuthentication([
         'secret' => 'my-jwt-secret'
    ]);
    return function (Request $request) use ($jwtAuth) {
        return $jwtAuth->decodeToken($jwtAuth->fetchToken($request));
    };
};

Then, in my routes definition I do like this:

$app->get('/dump-jwt-token', function($request, $response, $args) {
    $closure = $this->get('jwt_token');
    $token = $closure($request);
    $response->write($token);
    return $response;
});

Ok, this works to get my JWT inside the routes. But, I don't feel that's the right way to do it. I would appreciate some help about the right (or better) way to do it. Also, how shoudl I do to get my token from inside any controller or AbstractController class?

I am grateful for any help.

tuupola commented 8 years ago

Have you tried the example from README?

$app = new \Slim\App();

$app->add(new \Slim\Middleware\JwtAuthentication([
    "secret" => "supersecretkeyyoushouldnotcommittogithub",
    "callback" => function ($request, $response, $arguments) use ($app) {
        /* Do something with decoded token in here, for example: */
        $app->jwt = $arguments["decoded"];
    }
]));
mateuslopes commented 8 years ago

Hi tuupola! Thanks for your answer.

Yes, that was the first thing I've tried and it just not worked for me. But now, after your answer, I tried it again and I just discovered that my $app->jwt was not defined because the route I was trying to test, was listed to be a passthrough route in my JwtAuthentication settings initial object. So, when I removed the route from the passthrough array, it just worked perfectly. I noticed that happens because, when a route is passthrough route, the Jwt callback function is not called at all. I am sorry for this misunderstanding...

Okay, now I am able to retrieve my $app->jwt from inside any route declared with closures using the

function ($req,$res,$args) use ($app)
statement. But, I am still unable to retrieve it from inside routes declared inside controllers. Because, when declaring routes with controllers, I can not pass the use ($app) statement.

So, I have a base AbstractController like this:

abstract class AbstractController
{
    protected $app;
    public function __construct(Container $container)
    {
        $this->app = $container;
    }
    public function getApp()
    {
        return $this->app;
    }
   // This does not work
    public function getJwt1()
    {
        return $this->app->jwt;
    }
   // Also this does not work
    public function getJwt2()
    {
        return $this->app->get('jwt');
    }
   // Also this does not work
    public function getJwt3()
    {
        return $this->jwt;
    }
}

So now, the question is, how can I get the $app->jwt variable from inside a route defined with controllers?

mateuslopes commented 8 years ago

Now I understand how simple it is to retrieve an $app variable inside of the controllers. That is done using the container, and not the $app itself. So I just declared a container service like this:

$container['jwt'] = function (Container $c) use ($app) {
    return $app->jwt;
};

And then, I can get it from anywhere I have access to container instance (in these example from an abstract base controller):

abstract class AbstractController
{
    protected $app;
    public function __construct(Container $container) {
        $this->app = $container;
    }
    public function getJwt() {
        return $this->app->get('jwt');
    }
}

Thanks for the help! And sorry for my confusion about it.

tuupola commented 8 years ago

Great and thanks for the example! It will help people coming here from Google.

kirtangajjar commented 8 years ago

I have the same problem. I want to access JWT token body in a function which is called on a route.

Here is the route:

$app->put('/transcript', '\Myclass:Myfunction');

if we put the following code in dependencies.php

$container['jwt'] = function (Container $c) use ($app) {
    return $app->jwt;
};

and include it like:

require_once('vendor/autoload.php');
require_once('classes.php');
require_once 'dependencies.php';

$app = new \Slim\App($container);

We can't access $app in dependencies.php. And if we include it after $app like:

require_once('vendor/autoload.php');
require_once('classes.php');

$app = new \Slim\App($container);

require_once 'dependencies.php';

How could we pass $container to The App instance?

I'm new to DI and services in general. Perhaps somebody could explain it to me.

If possible, a minimal working example would be great.

tuupola commented 8 years ago

In callback store the token somewhere where you can fetch it later. Using $app is just an example. You do not need to use it.

$app = new \Slim\App();

$app->add(new \Slim\Middleware\JwtAuthentication([
    "secret" => "supersecretkeyyoushouldnotcommittogithub",
    "callback" => function ($request, $response, $arguments) use ($app) {
        print_r($arguments["decoded"]);
        /* Here save the decoded token somewhere. */
    }
]));

But this is more about Slim 3 general help and not directly related to JwtAuthentication middleware. Slim docs have some explanation on how the dependency injection works.

ronaldo-systemar commented 8 years ago

@kirtangajjar If you look a little up, @mateuslopes described a perfect example for your issue. Create a controller (base controller) that be extended by all your controllers, so, inject the container into it and voilà.

E.g:

Base.php

<?php
namespace Controllers;

class Base
{
    protected $container;

    public function __construct(\Slim\Container $container)
    {
        $this->container = $container;
    }
}

Users.php

<?php
namespace Controllers;

class Users extends Base
{
    /* Your controller actions */
}

Now, in your src/dependencies.php (if you have it, of course), do it:

$container = $app->getContainer();

// Controller container inject
$container['Base'] = function ($c) {
    return new Base($c);
};

Everytime Base controller is called, Slim will look into DI container and find the definition above, so, a new instance of Base is created with container injected, saving it into protected $container. Now, you can easy access container inside controller like this, e.g: $this->container->get('settings')['app']['key'] or something like this $this->container['jwt'];

If you are lost with file structure definition, as I said early src/dependencies.php, look at this skeleton project: https://github.com/slimphp/Slim-Skeleton

--- EDIT --- I forget to mention, you must instantiate your middleware like this, e.g:

$app->add(new Slim\Middleware\JwtAuthentication([
    "secret" => 'my_secret_key',
    "callback" => function ($request, $response, $arguments) use ($container) {
        $container['jwt'] = $arguments["decoded"];
    }
]));

Good lucky and have a nice coding

kirtangajjar commented 8 years ago

Oh! Thanks @ronaldo-systemar for the example. That piece of code really helped.

I was facing same fate as @mateuslopes and was unable to get token header. Later did I realized i had put it in passthrough for debugging.

Now that I have removed it, it works fine.

Thanks everyone for helping out!

ronaldo-systemar commented 8 years ago

@kirtangajjar You are welcome =)

A lot of Slim 2 users are facing this same problem when upgrading to Slim 3, because $app instance was easy to get into old version inside controllers. Since Slim 3 changed approach, basically everything is handled into container or middleware.

tuupola commented 8 years ago

I updated the README example for callback to following. Thanks @ronaldo-systemar for the tips.

$app = new \Slim\App();

$container = $app->getContainer();

$container["jwt"] = function ($container) {
    return new StdClass;
};

$app->add(new \Slim\Middleware\JwtAuthentication([
    "secret" => "supersecretkeyyoushouldnotcommittogithub",
    "callback" => function ($request, $response, $arguments) use ($container) {
        $container["jwt"] = $arguments["decoded"];
    }
]));

Now you can access the decoded tokens in your routes with:

$app->get("/test", function ($request, $response, $arguments) {
    print_r($this->jwt);
});
ronaldo-systemar commented 8 years ago

:+1: Hope helped.

Nice coding for all.

craigify commented 8 years ago
RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-l RewriteRule (.+)$ api.php [L,QSA,env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

Did this thread get off topic a little? When using that rewrite rule combination, I send everything over to api.php. You'd think I would see $_SERVER['HTTP_AUTHORIZATION'], but instead I see $_SERVER['REDIRECT_HTTP_AUTHORIZATION']. I'm using PHP-FPM with the Apache 2.4 module mod_proxy_fcgi. I don't know if others have seen this or not, but Apache mangles the environment variable at some point in the htaccess rule processing apparently. I had to make a kludge to check for the present of 'REDIRECT_HTTP_AUTHORIZATION' and then set 'HTTP_AUTHORIZATION' on _SERVER manually, which then allowed the fetchToken() method in JwtAuthentication.php to see it properly.

[http://stackoverflow.com/questions/3050444/when-setting-environment-variables-in-apache-rewriterule-directives-what-causes]

For some discussion on the redirection rule processor and environment variables about this same thing.

craigify commented 8 years ago

Further commenting on the OP, apache_request_headers() seems to not always be available. It isn't for me when using PHP-FPM. I think it might only be when using mod_php. I'm not 100% sure on this, though....

tuupola commented 8 years ago

@craigify That is an undocumented Apache feature. If you are affected by it you can set the environment to search the token from with environment setting.

$app->add(new \Slim\Middleware\JwtAuthentication([
    "secret" => "supersecretkeyyoushouldnotcommittogithub",
    "environment" => "REDIRECT_HTTP_AUTHORIZATION"
]));

I actually thought this was documented in the README but seems I have forgotten it.

tuupola commented 8 years ago

@craigify Actually when I think about it https://github.com/tuupola/slim-jwt-auth/commit/418bfa3edfee5d80e4d2ed01341956611f64cebd makes it more developer friendly. Now the middleware checks both HTTP_AUTHORIZATION and REDIRECT_HTTP_AUTHORIZATION by default.

JamesTheHacker commented 7 years ago

The above examples are fine if you're injecting the container directly into your controllers. But what about if you're not injecting the container and doing DI (instead of service location)? How do I pass the token object to controllers?

Here's an example that doesn't work ... obviously because JWT is empty at the time it's passed to the controller.

$container['JWT'] = function() {
    return new StdClass;
};

$container['MAPT\Controllers\GroupController'] = function($ci) {
    return new MAPT\Controllers\GroupController(
        $ci->GroupRepository,
        $ci->JWT
    );
};