Image

laravel

Sanctum

How to configure Sanctum

4 min read
Last update: November 28, 2021

Setup

php artisan migrate:fresh

routes/api.php

Route::get('/login', [AuthenticatedSessionController::class, 'create'])->name('api.auth.login');
Route::post('/login', [AuthenticatedSessionController::class, 'store'])->name('api.auth.login.post');
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])->name('api.auth.logout');

Route::middleware(['auth:sanctum'])->group(function () {
    Route::get('/user', [UserController::class, 'user'])->name('api.user');
});

composer.json

  • laravel/jetstream to get fortify for login routes
  • laravel/sanctum
{
    "require": {
        "laravel/jetstream": "^2.4",
        "laravel/sanctum": "^2.11"
    }
}

app/Http/Kernel.php

class Kernel extends HttpKernel
{
    protected $middlewareGroups = [
        
        // ...

        'api' => [
            \Illuminate\Session\Middleware\StartSession::class,
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];
}

config/cors.php

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

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,
];

config/sanctum.php

return [
    'prefix' => '/api/sanctum',

    // ...
]

database/seeders/UserSeeder.php

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run()
    {
        User::create([
            'name' => 'Admin',
            'email' => 'admin@mail.com',
            'password' => Hash::make('password'),
        ]);
    }
}

Seed database: php artisan db:seed --class=UserSeeder

Front (Nuxt)

nuxt.config.js

export default {
    modules: [
        // https://go.nuxtjs.dev/axios
        '@nuxtjs/axios',
    ],
    axios: {
        baseURL: 'http://localhost:8000/api',
        credentials: true,
        headers: {
            common: {
                'X-Requested-With': 'XMLHttpRequest',
                'Access-Control-Allow-Origin': '*',
                Accept: 'application/json, text/plain, */*',
            },
        },
    },
}

login-form.vue

<script>
export default {
  name: 'LoginForm',
  data() {
    return {
      loading: false,
      form: {
        email: '',
        password: '',
      },
    }
  },
  methods: {
    async submit() {
      this.loading = true

      try {
        await this.$axios.$get('/sanctum/csrf-cookie')
        await this.$axios.$post('/login', {
          email: this.form.email,
          password: this.form.password,
        })

        this.loading = false
      } catch (error) {
        console.error(error)

        this.loading = false
      }
    },
  },
}
</script>

Errors

  • session store not set on request

.env

SANCTUM_STATEFUL_DOMAINS=localhost:3000

app/Http/Kernel.php

class Kernel extends HttpKernel
{
    protected $middlewareGroups = [
        
        // ...

        'api' => [
            \Illuminate\Session\Middleware\StartSession::class,
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];
}
  • 419 csrf token mismatch

nuxt.config.js

export default {
    modules: [
        // https://go.nuxtjs.dev/axios
        '@nuxtjs/axios',
    ],
    axios: {
        baseURL: 'http://localhost:8000/api',
        credentials: true,
        headers: {
            common: {
                'X-Requested-With': 'XMLHttpRequest',
                'Access-Control-Allow-Origin': '*',
                Accept: 'application/json, text/plain, */*',
            },
        },
    },
}

OR

const instance = axios.create({
    baseURL: 'http://localhost:8000/api',
    withCredentials: true,
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Access-Control-Allow-Origin': '*',
        Accept: 'application/json, text/plain, */*',
    },
})

await instance.get('/sanctum/csrf-cookie')
await instance.post('/login', credentials)

Old Laravel Sanctum setup

{
  "require": {
    "laravel/framework": "^8.12",
    "laravel/sanctum": "2.9"
  }
}
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=www.my-domain.com
SESSION_DOMAIN=.my-domain.com
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::middleware(['auth:sanctum'])->group(function () {
    Route::get('/private', [PrivateController::class, 'index'])->name('private.index');
});
<?php

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout', 'register', 'home', 'user', 'dashboard'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => false,

    'supports_credentials' => true,
];
<?php

return [
    // ...

    'features' => [
        Features::registration(),
        Features::resetPasswords(),
        Features::emailVerification(),
        Features::updateProfileInformation(),
        Features::updatePasswords(),
        Features::twoFactorAuthentication([
            'confirmPassword' => true,
        ]),
    ],
];
<?php

// Add below line
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

class Kernel extends HttpKernel
{

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        // ...

        'api' => [
            // Add below line
            EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];
}

SPA

Nuxt.js

{
  "dependencies": {
    "@nuxtjs/auth-next": "5.0.0-1612791489.a5d8c28",
    "@nuxtjs/axios": "^5.13.1",
  }
}
export default {
  // ...
  
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/auth-next',
  ],

  axios: {
    baseURL: process.env.API_URL,
    credentials: true,
    https: false,
    headers: {
      common: {
        'X-Requested-With': 'XMLHttpRequest',
      },
    },
  },

  proxy: {
    '/api': {
      target: `${process.env.API_URL}/api`,
      pathRewrite: { '^/api': '/' },
    },
  },

  auth: {
    strategies: {
      laravelSanctum: {
        provider: 'laravel/sanctum',
        url: process.env.API_URL,
      },
    },
    redirect: {
      login: '/sign-in',
      logout: '/sign-in',
      callback: '/',
      home: '/dashboard',
    },
    cookie: {
      options: {
        sameSite: 'lax',
      },
    },
  },
}
PORT=3000
HOST=localhost
BASE_URL=http://localhost:3000
API_URL=http://localhost:8000
PORT=3005
HOST=127.0.0.1
BASE_URL=https://www.my-domain.com
API_URL=https://www.my-domain.com
<template>
  <form @submit.prevent="submit">
    <div>
      <label for="email"> Email address </label>
      <input
        id="email"
        v-model="form.email"
        name="email"
        type="email"
        required
      />
    </div>

    <div>
      <label for="password"> Password </label>
      <input
        id="password"
        v-model="form.password"
        name="password"
        type="password"
        required
      />
    </div>

    <button type="submit">Sign in</button>
  </form>
</template>

<script>
export default {
  name: 'PageSignin',
  auth: 'guest',
  data() {
    return {
      form: {
        email: '',
        password: '',
      },
    }
  },
  methods: {
    async submit() {
      try {
        await this.$auth.loginWith('laravelSanctum', {
          data: this.form,
        })
      } catch (error) {
        console.error(error)
      }
    },
  },
}
</script>
<template>
    <div>
        <h1>Dashboard</h1>
        <button @click="logout">Sign out</button>
        <div>
            {{ private }}
        </div>
    </div>
</template>

<script>
export default {
  name: 'PageDashboard',
  middleware: 'auth',
  async asyncData({ app, query, error, params, $content, $auth, store }) {
    try {
      const [private] = await Promise.all([app.$axios.$get(`/api/private`)])

      return {
        private,
      }
    } catch (error) {
      console.error(error)
    }
  },
  methods: {
    async logout() {
      try {
        await this.$auth.logout()
      } catch (error) {
        console.error(error)
      }
    },
  },
}
</script>