Jwt-auth: Cant pass token on tests (also, please add how to test to the Wiki)

Created on 7 Aug 2015  路  26Comments  路  Source: tymondesigns/jwt-auth

Hello amazing community.
This week I got a new project and I decided to give this bundle a try.
As a good dev should do, Im testing everything I can.
But, unfortunately, I cant perform a basic test, and I dont know why (not sure if Im doing wrong or if its a problem with the bundle).
Also, Id like to suggest to update the Wiki, adding a section for "testing best practices", since we all need to know how to generate tokens and test our app :mag_right:

The problem

I`ve got a route for login and a protected route:

<?php
use Illuminate\Http\Response as HttpResponse;

// login doesnt need JWT authentication
Route::post('login', function () {
    $credentials = Input::only('email', 'password');

    if (!$token = JWTAuth::attempt($credentials)) {
        return response()->json(false, HttpResponse::HTTP_UNAUTHORIZED);
    }

    return response()->json(compact('token'), HttpResponse::HTTP_ACCEPTED);
});

// all other admin routes needs to be verified for JWT Token
// When not logged in, Exceptions are raised and intercepted at App/Exceptions/Handler
Route::group(['before' => 'jwt-auth'], function () {
    Route::get('/', function () {
        $user = JWTAuth::parseToken()->toUser();
        return response()->json(compact('user'));
    });
});

And I wrote this simple test for it:

<?php
namespace Tests\App\Http;

use Illuminate\Http\Response as HttpResponse;
use JWTAuth;

/**
 * Class AdminRoutesSecurityTest
 * This class holds tests for validating security measures and JWT integration
 * for ADMIN API routes.
 *
 * @package Tests\App\Http
 */
class AdminRoutesSecurityTest extends \TestCase
{
    /**
     * User may want to access the main admin page without authenticating.
     * User should get access denied
     */
    public function testGetMainUnauthenticated()
    {
        // as a user, I try to access the admin panels without a JWT token
        $response = $this->call('GET', '/');
        // I should be blocked
        $this->assertEquals(HttpResponse::HTTP_UNAUTHORIZED, $response->status());
    }

    /**
     * User may want to access the main admin page.
     * For this, they will pass a JWT token
     */
    public function testGetMainAuthenticated()
    {
        $credentials = JWTAuth::attempt(['email' => '[email protected]', 'password' => 'secret']);

        // as a user, I try to access the admin panels without a JWT token
        $response = $this->call(
            'GET',
            '/',
            [], //parameters
            [], //cookies
            [], // files
            ['HTTP_Authorization' => 'Bearer ' . $credentials], // server
            []
        );
        // I should be accepted
        $this->assertEquals(HttpResponse::HTTP_OK, $response->status());
    }

    /**
     * User may want to login, but using wrong credentials.
     * This route should be free for all unauthenticated users.
     * Users should be warned when login fails
     */
    public function testLoginWithWrongData()
    {
        // as a user, I wrongly type my email and password
        $data = ['email' => 'email', 'password' => 'password'];
        // and I submit it to the login api
        $response = $this->call('POST', 'login', $data);
        // I shouldnt be able to login with wrong data
        $this->assertEquals(HttpResponse::HTTP_UNAUTHORIZED, $response->status());
    }

    /**
     * User may want to login.
     * This route should be free for all unauthenticated users.
     * User should receive an JWT token
     */
    public function testLoginSuccesfull()
    {
        // as a user, I wrongly type my email and password
        $data = ['email' => '[email protected]', 'password' => 'secret'];
        // and I submit it to the login api
        $response = $this->call('POST', 'login', $data);
        // I should be able to login
        $this->assertEquals(HttpResponse::HTTP_ACCEPTED, $response->status());
        // assert there is a TOKEN on the response
        $content = json_decode($response->getContent());
        $this->assertObjectHasAttribute('token', $content);
        $this->assertNotEmpty($content->token);
    }
}

All tests are passing, the only exception is testGetMainAuthenticated. Its failling, and I dont know why.
Apparently, while testing, JWT is unable to get the Authorization header.

Any thoughts?

Most helpful comment

Instead of parsing the token in the headers, set it as a query param like ..?token={token} then have a method in a class like:

    public function isValidToken(string $token)
    {
        try {
            if (!JWTAuth::authenticate($token)) {
                return response()->json(['user_not_found'], 404);
            }
        } catch (TokenExpiredException $e) {
            return response()->json(['token_expired'], $e->getStatusCode());
        } catch (TokenInvalidException $e) {
            return response()->json(['token_invalid'], $e->getStatusCode());
        } catch (JWTException $e) {
            return response()->json(['token_absent'], $e->getStatusCode());
        }

        return response()->json(['token_valid']);
    }

Your controller can get the token by invoking: $request->input('token'); and pass it on the class you created with the isValidToken() method.

Hope someone finds this useful!

All 26 comments

Im on Laravel 5.1

I've experienced the same issue.

Route works when manually tested, headers are sent in testing, but JWT-Auth mysteriously never read them. Did a bunch of debugging, determined that for some reason the request wasn't being properly set insite JWT-Auth.

Workaround was to add this to my controller:

public function __construct(){
    if ((\App::environment() == 'testing') && array_key_exists("HTTP_AUTHORIZATION",  \Request::server())) {
        JWTAuth::setRequest(\Route::getCurrentRequest());
    }
}

Not sure if this is relevant in your case, but I was having the same sort of issue with a Dingo API.

In my phpunit.xml file I had to specify localhost for the API domain and then my auth headers would be picked up.

I fear that your other tests where giving false positives by checking for the response status. To be safe, I would change your asserts to $this->seeJsonContains() and check a message so that you can be sure you are failing authentication rather than checking a generic 401 response.

+1

Have exactly same issue. Infact keep getting following:

2015-10-27 16:02:49] testing.ERROR: exception 'Tymon\JWTAuth\Exceptions\JWTException' with message 'The token could not be parsed from the request' in /vagrant/clip-team/vendor/tymon/jwt-auth/src/JWTAuth.php:195

@mrgodhani @hootlex you can add the following to your testCase.php file

 private function setClientToken(){
        $client = factory(App\User::class)->create(['type' => 'client']);

        $this->client = $client;
        $this->clientToken = JWTAuth::fromUser($client);
    }

    public function clientGet($url, $data = [])
    {
        $url .= '?token=' . $this->clientToken;
        return $this->get($url, $data);
    }

Also call $this->setClientToken() in the createApplication function.

And then in your tests, you can simply call $this->clientGet()..

@jadjoubran using your method I get following:

[2015-10-27 16:17:49] testing.ERROR: exception 'Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException' with message 'Failed to authenticate because of bad credentials or an invalid authorization header.' in /vagrant/clip-team/vendor/dingo/api/src/Auth/Auth.php:113

@mrgodhani Do you have the fix in your .htaccess?

@mrgodhani https://github.com/tymondesigns/jwt-auth/wiki/Authentication
Under note to Apache users

@jadjoubran well I use nginx. It works fine when using postman. It just doesn't work from testsuite.

ye still get same when passing as query string ?token= . I get following:

[2015-10-27 16:26:15] testing.ERROR: exception 'Tymon\JWTAuth\Exceptions\JWTException' with message 'The token could not be parsed from the request' in /vagrant/clip-team/vendor/tymon/jwt-auth/src/JWTAuth.php:195

@mrgodhani sorry mate can't help:/
I'm using it right now in a project and I have over 100 test cases there. I have tymondesigns/jwt-auth 0.5.5
Make sure your have this version in your composer.lock

Btw I am not trying to read the token from the tests though. All I care about is to see if the api.auth middleware is working (which is provided by dingo/api)

I don't mean to advertise, but if you're using Angular then you can get most of these stuff working out of the box here

Please make sure to update the post if you were able to fix it

@jadjoubran Found the solution. You need to add $this->refreshApplication(); under clientGet before requesting. As referenced here:

https://laracasts.com/discuss/channels/testing/laravel-testig-request-setting-header
https://laracasts.com/discuss/channels/testing/laravel-testcase-not-sending-authorization-headers

In general, Laravel+PHPUnit isn't very compatible with sending multiple requests in a single test. The application does not create new Request objects (among other things) because the testing environment handles Laravel's IoC container different than other environments do. This is a known issue, but not one likely to change. Automatically refreshing components in tests would be a drastic change, and it would be very difficult to distinguish when it's necessary or useful.

Yes, you can force the whole application to refresh with $this->refreshApplication(), or there may be a few ways to force JWTAuth specifically to refresh what is necessary. (perhaps something like @rlugge's use of JWTAuth::setRequest())

However, if you have the time, I would personally advocate for breaking your tests up into smaller pieces. You can have each request in its own test. If your test requires an existing state, like an invalidated token, you can create that manually. That way you aren't accidentally testing too many interactions and having a hard time separating them. (Indeed, to many people, even a _single_ server request is already too complex for a "unit test", though the Laravel framework is of the opinion that it's fine.)

If you really want these large-scale multi-request tests, your tests are definitely more "_functional_" than "unit", and you should probably look into a functional testing framework like Codeception or Behat.

I've just encountered this problem. I thought it would be helpful to share exactly what code is at fault. The root of the problem is coming from both Laravel and jwt-auth.

On the Laravel side of things, you'll only encounter this problem if you try to use the request methods from Illuminate\Foundation\Testing\CrawlerTrait. The various request methods on this trait (post, get, patch) are really just wrappers for CrawlerTrait::call().

    public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
    {
        $this->currentUri = $this->prepareUrlForRequest($uri);

        $request = Request::create(
            $this->currentUri, $method, $parameters,
            $cookies, $files, $server, $content
        );

        return $this->response = $this->app->make('Illuminate\Contracts\Http\Kernel')->handle($request);
    }

The call() method does something really cool here. It creates a request object and passes it on to the http Kernel for handling. This allows you to test controller actions without having to send a request over the wire.

Now let's take a look at some code from JWTAuthServiceProvider;

protected function registerJWTAuth()
    {
        $this->app['tymon.jwt.auth'] = $this->app->share(function ($app) {

            $auth = new JWTAuth(
                $app['tymon.jwt.manager'],
                $app['tymon.jwt.provider.user'],
                $app['tymon.jwt.provider.auth'],
                $app['request']
            );

            return $auth->setIdentifier($this->config('identifier'));
        });
    }

Nothing out of the ordinary here. This code is simply registering an instance of JWTAuth into the application. The only problem is that the request object that's being passed is the one coming from the PHPUnit request. JWTAuth will not be able to parse a token from a Laravel test "request" because they aren't passed through as you might expect.

You won't have this problem if you use an HTTP library like Guzzle to send requests to your API. However, those requests will not load environment variables from phpunit.xml.

Perhaps having the ability to pass a request object directly into the JWTAuth::parseToken() could fix this?

public function parseToken($method = 'bearer', $header = 'authorization', $query = 'token', $request = null)

@tjdavenport thankyou for looking into that.. Would you mind testing with the 0.6.*@dev release to see if it has been resolved there? Since you can set the request on the class that parses the request - see here.

This is accessible via the JWTAuth instance as it's passed through appropriately

@tymondesigns I'll test when I get some time.

For now, this is the best bandaid solution I can come up with

        Event::listen('router.matched', function($route, $request) {
            if (env('APP_ENV') === 'testing') {
                JWTAuth::setRequest($request);
            }
        });

That will set the correct request object regardless of where it came from.

There are two ways to send a token to our API, If header is not working simply append to your URL.
Example:

 http://api.mysite.com/me?token={yourtokenhere}

So you could do this:

In your TestCase

public function signIn($data=['email'=>'[email protected]', 'password'=>'password'])
{
    $this->post('/auth/signin', $data);
    $content = json_decode($this->response->getContent());

    $this->assertObjectHasAttribute('token', $content, 'Token does not exists');
    $this->token = $content->token;

    return $this;
 }

This will grab and store a token in $this->token so you can use it to make a request

Then later in your tests

public function testResticted()
{
    $this->signIn();
    $this->call('GET', '/restricted/page', ['token'=>$this->token], $cookies = [], $files = [], $server = []);
    dump($this->response);
 }

@tymondesigns Sorry to post here in this closed issue but I'm having the same problem but in my case in Lumen 5.2. If you prefer, I can open a new issue.

I have the 0.6.*@dev version of jwt-auth and everything works fine outside the test environment but when I try to run the tests with phpunit I get a exception 'TymonJWTAuth\Exceptions\JWTException' with message 'A token is required' in JWT:237.

My routes are defined to go through the jwt.auth middleware provided. In the setup of my tests I create a token and send it with each request. I checked that the request has the correct header so I think the problem is with JWT-Auth not reading it correctly as @tjdavenport suggested.

Can anyone give me any advice on how I can solve this or a workaround for this issue?

Thank you!

Edit: The real problem is that when you run the tests with PHPUnit you can't access the token through the JWTAuth facade. I manage to solve this by not using the facades to retrieve the tokens, which is probably a better solution.

calling refreshApplication fixed this for me in Lumen 5.2

I'm currently facing this issue with both Codeception and Laravel Tests powered by PHPUnit. Both methods (?token and HTTP Header) does not work. Codeception $I->amBearerAuthenticated also gives "Could not parse token from the request".

The only way I was able to put a band-aid on it was creating a middleware and setting it to all my routes in the Kernel Middleware attribute.

<?php

namespace App\Http\Middleware;

use Closure;
use Tymon\JWTAuth\Facades\JWTAuth;

class JWTToken {

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next) {
        if (!empty($request->header('authorization')) || !empty($request->get('token')))
            JWTAuth::setRequest($request);
        return $next($request);
    }
}

I tried installing version 1.0.0-alpha-2 to see if it would work, but the installation gives me the following error:

  - Installing tymon/jwt-auth (1.0.0-alpha.2)
    Downloading: 100%

Writing lock file
Generating autoload files
> Illuminate\Foundation\ComposerScripts::postUpdate
> php artisan optimize
PHP Fatal error:  Class 'Tymon\JWTAuth\Providers\JWTAuthServiceProvider' not found in /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php on line 146
PHP Stack trace:
PHP   1. {main}() /var/www/html/thunderwall/artisan:0
PHP   2. Illuminate\Foundation\Console\Kernel->handle() /var/www/html/thunderwall/artisan:36
PHP   3. Illuminate\Foundation\Console\Kernel->bootstrap() /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:105
PHP   4. Illuminate\Foundation\Application->bootstrapWith() /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:219
PHP   5. Illuminate\Foundation\Bootstrap\RegisterProviders->bootstrap() /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:203
PHP   6. Illuminate\Foundation\Application->registerConfiguredProviders() /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php:17
PHP   7. Illuminate\Foundation\ProviderRepository->load() /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:530
PHP   8. Illuminate\Foundation\ProviderRepository->compileManifest() /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php:60
PHP   9. Illuminate\Foundation\ProviderRepository->createProvider() /var/www/html/thunderwall/vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php:114


  [Symfony\Component\Debug\Exception\FatalErrorException]
  Class 'Tymon\JWTAuth\Providers\JWTAuthServiceProvider' not found

I don't know if it's really right solution, but You can make a per-test authentication directly from your test cases. For simplicity, I have created a method in my test case (it can be trait anyway)

public function authenticate(App\User $user)
{
    JWTAuth::setToken(JWTAuth::fromUser($user));
}

Then I just call $this->authenticate(User::first()); before making a request.

I hope this will be helpful.

How does that work when everything is reset once the request is made?

We have the following function on TestCase.php to generate auth headers.

````php
/**
* Generate authenication headers
*
* @return String
*/
public function createAuthHeader(User $user, $refreshApplication = false)
{
$this->authHeaders = ['Authorization' => 'Bearer ' . \TymonJWTAuth\FacadesJWTAuth::fromUser($user)];

    // Strange auth bug, we need to reboot the appilication
    // SEE: https://laracasts.com/discuss/channels/testing/laravel-testig-request-setting-header
    if ($refreshApplication) {
        $this->refreshApplication();
        $this->setUp();
    }

    return $this->authHeaders;
}

````

The only problem with this is that if you are using sqlite :memory: it also wipes the database.

I ended up with the following:

http://hocza.com/2017-01-09/day-1-lumen-jwt-testing-tymon-jwtauth/

I used this (5.4) in my TestCase class based on what @SomethingWrong posted:

public function authenticate(\App\User $user)
{

  \Tymon\JWTAuth\Facades\JWTAuth::setToken(\Tymon\JWTAuth\Facades\JWTAuth::fromUser($user));
}

Instead of parsing the token in the headers, set it as a query param like ..?token={token} then have a method in a class like:

    public function isValidToken(string $token)
    {
        try {
            if (!JWTAuth::authenticate($token)) {
                return response()->json(['user_not_found'], 404);
            }
        } catch (TokenExpiredException $e) {
            return response()->json(['token_expired'], $e->getStatusCode());
        } catch (TokenInvalidException $e) {
            return response()->json(['token_invalid'], $e->getStatusCode());
        } catch (JWTException $e) {
            return response()->json(['token_absent'], $e->getStatusCode());
        }

        return response()->json(['token_valid']);
    }

Your controller can get the token by invoking: $request->input('token'); and pass it on the class you created with the isValidToken() method.

Hope someone finds this useful!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kofi1995 picture kofi1995  路  3Comments

agneshoving picture agneshoving  路  3Comments

marciomansur picture marciomansur  路  3Comments

lbottoni picture lbottoni  路  3Comments

johncloud200 picture johncloud200  路  3Comments