DEV Community

StefanT123
StefanT123

Posted on

PKCE authenticaton for Nuxt SPA with Laravel as backend

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 my day to day workflow, but I will try to make it as generic as possible so that it can be implemented in other frameworks or even in vanilla javascript.

The Proof Key for Code Exchange extension is a technique for public clients to mitigate the threat of having the authorization code intercepted. The technique involves the client first creating a secret, and then using that secret again when exchanging the authorization code for an access token. This way if the code is intercepted, it will not be useful since the token request relies on the initial secret.

The basic workflow of the PKCE is this:

  1. User requests to login
  2. The SPA makes a random string for state and for code_verifier, then it hashes the code_verifier (we will use SHA256 as hashing algorithm), and it converts it to base64 url safe string, that's our code_challenge. Then it saves the state and code_verifier.
  3. Make a GET request to the backend with the query parameters needed: client_id, redirect_uri, response_type, scope, state, code_challenge and code_challenge_method (there can be other required paramateres)
  4. The user is redirected to the backend login page
  5. The user submits it's credentials
  6. The backend validates the submited credentials and authenticates the user
  7. The backend then proceeds to the intended url from step 3
  8. It returns a response containing code and state
  9. SPA then checks if the returned state is equal as the state that was saved when we made the initial request (in step 2)
  10. If it is the same, the SPA makes another request with query parameters grant_type, client_id, redirect_uri, code_verifier(that we saved in step 2) and code(that was returned by the backend) to get the token

For those who are lazy and don't want to read yet another post. Here are the links for the github repositories:

Table of contents

Backend

I will assume that you already have Laravel application set up, so I will go directly to the important parts of this post.

Setting Laravel Passport

We will use Laravel Passport which provides a full OAuth2 server implementation for your Laravel application. Specifically we will use the Authorization Code Grant with PKCE. As stated in the passport documentation

The Authorization Code grant with "Proof Key for Code Exchange" (PKCE) is a secure way to authenticate single page applications or native applications to access your API. This grant should be used when you can't guarantee that the client secret will be stored confidentially or in order to mitigate the threat of having the authorization code intercepted by an attacker. A combination of a "code verifier" and a "code challenge" replaces the client secret when exchanging the authorization code for an access token.

We are going to require the passport through composer
composer require laravel/passport

Run the migrations
php artisan migrate

And install passport
php artisan passport:install

Next we should add HasApiTokens trait to the User model

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    // [code]
}
Enter fullscreen mode Exit fullscreen mode

Register the Passport routes that we need within the boot method of AuthServiceProvider, and set the expiration time of the tokens

// [code]

use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    // [code]

    public function boot()
    {
        $this->registerPolicies();

        Passport::routes(function ($router) {
            $router->forAuthorization();
            $router->forAccessTokens();
            $router->forTransientTokens();
        });
        Passport::tokensExpireIn(now()->addMinutes(5));
        Passport::refreshTokensExpireIn(now()->addDays(10));
    }
}
Enter fullscreen mode Exit fullscreen mode

Set the api driver to passport in config/auth.php

// [code]

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

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
        'hash' => false,
    ],
],

// [code]
Enter fullscreen mode Exit fullscreen mode

And the last step is to create PKCE client
php artisan passport:client --public

You are then going to be prompted some questions, here are my answers:
Which user ID should the client be assigned to? -> 1
What should we name the client? -> pkce
Where should we redirect the request after authorization? -> http://localhost:3000/auth (your SPA domain)

Setting CORS

For laravel version < 7

Manually install fruitcake/laravel-cors and follow along, or you can create your own CORS middleware.

For laravel version > 7

Change your config/cors.php, so that you add the oauth/token in your paths, and your SPA origin in allowed_origins. My config looks like this

