Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to refresh expired token #215

Open
mira-thakkar opened this issue Mar 17, 2023 · 10 comments
Open

How to refresh expired token #215

mira-thakkar opened this issue Mar 17, 2023 · 10 comments
Labels
bug Something isn't working

Comments

@mira-thakkar
Copy link

Couldn't refresh the token for expired token from refresh api

I am following the exact steps from documentation to implement the Refresh token flow. But refersh api returns 401 status for the expired token(Non-expired token works fine), but i suppose refresh_ttl makes sense to refresh expired token

While debugging, I see that it's not going upto controller, but from middleware it gives 401 . I wonder that we're not using any specific middleware for refresh route, so how come package knows that for refresh route, Authenticate middleware will accept expired token, but for other routes, not ?

Your environment:

Q A
Bug yes
Framework Laravel
Framework version 9.42.2
Package version 1.x.y
PHP version 8.1.0

Steps to reproduce

Implement the package as specified in Quick Start, and call refresh api with expired token

Expected behaviour

refresh api should be able to send new token in exchange of expired token

Actual behaviour

refresh api returns 401 response

@mira-thakkar mira-thakkar added the bug Something isn't working label Mar 17, 2023
@zerossB
Copy link

zerossB commented Mar 22, 2023

I found a solution to this problem.

In the documentation it asks to put the default guard as API.

// config/auth.php

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

But for some reason when we login using the auth() helper it does not implicitly look for the default guard previously defined in config/auth.php

For that, I made the guard I want explicit in the Login and Refresh route (in the case of the api documentation)

For example:

API code

/**
 * Get a JWT via given credentials.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function login()
{
    $credentials = request(['email', 'password']);

    if (! $token = auth()->attempt($credentials)) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $this->respondWithToken($token);
}

...

/**
 * Refresh a token.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function refresh()
{
    return $this->respondWithToken(auth()->refresh());
}

Code making the guard explicit:

/**
 * Get a JWT via given credentials.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function login()
{
    $credentials = request(['email', 'password']);

    if (! $token = auth('api')->attempt($credentials)) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $this->respondWithToken($token);
}

...

/**
 * Refresh a token.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function refresh()
{
    return $this->respondWithToken(auth('api')->refresh());
}

Note that I left the auth('api') explicit, doing so worked for me with the refresh token.

@Messhias
Copy link
Collaborator

I found a solution to this problem.

In the documentation it asks to put the default guard as API.

// config/auth.php

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

But for some reason when we login using the auth() helper it does not implicitly look for the default guard previously defined in config/auth.php

For that, I made the guard I want explicit in the Login and Refresh route (in the case of the API documentation)

For example:

API code

/**
 * Get a JWT via the given credentials.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function login()
{
    $credentials = request(['email', 'password']);

    if (! $token = auth()->attempt($credentials)) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $this->respondWithToken($token);
}

...

/**
 * Refresh a token.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function refresh()
{
    return $this->respondWithToken(auth()->refresh());
}

Code making the guard explicit:

/**
 * Get a JWT via the given credentials.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function login()
{
    $credentials = request(['email', 'password']);

    if (! $token = auth('api')->attempt($credentials)) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $this->respondWithToken($token);
}

...

/**
 * Refresh a token.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function refresh()
{
    return $this->respondWithToken(auth('api')->refresh());
}

Note that I left the auth('api') explicit, doing so worked for me with the refresh token.

The default auth helper for some reason in Laravel is always simplicity to the web guard.

Also, It'll be more helpful if we have this written down in the documentation, would you like to contribute to adding this info to the documentation?

@mira-thakkar
Copy link
Author

mira-thakkar commented Mar 28, 2023

@Messhias @zerossB i tried the way you said to specify guard in auth helper. It works well for non-expired token, but it doesn't work on my side for EXPIRED TOKEN. The refresh route just returns from the middleware giving 401 error. I am attaching my code here to make it more understandable

Controller code

 public function login(Request $request)
  {
      $user = User::find(1); // This user comes from google signin, keeping it shorter here to understand
      $token = \auth('api')->login($user);
      return $this->respondWithToken($token);
  }

  public function refresh(Request $request)
  {
      logger()->info('refresh controller',[$request->header('Authorization')]);
      $token = \auth('api')->refresh();
     return $this->respondWithToken($token);
  }

routes/api.php

Route::post('login', [SocialAuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
    Route::post('refresh', [SocialAuthController::class,'refresh']);
});

For the expired token, the log specified in the refresh method is not logged, so it's not that token is failed to refresh, but it doesn't reach upto that line.

I am curious to know if there is something about middleware that i am missing.

@zerossB
Copy link

zerossB commented Mar 29, 2023

The default auth helper for some reason in Laravel is always simplicity to the web guard.

Also, It'll be more helpful if we have this written down in the documentation, would you like to contribute to adding this info to the documentation?

Of course! I can contribute!
I'll finalize the help for @mira-thakkar and then summarize for the official documentation.

@zerossB
Copy link

zerossB commented Mar 29, 2023

@Messhias @zerossB i tried the way you said to specify guard in auth helper. It works well for non-expired token, but it doesn't work on my side for EXPIRED TOKEN. The refresh route just returns from the middleware giving 401 error. I am attaching my code here to make it more understandable

Controller code

 public function login(Request $request)
  {
      $user = User::find(1); // This user comes from google signin, keeping it shorter here to understand
      $token = \auth('api')->login($user);
      return $this->respondWithToken($token);
  }

  public function refresh(Request $request)
  {
      logger()->info('refresh controller',[$request->header('Authorization')]);
      $token = \auth('api')->refresh();
     return $this->respondWithToken($token);
  }

routes/api.php

Route::post('login', [SocialAuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
    Route::post('refresh', [SocialAuthController::class,'refresh']);
});

For the expired token, the log specified in the refresh method is not logged, so it's not that token is failed to refresh, but it doesn't reach upto that line.

I am curious to know if there is something about middleware that i am missing.

That's strange, I tested it here with the changes you commented on and it worked here. There must be something missing or something like that.

I'm going to send here the files that I changed for you to compare with yours, ok?

// routes/api.php

Route::middleware('api')->prefix('auth')->group(function () {
    Route::post('login', 'AuthController@login');
    Route::post('logout', 'AuthController@logout');
    Route::post('refresh', 'AuthController@refresh');
    Route::post('me', 'AuthController@me');
});
// App\Providers\RouteServiceProvider

protected $namespace = "App\\Http\\Controllers";

/**
 * Define your route model bindings, pattern filters, and other route configuration.
 */
