LICENSE.md 0000644 00000002063 15025020026 0006141 0 ustar 00 The MIT License (MIT) Copyright (c) Taylor Otwell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. composer.json 0000644 00000002612 15025020026 0007257 0 ustar 00 { "name": "laravel/sanctum", "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", "keywords": ["laravel", "sanctum", "auth"], "license": "MIT", "support": { "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, "authors": [ { "name": "Taylor Otwell", "email": "taylor@laravel.com" } ], "require": { "php": "^8.0.2", "ext-json": "*", "illuminate/console": "^9.21|^10.0", "illuminate/contracts": "^9.21|^10.0", "illuminate/database": "^9.21|^10.0", "illuminate/support": "^9.21|^10.0" }, "require-dev": { "mockery/mockery": "^1.0", "orchestra/testbench": "^7.0|^8.0", "phpunit/phpunit": "^9.3" }, "autoload": { "psr-4": { "Laravel\\Sanctum\\": "src/" } }, "autoload-dev": { "psr-4": { "Laravel\\Sanctum\\Tests\\": "tests/" } }, "extra": { "branch-alias": { "dev-master": "3.x-dev" }, "laravel": { "providers": [ "Laravel\\Sanctum\\SanctumServiceProvider" ] } }, "config": { "sort-packages": true }, "minimum-stability": "dev", "prefer-stable": true } database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php 0000644 00000001530 15025020026 0022466 0 ustar 00 id(); $table->morphs('tokenable'); $table->string('name'); $table->string('token', 64)->unique(); $table->text('abilities')->nullable(); $table->timestamp('last_used_at')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('personal_access_tokens'); } }; src/HasApiTokens.php 0000644 00000003735 15025020026 0010375 0 ustar 00 morphMany(Sanctum::$personalAccessTokenModel, 'tokenable'); } /** * Determine if the current API token has a given scope. * * @param string $ability * @return bool */ public function tokenCan(string $ability) { return $this->accessToken && $this->accessToken->can($ability); } /** * Create a new personal access token for the user. * * @param string $name * @param array $abilities * @param \DateTimeInterface|null $expiresAt * @return \Laravel\Sanctum\NewAccessToken */ public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null) { $token = $this->tokens()->create([ 'name' => $name, 'token' => hash('sha256', $plainTextToken = Str::random(40)), 'abilities' => $abilities, 'expires_at' => $expiresAt, ]); return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken); } /** * Get the access token currently associated with the user. * * @return \Laravel\Sanctum\Contracts\HasAbilities */ public function currentAccessToken() { return $this->accessToken; } /** * Set the current access token for the user. * * @param \Laravel\Sanctum\Contracts\HasAbilities $accessToken * @return $this */ public function withAccessToken($accessToken) { $this->accessToken = $accessToken; return $this; } } src/Console/Commands/PruneExpired.php 0000644 00000002760 15025020026 0013616 0 ustar 00 option('hours'); $this->components->task( 'Pruning tokens with expired expires_at timestamps', fn () => $model::where('expires_at', '<', now()->subHours($hours))->delete() ); if ($expiration = config('sanctum.expiration')) { $this->components->task( 'Pruning tokens with expired expiration value based on configuration file', fn () => $model::where('created_at', '<', now()->subMinutes($expiration + ($hours * 60)))->delete() ); } else { $this->components->warn('Expiration value not specified in configuration file.'); } $this->components->info("Tokens expired for more than [$hours hours] pruned successfully."); return 0; } } src/TransientToken.php 0000644 00000001015 15025020026 0011001 0 ustar 00 shouldIgnoreMissing(false); if (in_array('*', $abilities)) { $token->shouldReceive('can')->withAnyArgs()->andReturn(true); } else { foreach ($abilities as $ability) { $token->shouldReceive('can')->with($ability)->andReturn(true); } } $user->withAccessToken($token); if (isset($user->wasRecentlyCreated) && $user->wasRecentlyCreated) { $user->wasRecentlyCreated = false; } app('auth')->guard($guard)->setUser($user); app('auth')->shouldUse($guard); return $user; } /** * Set the personal access token model name. * * @param string $model * @return void */ public static function usePersonalAccessTokenModel($model) { static::$personalAccessTokenModel = $model; } /** * Specify a callback that should be used to fetch the access token from the request. * * @param callable $callback * @return void */ public static function getAccessTokenFromRequestUsing(callable $callback) { static::$accessTokenRetrievalCallback = $callback; } /** * Specify a callback that should be used to authenticate access tokens. * * @param callable $callback * @return void */ public static function authenticateAccessTokensUsing(callable $callback) { static::$accessTokenAuthenticationCallback = $callback; } /** * Determine if Sanctum's migrations should be run. * * @return bool */ public static function shouldRunMigrations() { return static::$runsMigrations; } /** * Configure Sanctum to not register its migrations. * * @return static */ public static function ignoreMigrations() { static::$runsMigrations = false; return new static; } /** * Get the token model class name. * * @return string */ public static function personalAccessTokenModel() { return static::$personalAccessTokenModel; } } src/Events/TokenAuthenticated.php 0000644 00000000672 15025020026 0013070 0 ustar 00 token = $token; } } src/Guard.php 0000644 00000012216 15025020026 0007100 0 ustar 00 auth = $auth; $this->expiration = $expiration; $this->provider = $provider; } /** * Retrieve the authenticated user for the incoming request. * * @param \Illuminate\Http\Request $request * @return mixed */ public function __invoke(Request $request) { foreach (Arr::wrap(config('sanctum.guard', 'web')) as $guard) { if ($user = $this->auth->guard($guard)->user()) { return $this->supportsTokens($user) ? $user->withAccessToken(new TransientToken) : $user; } } if ($token = $this->getTokenFromRequest($request)) { $model = Sanctum::$personalAccessTokenModel; $accessToken = $model::findToken($token); if (! $this->isValidAccessToken($accessToken) || ! $this->supportsTokens($accessToken->tokenable)) { return; } $tokenable = $accessToken->tokenable->withAccessToken( $accessToken ); event(new TokenAuthenticated($accessToken)); if (method_exists($accessToken->getConnection(), 'hasModifiedRecords') && method_exists($accessToken->getConnection(), 'setRecordModificationState')) { tap($accessToken->getConnection()->hasModifiedRecords(), function ($hasModifiedRecords) use ($accessToken) { $accessToken->forceFill(['last_used_at' => now()])->save(); $accessToken->getConnection()->setRecordModificationState($hasModifiedRecords); }); } else { $accessToken->forceFill(['last_used_at' => now()])->save(); } return $tokenable; } } /** * Determine if the tokenable model supports API tokens. * * @param mixed $tokenable * @return bool */ protected function supportsTokens($tokenable = null) { return $tokenable && in_array(HasApiTokens::class, class_uses_recursive( get_class($tokenable) )); } /** * Get the token from the request. * * @param \Illuminate\Http\Request $request * @return string|null */ protected function getTokenFromRequest(Request $request) { if (is_callable(Sanctum::$accessTokenRetrievalCallback)) { return (string) (Sanctum::$accessTokenRetrievalCallback)($request); } $token = $request->bearerToken(); return $this->isValidBearerToken($token) ? $token : null; } /** * Determine if the bearer token is in the correct format. * * @param string|null $token * @return bool */ protected function isValidBearerToken(string $token = null) { if (! is_null($token) && str_contains($token, '|')) { $model = new Sanctum::$personalAccessTokenModel; if ($model->getKeyType() === 'int') { [$id, $token] = explode('|', $token, 2); return ctype_digit($id) && ! empty($token); } } return ! empty($token); } /** * Determine if the provided access token is valid. * * @param mixed $accessToken * @return bool */ protected function isValidAccessToken($accessToken): bool { if (! $accessToken) { return false; } $isValid = (! $this->expiration || $accessToken->created_at->gt(now()->subMinutes($this->expiration))) && (! $accessToken->expires_at || ! $accessToken->expires_at->isPast()) && $this->hasValidProvider($accessToken->tokenable); if (is_callable(Sanctum::$accessTokenAuthenticationCallback)) { $isValid = (bool) (Sanctum::$accessTokenAuthenticationCallback)($accessToken, $isValid); } return $isValid; } /** * Determine if the tokenable model matches the provider's model type. * * @param \Illuminate\Database\Eloquent\Model $tokenable * @return bool */ protected function hasValidProvider($tokenable) { if (is_null($this->provider)) { return true; } $model = config("auth.providers.{$this->provider}.model"); return $tokenable instanceof $model; } } src/NewAccessToken.php 0000644 00000002427 15025020026 0010715 0 ustar 00 accessToken = $accessToken; $this->plainTextToken = $plainTextToken; } /** * Get the instance as an array. * * @return array */ public function toArray() { return [ 'accessToken' => $this->accessToken, 'plainTextToken' => $this->plainTextToken, ]; } /** * Convert the object to its JSON representation. * * @param int $options * @return string */ public function toJson($options = 0) { return json_encode($this->toArray(), $options); } } src/PersonalAccessToken.php 0000644 00000004016 15025020026 0011743 0 ustar 00 'json', 'last_used_at' => 'datetime', 'expires_at' => 'datetime', ]; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'token', 'abilities', 'expires_at', ]; /** * The attributes that should be hidden for serialization. * * @var array */ protected $hidden = [ 'token', ]; /** * Get the tokenable model that the access token belongs to. * * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ public function tokenable() { return $this->morphTo('tokenable'); } /** * Find the token instance matching the given token. * * @param string $token * @return static|null */ public static function findToken($token) { if (strpos($token, '|') === false) { return static::where('token', hash('sha256', $token))->first(); } [$id, $token] = explode('|', $token, 2); if ($instance = static::find($id)) { return hash_equals($instance->token, hash('sha256', $token)) ? $instance : null; } } /** * Determine if the token has a given ability. * * @param string $ability * @return bool */ public function can($ability) { return in_array('*', $this->abilities) || array_key_exists($ability, array_flip($this->abilities)); } /** * Determine if the token is missing a given ability. * * @param string $ability * @return bool */ public function cant($ability) { return ! $this->can($ability); } } src/Http/Middleware/CheckForAnyAbility.php 0000644 00000001654 15025020026 0014510 0 ustar 00 user() || ! $request->user()->currentAccessToken()) { throw new AuthenticationException; } foreach ($abilities as $ability) { if ($request->user()->tokenCan($ability)) { return $next($request); } } throw new MissingAbilityException($abilities); } } src/Http/Middleware/CheckForAnyScope.php 0000644 00000001530 15025020026 0014155 0 ustar 00 handle($request, $next, ...$scopes); } catch (\Laravel\Sanctum\Exceptions\MissingAbilityException $e) { throw new MissingScopeException($e->abilities()); } } } src/Http/Middleware/CheckAbilities.php 0000644 00000001650 15025020026 0013675 0 ustar 00 user() || ! $request->user()->currentAccessToken()) { throw new AuthenticationException; } foreach ($abilities as $ability) { if (! $request->user()->tokenCan($ability)) { throw new MissingAbilityException($ability); } } return $next($request); } } src/Http/Middleware/CheckScopes.php 0000644 00000001513 15025020026 0013222 0 ustar 00 handle($request, $next, ...$scopes); } catch (\Laravel\Sanctum\Exceptions\MissingAbilityException $e) { throw new MissingScopeException($e->abilities()); } } } src/Http/Middleware/EnsureFrontendRequestsAreStateful.php 0000644 00000004345 15025020026 0017673 0 ustar 00 configureSecureCookieSessions(); return (new Pipeline(app()))->send($request)->through(static::fromFrontend($request) ? [ function ($request, $next) { $request->attributes->set('sanctum', true); return $next($request); }, config('sanctum.middleware.encrypt_cookies', \Illuminate\Cookie\Middleware\EncryptCookies::class), \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, config('sanctum.middleware.verify_csrf_token', \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class), ] : [])->then(function ($request) use ($next) { return $next($request); }); } /** * Configure secure cookie sessions. * * @return void */ protected function configureSecureCookieSessions() { config([ 'session.http_only' => true, 'session.same_site' => 'lax', ]); } /** * Determine if the given request is from the first-party application frontend. * * @param \Illuminate\Http\Request $request * @return bool */ public static function fromFrontend($request) { $domain = $request->headers->get('referer') ?: $request->headers->get('origin'); if (is_null($domain)) { return false; } $domain = Str::replaceFirst('https://', '', $domain); $domain = Str::replaceFirst('http://', '', $domain); $domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/"; $stateful = array_filter(config('sanctum.stateful', [])); return Str::is(Collection::make($stateful)->map(function ($uri) { return trim($uri).'/*'; })->all(), $domain); } } src/Http/Controllers/CsrfCookieController.php 0000644 00000001103 15025020026 0015347 0 ustar 00 expectsJson()) { return new JsonResponse(null, 204); } return new Response('', 204); } } src/Exceptions/MissingScopeException.php 0000644 00000001446 15025020026 0014444 0 ustar 00 scopes = Arr::wrap($scopes); } /** * Get the scopes that the user did not have. * * @return array */ public function scopes() { return $this->scopes; } } src/Exceptions/MissingAbilityException.php 0000644 00000001502 15025020026 0014761 0 ustar 00 abilities = Arr::wrap($abilities); } /** * Get the abilities that the user did not have. * * @return array */ public function abilities() { return $this->abilities; } } src/SanctumServiceProvider.php 0000644 00000007064 15025020026 0012511 0 ustar 00 array_merge([ 'driver' => 'sanctum', 'provider' => null, ], config('auth.guards.sanctum', [])), ]); if (! app()->configurationIsCached()) { $this->mergeConfigFrom(__DIR__.'/../config/sanctum.php', 'sanctum'); } } /** * Bootstrap any application services. * * @return void */ public function boot() { if (app()->runningInConsole()) { $this->registerMigrations(); $this->publishes([ __DIR__.'/../database/migrations' => database_path('migrations'), ], 'sanctum-migrations'); $this->publishes([ __DIR__.'/../config/sanctum.php' => config_path('sanctum.php'), ], 'sanctum-config'); $this->commands([ PruneExpired::class, ]); } $this->defineRoutes(); $this->configureGuard(); $this->configureMiddleware(); } /** * Register Sanctum's migration files. * * @return void */ protected function registerMigrations() { if (Sanctum::shouldRunMigrations()) { return $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); } } /** * Define the Sanctum routes. * * @return void */ protected function defineRoutes() { if (app()->routesAreCached() || config('sanctum.routes') === false) { return; } Route::group(['prefix' => config('sanctum.prefix', 'sanctum')], function () { Route::get( '/csrf-cookie', CsrfCookieController::class.'@show' )->middleware('web')->name('sanctum.csrf-cookie'); }); } /** * Configure the Sanctum authentication guard. * * @return void */ protected function configureGuard() { Auth::resolved(function ($auth) { $auth->extend('sanctum', function ($app, $name, array $config) use ($auth) { return tap($this->createGuard($auth, $config), function ($guard) { app()->refresh('request', $guard, 'setRequest'); }); }); }); } /** * Register the guard. * * @param \Illuminate\Contracts\Auth\Factory $auth * @param array $config * @return RequestGuard */ protected function createGuard($auth, $config) { return new RequestGuard( new Guard($auth, config('sanctum.expiration'), $config['provider']), request(), $auth->createUserProvider($config['provider'] ?? null) ); } /** * Configure the Sanctum middleware and priority. * * @return void */ protected function configureMiddleware() { $kernel = app()->make(Kernel::class); $kernel->prependToMiddlewarePriority(EnsureFrontendRequestsAreStateful::class); } } README.md 0000644 00000003020 15025020026 0006006 0 ustar 00