return [
    'paths' => ['api/*', 'oauth/token'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['http://localhost:3000'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => false,

];
Enter fullscreen mode Exit fullscreen mode

Creating the API

Create the routes in routes/web.php, now this is important, the routes MUST be placed in routes/web, all the other routes can be in routes/api, but the login route must be in routes/web, because we will need session.

Route::view('login', 'login');
Route::post('login', 'AuthController@login')->name('login');
Enter fullscreen mode Exit fullscreen mode

Now, create the login view and the AuthController.

In the resources/views create new login.blade.php file and in there we will put some basic form. I won't apply any style to it.

<form method="post" action="{{ route('login') }}">
    @csrf

    <label for="email">Email:</label>
    <input type="text" name="email">

    <label for="password">Password:</label>
    <input type="password" name="password">
    <button>Login</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Make AuthController and create login method in there

// [code]

public function login(Request $request)
{
    if (auth()->guard()->attempt($request->only('email', 'password'))) {
        return redirect()->intended();
    }

    throw new \Exception('There was some error while trying to log you in');
}
Enter fullscreen mode Exit fullscreen mode

In this method we attempt to login the user with the credentials he provided, if the login is successfull we are redirecting them to the intended url, which will be the oauth/authorize with all the query parameters, if not, it will throw an exception.

Ok, that was it for the backend, now let's make the SPA.

Frontend

Create new nuxt application and select the tools you want to use, I will just use the axios module
npx create-nuxt-app <name-of-your-app>

Then we are going to need the crypto package for encryption
npm install crypto-js

Now replace all the code in pages/index.vue with this

<template>
  <div class="container">
    <button @click.prevent="openLoginWindow">Login</button>
  </div>
</template>

<script>

import crypto from 'crypto-js';

export default {
  data() {
    return {
      email: '',
      password: '',
      state: '',
      challenge: '',
    }
  },

  computed: {
    loginUrl() {
      return 'http://your-url/oauth/authorize?client_id=1&redirect_uri=http://localhost:3000/auth&response_type=code&scope=*&state=' + this.state + '&code_challenge=' + this.challenge + '&code_challenge_method=S256'
    }
  },

  mounted() {
    window.addEventListener('message', (e) => {
      if (e.origin !== 'http://localhost:3000' || ! Object.keys(e.data).includes('access_token')) {
        return;
      }

      const {token_type, expires_in, access_token, refresh_token} = e.data;
      this.$axios.setToken(access_token, token_type);

      this.$axios.$get('http://passport-pkce.web/api/user')
        .then(resp => {
          console.log(resp);
        })
    });

    this.state = this.createRandomString(40);
    const verifier = this.createRandomString(128);

    this.challenge = this.base64Url(crypto.SHA256(verifier));
    window.localStorage.setItem('state', this.state);
    window.localStorage.setItem('verifier', verifier);
  },

  methods: {
    openLoginWindow() {
      window.open(this.loginUrl, 'popup', 'width=700,height=700');
    },

    createRandomString(num) {
      return [...Array(num)].map(() => Math.random().toString(36)[2]).join('')
    },

    base64Url(string) {
      return string.toString(crypto.enc.Base64)
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Let me explain what's going on in here

  • Creating the template, nothing fancy going on in here, we are creating a button and attaching onClick event that will trigger some function.
  • In the mounted event, we are binding an event listener to the window that we are going to use later, we are setting state to be some random 40 characters string, we are creating verifier that will be some random 128 character string, and then we are setting the challenge. The challenge is SHA256 encrypted verifier string converted to base64 string. And we are setting the state and the verifier in the localStorage.
  • Then we have some methods that we've defined.

Now the flow look like this

  1. User clicks on the login button
  2. On click it triggers a openLoginWindow function, which opens new popup window for the provided url
    • this.loginUrl is a computed property that holds the url on which we want to authorize our app. It consist of base url (http://your-url/), the route for the authorization (oauth/authorize - this is the route that passport provides for us) and query parameters that we need to pass (you can look for them in the passports documentation): client_id, redirect_uri, response_type, scope, state, code_challenge and code_challenge_method.
  3. The popup opens, and since we are not logged in and the oauth/authorize route is protected by auth middleware, we are redirected to the login page, but out intended url is saved in session.
  4. After we submit our credentials and we are successfully logged in, we are the redirected to out intended url (which is the oauth/authorize with all the query parameters).
  5. And if the query parameters are good, we are redirected to the redirect_url that we specified (in my case http://localhost:3000/auth), with state and code in the response.
  6. On the auth page, that we are going to create, we need to check if the state returned from Laravel is the same as the state that we've saved in the localStorage, if it is we are going to make a post request to http://your-url/oauth/token with query parameters: grant_type, client_id, redirect_uri, code_verifier (this is the verifier that we stored in the localStorage) and code (that was returned by laravel).
  7. If everything is ok, we're going to emmit an event (we're listening for that event in our index page) with the response provided by laraavel, in that response is our token.
  8. The event listener function is called and we are setting the token on our axios instance.

Let's make our auth page so that everything becomes more clear. In pages create new page auth.vue and put this inside

<template>
  <h1>Logging in...</h1>
</template>

<script>
  export default {
    mounted() {
      const urlParams = new URLSearchParams(window.location.search);
      const code = urlParams.get('code');
      const state = urlParams.get('state');

      if (code && state) {
        if (state === window.localStorage.getItem('state')) {
          let params = {
            grant_type: 'authorization_code',
            client_id: 1,
            redirect_uri: 'http://localhost:3000/auth',
            code_verifier: window.localStorage.getItem('verifier'),
            code
          }

          this.$axios.$post('http://pkce-back.web/oauth/token', params)
            .then(resp => {
              window.opener.postMessage(resp);
              localStorage.removeItem('state');
              localStorage.removeItem('verifier');
              window.close();
            })
            .catch(e => {
              console.dir(e);
            });
        }
      }
    },
  }
</script>

Enter fullscreen mode Exit fullscreen mode

Everything in here is explained in the 6th and 7th step. But once again, we are getting the state and code from the url, we are checking if the state from the url and the state we've stored in the localStorage are the same, if they are, make a post request to oauth/token with the required parameters and on success, emit an event and pass the laravel response which contains the token.

That's it, that's all you have to do, of course this is a basic example, your access_token should be short-lived and it should be stored in the cookies, and your refresh_token should be long-lived and it should be set in httponly cookie in order to secure your application. This was relatively short post to cover all of that, but if you want to know more, you can look at my other post Secure authentication in Nuxt SPA with Laravel as back-end, where I cover these things.

If you have any questions or suggestions, please comment below.

Top comments (67)

Collapse
 
thorbn profile image
Thor • Edited

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

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

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

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

Thread Thread
 
thorbn profile image
Thor • Edited

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

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

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

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 • Edited

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
 
magnaibayarmn profile image
Г. Магнайбаяр

Thank you man! You saved my night. The league oauth2 server used hash() function as binary output. But most of JS examples can not produce as I expected. I was stucked at creating codeVerifier as they follows. Your solution saved me!

Collapse
 
stefant123 profile image
StefanT123

glad I could help

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

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 • Edited

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

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 • Edited

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

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

We are using Passport Grant Token

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

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 • Edited

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

Collapse
 
eserenna profile image
eserenna

i have a question. so this pkce thing that u make can only be used by user_id 1?

Collapse
 
stefant123 profile image
StefanT123

I assume that you are talking about client_id? Or am I wrong?

Collapse
 
eserenna profile image
eserenna

Which user ID should the client be assigned to? -> 1

Thread Thread
 
stefant123 profile image
StefanT123

you can put any id there

Thread Thread
 
eserenna profile image
eserenna

Ya. So that client_id only belong to user_id 1 am i right? So for each user need to create 1 client_id for this pkce thing?

Thread Thread
 
stefant123 profile image
StefanT123

no, you will need just one client

Thread Thread
 
eserenna profile image
eserenna

So in db there is user_id 1. That part will be ignored by laravel?

Thread Thread
 
eserenna profile image
eserenna

how do i login with pkce to user_id 2?

Thread Thread
 
stefant123 profile image
StefanT123

the login flow is the same for every user

Collapse
 
thorbn profile image
Thor • Edited

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

No, they are not deleted automatically

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 • Edited
  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 • Edited

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

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