public function boot(): void
{
    $this->configureRateLimiting();

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

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

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Request;

class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    public function login(Request $request)
    {
        $user = User::find(1);
        $token = auth('api')->login($user);
        return $this->respondWithToken($token);
    }

    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60
        ]);
    }

    public function me()
    {
        return response()->json(auth('api')->user());
    }

    public function logout()
    {
        auth('api')->logout();
        return response()->json(['message' => 'Successfully logged out']);
    }

    public function refresh(Request $request)
    {
        $token = auth('api')->refresh();
        return $this->respondWithToken($token);
    }
}
// config/auth.php

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

@Messhias
Copy link
Collaborator

@Messhias @zerossB i tried the way you said to specify guard in auth helper. It works well for non-expired token, but it doesn't work on my side for EXPIRED TOKEN. The refresh route just returns from the middleware giving 401 error. I am attaching my code here to make it more understandable
Controller code

 public function login(Request $request)
  {
      $user = User::find(1); // This user comes from google signin, keeping it shorter here to understand
      $token = \auth('api')->login($user);
      return $this->respondWithToken($token);
  }

  public function refresh(Request $request)
  {
      logger()->info('refresh controller',[$request->header('Authorization')]);
      $token = \auth('api')->refresh();
     return $this->respondWithToken($token);
  }

routes/api.php

Route::post('login', [SocialAuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
    Route::post('refresh', [SocialAuthController::class,'refresh']);
});

For the expired token, the log specified in the refresh method is not logged, so it's not that token is failed to refresh, but it doesn't reach upto that line.
I am curious to know if there is something about middleware that i am missing.

That's strange, I tested it here with the changes you commented on and it worked here. There must be something missing or something like that.

I'm going to send here the files that I changed for you to compare with yours, ok?

// routes/api.php

Route::middleware('api')->prefix('auth')->group(function () {
    Route::post('login', 'AuthController@login');
    Route::post('logout', 'AuthController@logout');
    Route::post('refresh', 'AuthController@refresh');
    Route::post('me', 'AuthController@me');
});
// App\Providers\RouteServiceProvider

protected $namespace = "App\\Http\\Controllers";

/**
 * Define your route model bindings, pattern filters, and other route configuration.
 */
public function boot(): void
{
    $this->configureRateLimiting();

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

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

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Request;

class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    public function login(Request $request)
    {
        $user = User::find(1);
        $token = auth('api')->login($user);
        return $this->respondWithToken($token);
    }

    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60
        ]);
    }

    public function me()
    {
        return response()->json(auth('api')->user());
    }

    public function logout()
    {
        auth('api')->logout();
        return response()->json(['message' => 'Successfully logged out']);
    }

    public function refresh(Request $request)
    {
        $token = auth('api')->refresh();
        return $this->respondWithToken($token);
    }
}
// config/auth.php

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

Did it solved your issue?

@mira-thakkar
Copy link
Author

mira-thakkar commented Apr 5, 2023

Sorry, for taking long to get back to this
But Unfortunately, no @Messhias

@zerossB i used the exact same code as you specified here, but no luck.
also i wonder we're specifying api middleware twice[ RouteServiceProvider & routes/api.php], any specific reason behind that?
i want to give details about my jwt configs as well.
As of now for testing purpose i use
JWT_TTL = 5 (mins)
JWT_REFRESH_TTL = 1440 (mins)
so when i try to make refresh call with the token after 5 mins, it gives me 401

@Messhias
Copy link
Collaborator

Messhias commented May 1, 2023

Sorry, for taking long to get back to this But Unfortunately, no @Messhias

@zerossB i used the exact same code as you specified here, but no luck. also i wonder we're specifying api middleware twice[ RouteServiceProvider & routes/api.php], any specific reason behind that? i want to give details about my jwt configs as well. As of now for testing purpose i use JWT_TTL = 5 (mins) JWT_REFRESH_TTL = 1440 (mins) so when i try to make refresh call with the token after 5 mins, it gives me 401

Ok, waiting for the details.

@larswoltersdev
Copy link

larswoltersdev commented May 26, 2023

Experiencing the same issue by reproducing the above code.

Found solution: remove the auth:api middleware from the refresh endpoint.

If you put the auth:api middleware on the refresh route, Laravel would try to authenticate the incoming request, and if the token is expired, the middleware would block the request and return an "Unauthenticated" response.

@iqbalatma
Copy link

when login, i send back 2 type token, access token (with short TTL) and refresh token (with long TTL). I also add custom claim on access and refresh, so when access token invalid, frontend can send request to refresh token, and they will get new access and refresh token. but this mechanism need to custom by myself. for blacklist token i using this approach https://dev.to/webjose/how-to-invalidate-jwt-tokens-without-collecting-tokens-47pk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants