loading...

PKCE authenticaton for Nuxt SPA with Laravel as backend

StefanT123 on April 07, 2020

In this post I will show you how you can use PKCE(Proof Key for Code Exchange) for authentication. I will use Nuxt.js, because that's what I use in...
pic
Editor guide
Collapse
thorbn profile image
Thor

Hi,
SOLVED:

Everything work fine, but now I get

When logging in, when returning to nuxt auth.vue I get:
x-access-token undefined localhost / Session 23

Is it a backend problem?

Problem was the client id in the frontend that was wrong in two files. login and auth

Collapse
thorbn profile image
Thor

Hi, when i want to retrive data (articles) when i'm loggedIn i get this error:

Access to XMLHttpRequest at 'domain.com/api/trials/' from origin 'app.domain.com/' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

// nuxt -> index.vue (works local but not on the domains)
async asyncData({ $axios }) {
const trials = await $axios.$get(process.env.LARAVEL_ENDPOINT + "/api/trials/");
return { trials };
},
......

// laravel api route /api/trials
Route::group(['middleware' => 'cors'], function()
{
Route::group(['middleware' => ['auth:api']], function () {
Route::resource('trials', 'API\TrialController')
->only(['index', 'store', 'show', 'edit', 'update','destroy']);
});
.......
// trialcontroller
class TrialController extends Controller
{
public function __construct()
{
$this->middleware('auth:api');
}
public function index()
{
$trials = Auth::user()->parentTrials()->get();
........

Collapse
stefant123 profile image
StefanT123 Author

If you are using laravel version 6.x, then you should add CORS middleware, if you are using laravel 7.x, you should just setup the CORS that's already there.

Collapse
thorbn profile image
Thor

I use this in the backend: "fruitcake/laravel-cors": "^2.0",

I can retrive data with postman if I use the header: Authorization: Bearer eyJ0eXAiOiJKV1Q.....

Do i need to set some headers to the nuxt request?

async asyncData({ $axios }) {
const trials = await $axios.$get(process.env.LARAVEL_ENDPOINT + "/api/trials/");
return { trials };
},

if yes how? i see many others have the same problem

Thread Thread
stefant123 profile image
StefanT123 Author

You need to send Bearer header with every request

Thread Thread
thorbn profile image
Thor

Do you have an example?

I'm trying with this but I cant get the token

async asyncData({ $axios }) {

async asyncData (context) {
const trials = await $axios.$get(process.env.LARAVEL_ENDPOINT + "/api/trials/", {}, { headers: {"Authorization" : Bearer ${context.app.$auth.getToken('local')}} })

I can se the cookie x-access-token in chrome developer tool under application

Thread Thread
thorbn profile image
Thor

Now I got the access_token:
const access_token = cookies.get('x-access-token');
console.log(access_token);

    const trials = await $axios.$get(process.env.LARAVEL_ENDPOINT + "/api/trials/", {}, { headers: {"Authorization" : `Bearer ${access_token}`} });

But still no content, only errors:

Access to XMLHttpRequest at 'domain.com/api/trials/' from origin 'app.domain.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
cf5ce039ccd82a3e879b.js:1 GET domain.com/api/trials/ net::ERR_FAILED


Request URL: domain.com/api/trials/
Referrer Policy: no-referrer-when-downgrade
Provisional headers are shown

Thread Thread
stefant123 profile image
StefanT123 Author

You should do something like this this.$axios.setToken(access_token, token_type);

Thread Thread
thorbn profile image
Thor

Hi again,

Now I need to "update a Post" and the error comes again, but only on the domain not local:


Console error:
Access to XMLHttpRequest at 'domain.com/api/trials/' from origin 'app.domain.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
600bbbde120028ba59aa.js:1 POST domain.com/api/trials/ net::ERR_FAILED


Laravel log error:
[2020-06-22 16:05:59] local.ERROR: The resource owner or authorization server denied the request. {"exception":"object
[stacktrace].....\TokenGuard.php(149): Laravel\Passport\Guards\TokenGuard->getPsrRequestViaBearerToken


My update submit:

UpdateForm(key) {
event.preventDefault();
var app = this;

    const access_token = cookies.get('x-access-token');
    this.$axios.setToken(access_token, 'Bearer')
    this.$axios.setHeader('Content-Type', 'application/json', [
      'post'
    ])

    this.$axios.$post(process.env.LARAVEL_ENDPOINT+'/api/trials', {
        this.editedItem.employees_dates[key],
        date: this.editedItem.key,
      }
      )
      .then(resp => {
        app.$router.push({ path: "/datetrialupdate" });

      })
      .catch(error => {

        alert("error");
      });
  },
Thread Thread
stefant123 profile image
StefanT123 Author

You need to setup the correct domain for the CORS

Thread Thread
thorbn profile image
Thor

But it works for GET (posts) and get users but not update content (post)

Thread Thread
stefant123 profile image
StefanT123 Author

See if the post request has been enabled for CORS

Thread Thread
thorbn profile image
Thor

how can I see that? i'm new to cors, its hard to debug when it works local but not on the server

Thread Thread
stefant123 profile image
StefanT123 Author

Read the documentation for fruitcake/laravel-cors

Thread Thread
thorbn profile image
Thor

When I read the docs I can't find info on what to change to get post to work . Do you know where to se an example on a Update with "post" have been used, with Nuxt/Passport? almost all examples are just GET

Thread Thread
thorbn profile image
Thor

Now it works. :)

My post looks like:

    const access_token = cookies.get('x-access-token');
    this.$axios.setToken(access_token, 'Bearer')

    this.$axios.setHeader('Content-Type', 'application/json', [
      'post'
    ])

    this.$axios
      .$post(process.env.LARAVEL_ENDPOINT + "/api/trials", {

        grant_type: 'authorization_code',
        client_id: 1,
        ..................

I also removed a / from /api/trials (is was /api/trials/)

I will try later to remove some of the code to see if it all have to be there.

Collapse
themustafaomar profile image
Mustafa Omar

Firstly, Thank you for your great work!
I've been searching for a long time on how to authenticate Vue SPAs with Laravel and found Laravel Passport but it seems that Laravel Passport wasn't made for this purpose!
and PKCE is breaking the UX, actually, this prompt is a bit confuses the users.
I'm looking for something simple for SPAs.
I found an alternative which is Sanctum, I read the intro about it in the docs and found that Scantum was mainly built for SPA.
My question about best practices and security, do you recommend using Sanctum instead?

Collapse
stefant123 profile image
StefanT123 Author

If your both applications are on the same top-level domain, yes, it's best to use Sanctrum. If they are not on the same top-level domain, you can't use Sanctrum.

Collapse
nomikz profile image
nomikz

Thanks for informative post.
I wanted to know if front and back on two domains, can't we use sanctum laravel.com/docs/7.x/sanctum#api-t... api token authentication?

Thread Thread
stefant123 profile image
StefanT123 Author

You can, but I haven't used it yet. I think it's not as flexible as passport.

Collapse
themustafaomar profile image
Mustafa Omar

Thank you for the fast response <3

Collapse
devondahon profile image
gvi

Why not using @nuxtjs/auth-next module ?

If it's not flexible enough, you can make your own scheme like this:
schemes/laravelPassport.js:

import Oauth2Scheme from '@nuxtjs/auth-next/dist/schemes/oauth2'

export default class LaravelPassport extends Oauth2Scheme {
  async logout() {
    if (this.options.endpoints.logout) {
      await this.$auth
        .requestWith(this.name, this.options.endpoints.logout)
        .catch(() => {})
    }
    return this.$auth.reset()
  }
}

And then use it like this in nuxt.config.js:

  auth: {
    redirect: {
      logout: '/',
      callback: '/auth/callback'
    },
    strategies: {
      laravelPassport: {
        provider: 'laravel/passport',
        scheme: '~/schemes/laravelPassport',
        url: 'http://backend.test',
        endpoints: {
          userInfo: '/api/user',
          logout: {
            url: '/api/logout',
            method: 'post'
          }
        },
        clientId: '4',
        clientSecret: '***'
      }
    }
  },
Collapse
stefant123 profile image
StefanT123 Author

What you did here is almost the same as if you have done it custom, without using the nuxtjs/auth-next module. So why should we introduce another package in our project if the code is similar?!

Collapse
devondahon profile image
gvi

With your method here, I'm logged in the Laravel backend.
Is it an expected result ? Is there a way to prevent from logging in the backend ?
I have the same issue with @nuxtjs/auth (and @nuxtjs/auth-next module).
Maybe it's something I'm misunderstanding about using Laravel Passport.

Collapse
stefant123 profile image
StefanT123 Author

You must be logged in the backend if you want to make a request to the backend.

Collapse
devondahon profile image
gvi

Also, why not using Passport Grant Token ?

Collapse
stefant123 profile image
StefanT123 Author

We are using Passport Grant Token

Collapse
efillman profile image
Evan Fillman

Thank you for the excellent tutorial. This is the best tutorial I have found of using Passports PKCE functionality and I would have not been able to figure it out myself. I was able to port your solution today into a React front-end with Laravel back-end.

I just have a few questions about using this technique if you have time.

1) If the OAuth2 server knows when the access token expires and therefore won't allow access of an expired token, it seems that its fine to store the access token not in a cookie since our main protection is the quick timeout? (I'm planning on using Redux which is basically session)

2) Does a registration page necessarily now need to send a user to the PKCE flow after registration rather than issue a token using password grant on initial registration?

3) Does PKCE matter for the refresh token exchange? As in, after we have gone to all this trouble to get the access token through PKCE do we shoot ourselves in the foot if we are not using the same precaution on a subsequent refresh token?

Collapse
stefant123 profile image
StefanT123 Author
  1. I'm always storing my short-lived access_token in the cookie, and my long-lived refresh_token in the httponly cookie, also Laravel has a CSRF protection out of the box. That way I'm protected from potential XSS and CSRF attacks.

  2. I think you must use the PKCE flow, because the client_ids are not the same. But I've never tested this, maybe you could try it out and comment here if you could make it work.

  3. No, we are having a separate route in which we're refreshing out token. My refresh_tokens expiry time is 10 days, but the user refresh_token is newing up every time new access_token is requested. So the user will have to go through the whole PKCE flow if they weren't active for 10 days straight.

Collapse
efillman profile image
Evan Fillman

So I have been reading RFCs today...because quarantine and I am actually attempting to understand what "the standard" is. I think the best documents are currently OAuth 2.0 for Browser-Based Apps and Proof Key for Code Exchange by OAuth Public Clients. The interesting part (section 6) in the best practices document (1st link) there is great discussion about choosing an OAuth2 solution based off the architecture you're working with, which makes a lot of sense. Unlocking this critical aspect essentially answered some of my questions.

The most important being to realize that if your architecture and requirements don't "need" redirects then there is probably a more secure way to accomplish the task without PKCE, essentially that PKCE would be a less secure option if one can get it done without needing redirects. Maybe that was super obvious but I was just assuming that PKCE is the new standard so everyone needs to switch to it which was causing me to try to jam all the concepts together.

1) Regarding Storing Tokens. I think you were very close (if not 100%) to what is suggested in the best practices with your previous post Secure authentication in Nuxt SPA with Laravel as back-end

2) Regarding User Registration. Based off the above logic an application that also worries about initial user registration probably is in a position architecturally to not need PKCE so it is out of bounds.

3) Regarding Refresh Tokens and PKCE. If one is truly working with a public client (that you really need PKCE) it is the case that issuing refresh tokens is indeed more risk than the access token. In my opinion the separate time expiry you suggested would not increase security, but there are some interesting suggestions for refresh tokens with PKCE in the best practices, including not using them, or using a decreasing expiration refresh token that still needs to be reacquired every 24 hours.

Thread Thread
stefant123 profile image
StefanT123 Author

Yes, the part with decreasing the refresh_token expiration time is very interesting, and I might try that.

Collapse
franfoukal profile image
franfoukal

Hi! Very helpful tutorial!
The only inconvenient that I have is that the session starts in the login, and when I tried to revoke the token (the route is under auth:api middleware) the session is not over and when i triy to login again, it skips the login form and jumps to the authorization prompt or just to callback page. I tried to create a route in the web middleware which kills the session but always stores the cookie 'laravel_session' and 'XSRF-TOKEN' and can't delete them.
Did this happen to you? Thanks in advance.

Collapse
stefant123 profile image
StefanT123 Author

How do you revoke the token?

Collapse
franfoukal profile image
franfoukal

Yes, not only using

Auth::user()->token()->revoke(); 

but also like indicates the docs:
Laravel - revoking tokens

This is not working for me because uses the laravel_session cookie data to persist login and return a new access_token without ask for credentials again, redirecting to the callback page directly.
Laravel destroy the session after a while or when the browser is closed but it's a problem when I want to change user to login because I have to wait or close everything.

Maybe the problem is the session based login, but there is no much info about it.

I would like to know if it has happened to you and if anyone could solve it.
Sorry about my english, is not my mother tongue. And thanks again!

Thread Thread
stefant123 profile image
StefanT123 Author

Maybe you should try to revoke the token and clear the users session, maybe that will do it. But I don't know if this is the right way to logout some user...

Thread Thread
franfoukal profile image
franfoukal

After several trials, I came up with a solution (not an elegant one I guess) that works.
It's a mix from logout from the API guard (api.php routes with auth:api middleware), revoking the token:

public function logoutAPI(){

        Auth::user()->token()->revoke();
        $tokenId = Auth::user()->token()->id;

        $tokenRepository = app('Laravel\Passport\TokenRepository');
        $refreshTokenRepository = app('Laravel\Passport\RefreshTokenRepository');
        $tokenRepository->revokeAccessToken($tokenId);
        $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

        return response()->json([
            'msg' => 'You have been succesfully logged out'
        ],200);
    }

And in the web guard (web.php routes), kill the session:

    public function logoutSession(Request $request){
        Auth::guard('web')->logout();
        Session::flush();
        //the frontend sends a logout_uri query string to redirect
        return response()->redirectTo($request->query('logout_uri'));
    }

In the frontend I send an axios post request to the logoutAPI route and then call the logoutSession route. Here is the code using the @nuxtjs/auth-next module.

        logout(){
            this.$axios.get('/api/logout')
            .then(response => {
                this.$auth.reset(); //deletes tokens in nuxt app
                this.$auth.logout(); //redirects to logoutSession 
                this.$axios.setHeader('Authorization', null); 
            })
            .catch(error => console.log(error.response));
        }

This way, every time I logout from the app and login again, the credentials are required and doesn't persists.

Thanks for your replies, I hope this helps someone!

Collapse
thorbn profile image
Thor

Sorry to disturb again.

I'm still having problems with, how to get the users info (like name) to show on page (like in the footer). If reloading the webapp I still have the access token but the name I set with this.$auth.setUser(resp.name) is gone. Do you have and example on how to get the webapp to save the user name on reload? I can do it with a cookie, but think that its not safe.

Collapse
stefant123 profile image
StefanT123 Author

You can use the cookies to persist the state or use some package that does that. Don't worry about security, because the user name is not something that should not be publicly visible.

Collapse
thorbn profile image
Thor

Can other users not just change the userId in the local cookie to something else and then get other users info from the api? In my case todos

Thread Thread
stefant123 profile image
StefanT123 Author

Well, if you've set up your back-end properly, they won't be able to do that

Collapse
thorbn profile image
Thor

Hi again;

Now im at: logout

I can logout on the frontend but the backend (laravel is still logget ind) so if i click login on the frontend the user will get logget ind again.

How to logout on the laravel backend so the user have to write the email and password again to login?

This dont work: the user is not logget out in the backend

Api route:
Route::middleware(['auth:api'])->group(function () {
Route::post('/logout', 'Auth\AuthWepAppController@logoutApi');
});

AuthWepAppController:
public function logoutApi (Request $request) {

   $request->session()->flush();
Collapse
thorbn profile image
Thor

Hi, one more question

I'm new to passport so what about all the auth tokens in the db? I can see many of them, Are they delete automatic by Laravel passport?

SELECT * FROM oauth_access_tokens

5fea03842964060c10590e072ce5571478c4270705caf5c447...
22
1
authToken
[]
0
2020-05-14 11:41:19
2020-05-14 11:41:19
2021-05-14 11:41:19
874657bfbafafa674aeac5f5c01b967fa7b95dff71ef4c2c1e...
22
1
.................

Collapse
stefant123 profile image
StefanT123 Author

No, they are not deleted automatically

Collapse
thorbn profile image
Thor

Hi again Stefan,

With the code you have written, the user logges out when refreshing the page, howto handle so the user don't logges out on page refresh?

The attached screendump is what I have after page refresh

dev-to-uploads.s3.amazonaws.com/i/...

Collapse
stefant123 profile image
StefanT123 Author

On page reload, check for the token in cookies, and if there is a token in the cookies, just set it in axios

Collapse
thorbn profile image
Thor

I did:

In authIt.js

const access_token = cookies.get('x-access-token');
if
no token redirect to /login

else

store.$axios.$get(process.env.LARAVEL_ENDPOINT+'/api/user')
.then(resp => {
store.$auth.setUser(resp)
......

Collapse
thorbn profile image
Thor

Hi again,

Question about logout:

I can do this in the frontend: this.$auth.logout();

But how to logout the laravel backend?

I have tried:

Nuxt:
logout() {
this.$axios.$post(process.env.LARAVEL_ENDPOINT+'/api/logout')
.then(response => {.....

Laravel backend:

API.php *******************

Route::middleware('auth:api')->group(function () {
Route::post('logout', 'Auth\AuthWepAppController@logoutApi')->name('logout');
});

AuthWepAppController *******************

public function logoutApi (Request $request) {

    $token = $request->user()->token();
    $token->revoke();

    $response = 'You have been succesfully logged out!';
    return response($response, 200);
    }

It gives me this error:
message": "Call to undefined method Illuminate\Support\Facades\Request::user()",

What am I missing? thanks

Collapse
thorbn profile image
Thor

Hi again Stefan

I'm trying again to figure out how to logout from the backend. How do you logout on the backend?

This dont work: the user is not logget out in the backend


Api route:
Route::middleware(['auth:api'])->group(function () {
Route::post('/logout', 'Auth\AuthWepAppController@logoutApi');
});

AuthWepAppController:
public function logoutApi (Request $request) {

$request->session()->flush();
}


Collapse
stefant123 profile image
StefanT123 Author

Just get the users token and delete it.
Auth::user()->token()->delete

Collapse
thorbn profile image
Thor

Great tutorial. I'm new to Passport and PKCE.

What is the "best practice" way on checking if a user is logged in frontend? so instead there will be a logout button and not a login button.

Collapse
stefant123 profile image
StefanT123 Author

You should check if the token exists in the cookies or in the localStorage (wherever you have stored it)

Collapse
thorbn profile image
Thor

I have not stored it yet. The way you store it here is that the best way? dev.to/stefant123/secure-authentic...

Thread Thread
stefant123 profile image
StefanT123 Author

I think yes, or I still haven't found any better way to do it.