{"id":190,"date":"2025-08-26T14:56:07","date_gmt":"2025-08-26T14:56:07","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=190"},"modified":"2025-08-26T14:56:10","modified_gmt":"2025-08-26T14:56:10","slug":"securing-laravel-apis-with-sanctum-complete-guide","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/securing-laravel-apis-with-sanctum-complete-guide\/","title":{"rendered":"Securing Laravel APIs with Sanctum: Complete Guide"},"content":{"rendered":"\n<p>APIs power modern applications \u2014 from single-page apps (SPAs) to mobile apps and even IoT devices. But APIs also open doors to attackers if not secured properly. That\u2019s where <strong>Laravel Sanctum<\/strong> comes in. It\u2019s a lightweight package that makes it easy to protect your Laravel APIs with tokens, cookies, and middleware.<\/p>\n\n\n\n<p>In this tutorial, you\u2019ll learn how to <strong>secure your Laravel 12 APIs with Sanctum<\/strong>. We\u2019ll go step by step: installing Sanctum, configuring it, issuing tokens, protecting routes, and using it with SPAs and mobile clients. Along the way, we\u2019ll explain key concepts so you understand not just how, but why it works.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1 &#8211; What is Laravel Sanctum?<\/strong><\/h2>\n\n\n\n<p><strong>Sanctum<\/strong> is Laravel\u2019s recommended way to authenticate APIs. It supports two main use cases:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>API tokens:<\/strong> Ideal for mobile apps or external services. Each user can create and manage personal access tokens.<\/li>\n<li><strong>SPA authentication:<\/strong> Uses Laravel\u2019s session cookies with CSRF protection to secure single-page apps built with React, Vue, etc.<\/li>\n<\/ul>\n\n\n\n<p>Unlike Passport (Laravel\u2019s OAuth2 package), Sanctum is simple, lightweight, and perfect for most apps that don\u2019t need full OAuth2 complexity.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2 &#8211; Install and Configure Sanctum<\/strong><\/h2>\n\n\n\n<p>First, install Sanctum via Composer:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">composer require laravel\/sanctum<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Publish the Sanctum configuration and migration files:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">php artisan vendor:publish --provider=<span class=\"hljs-string\">\"Laravel\\Sanctum\\SanctumServiceProvider\"<\/span>\nphp artisan migrate<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This creates a <code>personal_access_tokens<\/code> table in your database, which will store API tokens for users.<\/p>\n\n\n\n<p>Now, add Sanctum\u2019s middleware in <code>app\/Http\/Kernel.php<\/code> under the <code>api<\/code> group:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Kernel.php<\/span>\n<span class=\"hljs-keyword\">protected<\/span> $middlewareGroups = &#91;\n    <span class=\"hljs-string\">'api'<\/span> =&gt; &#91;\n        \\Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful::class,\n        <span class=\"hljs-string\">'throttle:api'<\/span>,\n        \\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,\n    ],\n];<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3 &#8211; Enable Token Abilities in the User Model<\/strong><\/h2>\n\n\n\n<p>To let users issue and manage tokens, add the <code>HasApiTokens<\/code> trait to your <code>User<\/code> model:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/User.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Foundation<\/span>\\<span class=\"hljs-title\">Auth<\/span>\\<span class=\"hljs-title\">User<\/span> <span class=\"hljs-title\">as<\/span> <span class=\"hljs-title\">Authenticatable<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Laravel<\/span>\\<span class=\"hljs-title\">Sanctum<\/span>\\<span class=\"hljs-title\">HasApiTokens<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">User<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Authenticatable<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">HasApiTokens<\/span>;\n\n    <span class=\"hljs-keyword\">protected<\/span> $fillable = &#91;<span class=\"hljs-string\">'name'<\/span>,<span class=\"hljs-string\">'email'<\/span>,<span class=\"hljs-string\">'password'<\/span>];\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now each user can create personal access tokens with abilities (like <code>create-posts<\/code>, <code>delete-posts<\/code>).<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4 &#8211; Issue and Use API Tokens<\/strong><\/h2>\n\n\n\n<p>Let\u2019s create an endpoint where a user can log in and receive a token:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/api.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Request<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Hash<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">User<\/span>;\n\nRoute::post(<span class=\"hljs-string\">'\/login'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">(Request $request)<\/span> <\/span>{\n    $request-&gt;validate(&#91;\n        <span class=\"hljs-string\">'email'<\/span> =&gt; <span class=\"hljs-string\">'required|email'<\/span>,\n        <span class=\"hljs-string\">'password'<\/span> =&gt; <span class=\"hljs-string\">'required'<\/span>,\n    ]);\n\n    $user = User::where(<span class=\"hljs-string\">'email'<\/span>,$request-&gt;email)-&gt;first();\n\n    <span class=\"hljs-keyword\">if<\/span> (! $user || ! Hash::check($request-&gt;password, $user-&gt;password)) {\n        <span class=\"hljs-keyword\">return<\/span> response()-&gt;json(&#91;<span class=\"hljs-string\">'message'<\/span> =&gt; <span class=\"hljs-string\">'Invalid credentials'<\/span>], <span class=\"hljs-number\">401<\/span>);\n    }\n\n    $token = $user-&gt;createToken(<span class=\"hljs-string\">'api-token'<\/span>,&#91;<span class=\"hljs-string\">'create-posts'<\/span>,<span class=\"hljs-string\">'delete-posts'<\/span>])-&gt;plainTextToken;\n\n    <span class=\"hljs-keyword\">return<\/span> response()-&gt;json(&#91;<span class=\"hljs-string\">'token'<\/span> =&gt; $token]);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now, the user can authenticate by including the token in the <code>Authorization<\/code> header:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">curl -H <span class=\"hljs-string\">\"Authorization: Bearer {TOKEN}\"<\/span> http:\/\/localhost:8000\/api\/user<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Tokens can also be scoped. For example, you could give one token <code>create-posts<\/code> and another <code>read-posts<\/code>, letting you restrict what each client can do.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5 &#8211; Protect API Routes with Sanctum Middleware<\/strong><\/h2>\n\n\n\n<p>To secure your API endpoints, apply the <code>auth:sanctum<\/code> middleware:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/api.php<\/span>\nRoute::middleware(<span class=\"hljs-string\">'auth:sanctum'<\/span>)-&gt;get(<span class=\"hljs-string\">'\/user'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">(Request $request)<\/span> <\/span>{\n    <span class=\"hljs-keyword\">return<\/span> $request-&gt;user();\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now, only requests with a valid Sanctum token will succeed. Invalid or missing tokens return <code>401 Unauthorized<\/code>.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6 &#8211; Sanctum with SPAs<\/strong><\/h2>\n\n\n\n<p>If you\u2019re building a Vue, React, or Angular SPA that talks to your Laravel backend, Sanctum provides cookie-based authentication. Here\u2019s how it works:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The SPA makes a login request to <code>\/login<\/code> with credentials.<\/li>\n<li>Laravel responds with a session cookie.<\/li>\n<li>Subsequent requests include this cookie, protected by CSRF tokens.<\/li>\n<\/ul>\n\n\n\n<p>To enable this, add your SPA\u2019s domain to <code>SANCTUM_STATEFUL_DOMAINS<\/code> in <code>.env<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\"><span class=\"hljs-comment\"># .env<\/span>\nSANCTUM_STATEFUL_DOMAINS=localhost:3000,myapp.com<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This lets Sanctum recognize stateful requests from your frontend, making cookie-based auth work seamlessly.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7 &#8211; Common Errors &amp; Fixes<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>401 Unauthorized:<\/strong> Make sure the token is included in the <code>Authorization<\/code> header or the SPA is listed in <code>SANCTUM_STATEFUL_DOMAINS<\/code>.<\/li>\n<li><strong>CSRF mismatch:<\/strong> When using SPA auth, ensure you include the <code>X-XSRF-TOKEN<\/code> header with requests.<\/li>\n<li><strong>Token not found:<\/strong> Run <code>php artisan migrate<\/code> to create the <code>personal_access_tokens<\/code> table.<\/li>\n<\/ul>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p>You\u2019ve learned how to <strong>secure Laravel APIs with Sanctum<\/strong>. We installed and configured Sanctum, issued API tokens, protected routes, and even integrated it with SPAs. This lightweight solution is perfect for most apps, providing strong security without the complexity of OAuth2.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/how-to-add-jwt-authentication-to-laravel-apis\">How to Add JWT Authentication to Laravel APIs<\/a> \u2014 explore an alternative token-based approach.<\/li>\n<li><a href=\"\/blog\/building-a-mobile-app-backend-with-laravel-12-api\">Building a Mobile App Backend with Laravel 12 API<\/a> \u2014 use Sanctum in a mobile environment.<\/li>\n<li><a href=\"\/blog\/how-to-build-a-rest-api-with-laravel-12-and-sanctum\">How to Build a REST API with Laravel 12 &amp; Sanctum<\/a> \u2014 complete implementation guide.<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>APIs power modern applications \u2014 from single-page apps (SPAs) to mobile apps and even IoT devices. But APIs also open doors to attackers if not secured properly. That\u2019s where Laravel Sanctum comes in. It\u2019s a lightweight package that makes it easy to protect your Laravel APIs with tokens, cookies, and middleware. In this tutorial, you\u2019ll [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":194,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[25,12],"class_list":["post-190","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-api","tag-authentication"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/190","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=190"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/190\/revisions"}],"predecessor-version":[{"id":193,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/190\/revisions\/193"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/194"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=190"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}