laravel / passport

Laravel Passport provides OAuth2 server support to Laravel.
https://laravel.com/docs/passport
MIT License
3.28k stars 777 forks source link

Unable to refresh the access token via /oauth/token #697

Closed paulm17 closed 6 years ago

paulm17 commented 6 years ago

I've gone through a lot of SO questions, asked in gitter and larachat and been through a lot of medium guides. Opening up an issue because I've run out of ideas.

I have a situation where I need short lived access tokens. I have an access token which has expired and now I need to refresh it.

Here's what the userLogin controller looks like, which gives out the access and refresh tokens.

public function userLogin(Request $request) {        
    $validator = Validator::make($request->all(), [
        'email' => 'required|email',
        'password' => 'required|min:3|max:12'
    ]);

    if ($validator->fails()) {
        return response()->json(['error' => $validator->errors()], 401);
    }

    $data = [
        'grant_type' => 'password',
        'client_id' => env('CLIENT_ID', ''),
        'client_secret' => env('CLIENT_SECRET', ''),
        'username' => request('email'),
        'password' => request('password'),
    ];

    $request = Request::create('/oauth/token', 'POST', $data);

    return app()->handle($request);        
}

I put this here in the event that it's incorrect.

Here is the payload that is given:

access token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImUxYzkzMjU1ZjhmMDRlNDBkMjE5ZWNjYTA5YWU0M2QwMzc5MmY2NDQxNTZjMmE5NWI1NmQ5MTg0NGU4OTEzOWVlYzRkYzIxMGRiZjQyY2JjIn0.eyJhdWQiOiIyIiwianRpIjoiZTFjOTMyNTVmOGYwNGU0MGQyMTllY2NhMDlhZTQzZDAzNzkyZjY0NDE1NmMyYTk1YjU2ZDkxODQ0ZTg5MTM5ZWVjNGRjMjEwZGJmNDJjYmMiLCJpYXQiOjE1MjQ5NDAzNTksIm5iZiI6MTUyNDk0MDM1OSwiZXhwIjoxNTI0OTQwNDc5LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.zkFBtSmNGqVE-fNVaWgksieeAEYzgSaWal4Yo1Tjmp4SnSRJiT_9wxVFtn5UZH81JV3jaQisPeWFC-6SqB5kQIbT_O1m3KZTUdpxzEap-cI8bya1JC63r-yQ_3PfegST9HCImuQFAFK6QiOt0q1drfa3z0omvxV4ZKaF9TinjkUBLuj13xWK41rfvdMpBD9w5yROS8knm1lBfaX59DAoOTeD0LBEUmw-1u9KGmQ3Jv-nJ8_YUGFKidiEOXbZcCxllaD3pFJ5USncG8-d2NS93pDSqSVhlw4ka-hh6AO-jxAl5yLrxfwhC1L62oOV-8XDFb1zLGePUowTvqRgVbWMrqWIzLPUJLMyy2yegHZjhj7yNb0TE8TExpWYDuXvC6CqTtPsY7TIretWN0-RghAFNha7Vs3lvClIEygZW_EcYFQA08gETma4C_Wrd58yTM4diHiguWvEQO6PePKKNcI1Twbb20Xxeik_4h6_gXS8e5pYFsEzv-cn3-DfRHke5f8kEkc0_8F4FWsxe3LQPwbR4TFXGQe6QWGPw8twxOScY55fsC6MzZMnPYsnHgznIeSvvLynaRTi5OvZ-U03xMqJ9iyvUNQDosQldpOfKreu2GvBX8UrR7P-lqxrs4vT9K8-lKBU-zh-HqfLDcqnB0JoSIozmwHkifnxEnWjFOfeh-g

expiration:

1524940534635

refresh token:

def5020081df3f1d3e50280cf7fe7bf30c34faa7a8370ac84eaae2bf7d51c6053592f13b3b2a180c4897e4afc774c66af663d1cb6d3637bc50b9f49cf4feec58f1aef5eebd0c098085079e2a238986626af2834a7600ccfbaef1408d5300881cfe3b84acbe6d9bb208c75ad3109f0dfcdbe1fb69a0b1263f1a8aaa6d9a5d7a2f048e7f8235911155fdb48e208d4c4f1fc10f98679141a171e104aa851848e6284de1e919cb7b588e470f0549449f610e55194e71937a782267a9a791732e49903c4b6c624603caaf10e8e092f962f7c43741866e3d93aac377988b59b6a3b4a4a94864907626da75e2cb22aead00b4fdae203caf9d14c808f620d101cbb1fbb6c2db6d80daf01c60c5ce87d8a4199039ee29ba0f5a212f2a9a9c2809e261013226ce3080c41420e93392539356a0061a369f8e287301ee6b4df564a5b2ea39a47f68797c272b4c3dfa16a2f610c4f4dd836445f51df1203725d760e56e8314d6c4

token type:

Bearer

I have a middleware that is checking the expiry date of the access token and to determine if it needs refreshing:

class CheckAccessToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $request->header('authorization')));
        $token = (new \Lcobucci\JWT\Parser())->parse($jwt);
        $access_token = $token->getHeader('jti');
        $expires_at = $token->getClaim('exp');
        $refresh_token = $request->header('refresh-token');

        if (Carbon::now()->timestamp > $expires_at) {
            // Access Token has expired
            // Check Refresh Token
            $table = DB::table('oauth_refresh_tokens')
            ->select('id', 'expires_at')
            ->where('access_token_id', $access_token)
            ->where('revoked', 0)
            ->first();

            $expires_at = Carbon::parse($table->expires_at)->timestamp;
            $client_id = env('CLIENT_ID', '');
            $client_secret = env('CLIENT_SECRET', '');

            // Check Refresh Token Expired
            if ($expires_at > Carbon::now()->timestamp) {
                // Refresh Access Token
                $data = [
                    'grant_type' => 'refresh_token',
                    'refresh_token' => $refresh_token,
                    'client_id' => env('CLIENT_ID', ''),
                    'client_secret' => env('CLIENT_SECRET', ''),
                    'scope' => '',
                ];

                $request = Request::create('/oauth/token', 'POST', $data);
                $response = app()->handle($request);
                $data = json_decode($response->getContent());

                dd($response->getContent());
            } else {
                // Get UX to show Login Modal
                return response()->json([
                    'error' => [
                        'status' => 401,
                        'message' => 'Unauthorized',
                    ],
                ], 401);                          
            }
        }

        return $next($request);
    }
}

Finally, I have looked at this guide for refreshing the token, but I keep seeing the error message that two dots are required.

https://laravel.com/docs/5.6/passport#refreshing-tokens

Would appreciate any help here.

Thanks

jayant1993 commented 6 years ago

Can you show the guzzle error?

Sephster commented 6 years ago

Your JWT token is not generated correctly.

def5020081df3f1d3e50280cf7fe7bf30c34faa7a8370ac84eaae2bf7d51c6053592f13b3b2a180c4897e4afc774c66af663d1cb6d3637bc50b9f49cf4feec58f1aef5eebd0c098085079e2a238986626af2834a7600ccfbaef1408d5300881cfe3b84acbe6d9bb208c75ad3109f0dfcdbe1fb69a0b1263f1a8aaa6d9a5d7a2f048e7f8235911155fdb48e208d4c4f1fc10f98679141a171e104aa851848e6284de1e919cb7b588e470f0549449f610e55194e71937a782267a9a791732e49903c4b6c624603caaf10e8e092f962f7c43741866e3d93aac377988b59b6a3b4a4a94864907626da75e2cb22aead00b4fdae203caf9d14c808f620d101cbb1fbb6c2db6d80daf01c60c5ce87d8a4199039ee29ba0f5a212f2a9a9c2809e261013226ce3080c41420e93392539356a0061a369f8e287301ee6b4df564a5b2ea39a47f68797c272b4c3dfa16a2f610c4f4dd836445f51df1203725d760e56e8314d6c4

I think your code is expecting your refresh token to be a JWT but the refresh token you've listed above isn't a JWT.

paulm17 commented 6 years ago

@jayant1993 Thanks for responding. I have updated the function. The guzzle error was a mistype. I have updated the text.

@Sephster Due to the new response, I started looking at this again and before I was just starting with laravel and now things have progressed and I see where I was going wrong.

I have updated the function and I'm passing in the actual refresh tokens that I get from when the user is first authenticated.

However, looking at some other issues here. I have found that the refresh token is correct. So I don't know why I cant perform the refresh token task.

paulm17 commented 6 years ago

Resolved!

I completely missed the fact that the new Request::create was going through the middleware process again. Of course, at this point there is no Access Token, hence the parse is going to fail.

Changed my code to reflect this fact and of course, it's working fine!

nicolasflorth commented 6 years ago

@paulm17 Hi Paul! How did you solved your problem? I can't understand what changes you added to the initial code as a fix. Thanks!

paulm17 commented 6 years ago

@nicolasflorth What I realised was that using the middleware was the wrong approach for an SPA.

I'm able to with my Vue app, to use two endpoints depending on the state of the access token. If it's not present to use the login API endpoint. If it's available to then submit itself and the refresh token to the refreshToken API endpoint.

I used many guides to craft my auth mechanism for Vue. Here is one of those guides: http://voerro.com/blog/building-spas-with-laravel-5.5-and-vue.js-2-pt.1

Hope this all helps:


Here's my login endpoint:

public function userLogin(Request $request) {
    $validator = Validator::make($request->all(), [
        'emailAddress' => 'required|email',
        'password' => 'required|min:3|max:12'
    ]);

    if ($validator->fails()) {
        return response()->json(['error' => $validator->errors()], 401);
    }

    $data = [
        'grant_type' => 'password',
        'client_id' => env('CLIENT_ID', ''),
        'client_secret' => env('CLIENT_SECRET', ''),
        'username' => request('emailAddress'),
        'password' => request('password'),
        'scope' => '',
    ];

    $request = Request::create('/oauth/token', 'POST', $data);
    return app()->handle($request);        
}

Here's my refresh token endpoint:

public function userRefreshToken(Request $request)
{
    $jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $request->header('authorization')));
    $token = (new \Lcobucci\JWT\Parser())->parse($jwt);
    $access_token = $token->getHeader('jti');
    $expires_at = $token->getClaim('exp');

    // Get UserID
    $userToken = AccessToken::where('id', $access_token)->first();
    $userID = $userToken->users->id;        

    // Get Refresh Token
    $userRefreshToken = UserRefreshToken::where('user_id', $userID)->first();
    $refreshToken = $userRefreshToken->refresh_token;

    // Access Token has expired
    // Check Refresh Token
    $table = DB::table('oauth_refresh_tokens')
    ->select('id', 'expires_at')
    ->where('access_token_id', $access_token)
    ->where('revoked', 0)
    ->first();

    $expires_at = Carbon::parse($table->expires_at)->timestamp;
    $client_id = env('CLIENT_ID', '');
    $client_secret = env('CLIENT_SECRET', '');

    // Check Refresh Token Expired
    if ($expires_at > Carbon::now()->timestamp) {
        // Refresh Access Token
        $data = [
            'grant_type' => 'refresh_token',
            'refresh_token' => $refreshToken,
            'client_id' => env('CLIENT_ID', ''),
            'client_secret' => env('CLIENT_SECRET', ''),
            'scope' => '',
        ];            

        $request = Request::create('/oauth/token', 'POST', $data);
        $response = app()->handle($request);
        $rData = json_decode($response->getContent());

        $userRefreshToken = UserRefreshToken::where('user_id', $userID)->first();
        $userRefreshToken->refresh_token = $rData->refresh_token;
        $userRefreshToken->save();

        $data = array(
            "token_type" => $rData->token_type,
            "expires_in" => $rData->expires_in,
            "access_token" => $rData->access_token,
            "refresh_token" => $rData->refresh_token
        );

        return response()->json($data, 200);
    } else {
        // Refresh Token Expired
        // Get UX to show Login Modal
        return response()->json([
            'error' => [
                'status' => 401,
                'message' => 'Unauthorized',
            ],
        ], 401); 
    }
}

AccessToken Model:

namespace App\Models\Common;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class AccessToken extends Model
{
    protected $table = 'oauth_access_tokens';

    /*public function refresh_token(){
        return $this->hasMany('App\RefreshToken', 'id', 'access_token_id');
    }*/

    public function users() {
        return $this->belongsTo('App\User', 'user_id', 'id');
    }
}

UserRefreshToken Model:

namespace App\Models\Common;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Rorecek\Ulid\HasUlid;

class UserRefreshToken extends Model
{
    use HasUlid;
    use SoftDeletes;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'user_refresh_tokens';

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = ['deleted_at'];

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [

    ];
}
nicolasflorth commented 6 years ago

@paulm17 Thanks a lot for your helpful fast answer. I still have a problem, it seems to be everywhere on the internet without a clear solution.

I am doing it pretty much the same like in this tutorial http://voerro.com/blog/building-spas-with-laravel-5.5-and-vue.js-2-pt.1 and as yourself

When doing: `$client = DB::table('oauth_clients')->where('id', 3)->first(); $data = [ 'grant_type' => 'password', 'client_id' => $client->id, 'client_secret' => $client->secret, 'username' => request('username'), 'password' => request('password'), ];

$request = Request::create('/oauth/token', 'POST', $data);`

I get the same as

"token_type": "Bearer", "expires_in": 31536000, "access_token": "eyJ0eXAiOiJK...", "refresh_token": "def50200..." it seems to be fine, but

What I have been researching and tried to fix for hours is that the access_token generated is starting with "eyJ0eXAiOiJK...." and refresh_token with "def50200..." (always the same at the beginning of the string) but into the database "oauth_access_tokens" table the access_token is different and a lot shorter, like 1edef183c8e55baa4b28a70482de37902f658ed64a7b996259f955c074951915374c87ad4e160528 The same with refresh_token.

To make it work I need to interrogate the database by User ID, get the access_token and send it as Authorization Header to frontend.

I don't know if is something that I don't understand...a step that I miss

paulm17 commented 6 years ago

From what I can remember, the tokens in the database have been encoded.

My solution fixes the problems you are having.

What I suggest is going through my code piecemeal and working out how it works. It's what I had to do, to figure out the final solution.

jmoraleswk commented 4 years ago

so in the end, you didnt use the middleware ?? use 2 endpoints ?? @paulm17