PKCE authenticaton for Nuxt SPA with Laravel as backend
Tags: php, javascript, laravel, security
Published: 2020-04-07 09:53:16

---
title: PKCE authenticaton for Nuxt SPA with Laravel as backend
published: true
description: 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.
tags: php,javascript,laravel,security
---

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:
+ [Laravel (backend)](https://github.com/StefanT123/pkce-backend)
+ [Nuxt (frontend)](https://github.com/StefanT123/pkce-frontend)

## Table of contents
+ [Backend](#backend)
  + [Setting Laravel Passport](#passport)
  + [Setting CORS](#cors)
  + [Creating the API](#api)
+ [Frontend](#frontend)

### Backend <a name="backend"></a>

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 <a name="passport"></a>

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
```php
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]
}
```

Register the `Passport` routes that we need within the `boot` method of `AuthServiceProvider`, and set the expiration time of the tokens
```php
// [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));
    }
}
```

Set the api driver to `passport` in `config/auth.php`
```php
// [code]

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

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

// [code]
```

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 <a name="cors"></a>

##### **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
```php
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,

];
```

#### Creating the API <a name="api"></a>

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.
```php
Route::view('login', 'login');
Route::post('login', 'AuthController@login')->name('login');
```

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.
```html
<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>
```

Make `AuthController` and create `login` method in there
```php
// [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');
}
```

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 <a name="frontend"></a>

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
```vue
<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>
```

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
```vue
<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>

```

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](https://dev.to/stefant123/secure-authentication-in-nuxt-spa-with-laravel-as-back-end-19a9), where I cover these things.

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