From 95b02fafff522c59d4890913d62cb6b58d1a3bb5 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 2 Sep 2022 00:35:36 -0500 Subject: [PATCH 1/5] ApiToken docs and filter update. --- docs/guides/api-tokens.md | 88 +++++++++++++++++++ docs/guides/index.md | 3 + src/Filters/TokenAuth.php | 2 +- .../Filters/AbstractFilterTest.php | 3 + .../Filters/TokenFilterTest.php | 23 +++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 docs/guides/api-tokens.md create mode 100644 docs/guides/index.md diff --git a/docs/guides/api-tokens.md b/docs/guides/api-tokens.md new file mode 100644 index 000000000..d8e6ebf7d --- /dev/null +++ b/docs/guides/api-tokens.md @@ -0,0 +1,88 @@ +# API Tokens + +Access Tokens can be used to authenticate users for your own site, or when allowing third-party developers to access your API. When making requests using access tokens, the token should be included in the `Authorization` header as a `Bearer` token. + +To issue tokens for users, the `UserModel` must use the `CodeIgniter\Shield\Authentication\Traits\HasAccessTokens` trait. The `UserModel` that ships with Shield already uses this trait. + +```php +use CodeIgniter\Shield\Authentication\Traits\HasAccessTokens +use CodeIgniter\Model; + +class UserModel extneds Model +{ + use HasAccessTokens; +} +``` + +Tokens are issued with the `generateAccessToken()` method on the user. This returns a `CodeIgniter\Shield\Entities\AccessToken` instance. Tokens are hashed using a SHA-256 algorithm before being saved to the database. The access token returned when you generate it will include a `raw_token` field that contains the plain-text, un-hashed, token. You should display this to your user at once so they have a chance to copy it somewhere safe, as this is the only time this will be available. After this request, there is no way to get the raw token. + +The `generateAccessToken` method requires a name for the token. These are free strings and are often used to identify the user/device the token was generated from, like 'Johns MacBook Air'. + +```php +$routes->get('/access/token', static function() { + $token = auth()->user()->generateAccessToken(request()->getVar('token_name)); + + return ['token' => $token->raw_token]; +}); +``` + +You can access all of the users' tokens with the `accessTokens()` method on the user. + +```php +$tokens = $user->accessTokens(); +foreach($tokens as $token) { + // +} +``` + +## Token Permissions + +Access tokens can be given `scopes`, which are basically permission strings, for the token. This is generally not the same as the permission the user has, but is used to specify the permissions on the API itself. If not specified, the token is granted all access to all scopes. This might be enough for a smaller API. + +```php +return $user->generateAccessToken('token-name', ['users-read'])->raw_token; +``` + +NOTE: At this time, scope names should avoid using a colon (:) as this causes issues with the route filters being correctly recognized. + +When handling incoming requests you can check if the token has been granted access to the scope with the `tokenCan` method. + +```php +if ($user->tokenCan('users-read')) { + // +} +``` + +### Revoking Tokens + +Tokens can be revoked by deleting them from the database with the `revokeAccessToken($rawToken)` or `revokeAllAccessTokens()` methods. + +```php +$user->revokeAccessToken($rawToken); +$user->revokeAllAccessTokens(); +``` + +## Protecting Routes + +The first way to specify which routes are protected is to use the `tokens` controller filter. + +For example, to ensure it protects all routes under the `/api` route group, you would use the `$filters` setting on `app/Config/Filters.php`. + +```php +public $filters = [ + 'tokens' => ['before' => ['api/*']], +]; +``` + +You can also specify the filter should run on one or more routes within the routes file itself: + +```php +$routes->group('api', ['filter' => 'tokens'], function($routes) { + // +}); +$routes->get('users', 'UserController::list', ['filter' => 'tokens:users-read']); +``` + +When the filter runs, it checks the `Authorization` header for a `Bearer` value that has the raw token. It then looks hashes the raw token and looks it up in the database. Once found, it can determine the correct user, which will then be available through an `auth()->user()` call. + +Note: Currently only a single scope can be used on a route filter. If multiple scopes are passed in, only the first one is checked. diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 000000000..5ca573ea4 --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,3 @@ +# Shield Guides + +These guides provide short tutorials on setting up or using different aspects of Shield. diff --git a/src/Filters/TokenAuth.php b/src/Filters/TokenAuth.php index 7b03c268b..a877f9064 100644 --- a/src/Filters/TokenAuth.php +++ b/src/Filters/TokenAuth.php @@ -48,7 +48,7 @@ public function before(RequestInterface $request, $arguments = null) 'token' => $request->getHeaderLine(setting('Auth.authenticatorHeader')['tokens'] ?? 'Authorization'), ]); - if (! $result->isOK()) { + if (! $result->isOK() || (! empty($arguments) && $result->extraInfo()->tokenCant($arguments[0]))) { return redirect()->to('/login'); } diff --git a/tests/Authentication/Filters/AbstractFilterTest.php b/tests/Authentication/Filters/AbstractFilterTest.php index f5cd12840..d1321e1b5 100644 --- a/tests/Authentication/Filters/AbstractFilterTest.php +++ b/tests/Authentication/Filters/AbstractFilterTest.php @@ -61,6 +61,9 @@ static function ($routes): void { echo 'Open'; }); $routes->get('login', 'AuthController::login', ['as' => 'login']); + $routes->get('protected-user-route', static function (): void { + echo 'Protected'; + }, ['filter' => $this->alias . ':users-read']); Services::injectMock('routes', $routes); } diff --git a/tests/Authentication/Filters/TokenFilterTest.php b/tests/Authentication/Filters/TokenFilterTest.php index 0f001869d..a7cc8080b 100644 --- a/tests/Authentication/Filters/TokenFilterTest.php +++ b/tests/Authentication/Filters/TokenFilterTest.php @@ -63,4 +63,27 @@ public function testRecordActiveDate(): void // Last Active should be greater than 'updated_at' column $this->assertGreaterThan(auth('tokens')->user()->updated_at, auth('tokens')->user()->last_active); } + + public function testFiltersProtectsWithScopes(): void + { + /** @var User $user1 */ + $user1 = fake(UserModel::class); + $token1 = $user1->generateAccessToken('foo', ['users-read']); + /** @var User $user2 */ + $user2 = fake(UserModel::class); + $token2 = $user2->generateAccessToken('foo', ['users-write']); + + // User 1 should be able to access the route + $this->withHeaders(['Authorization' => 'Bearer ' . $token1->raw_token]) + ->get('protected-user-route'); + + // Last Active should be greater than 'updated_at' column + $this->assertGreaterThan(auth('tokens')->user()->updated_at, auth('tokens')->user()->last_active); + + // User 2 should NOT be able to access the route + $result = $this->withHeaders(['Authorization' => 'Bearer ' . $token2->raw_token]) + ->get('protected-user-route'); + + $result->assertRedirectTo('/login'); + } } From fb0f387679651f0624f705449b3fbf32730c9b62 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 2 Sep 2022 00:41:39 -0500 Subject: [PATCH 2/5] Update doc title and links --- docs/guides/api-tokens.md | 2 +- docs/guides/index.md | 3 --- docs/index.md | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 docs/guides/index.md diff --git a/docs/guides/api-tokens.md b/docs/guides/api-tokens.md index d8e6ebf7d..5e522444e 100644 --- a/docs/guides/api-tokens.md +++ b/docs/guides/api-tokens.md @@ -1,4 +1,4 @@ -# API Tokens +# Protecting an API with Access Tokens Access Tokens can be used to authenticate users for your own site, or when allowing third-party developers to access your API. When making requests using access tokens, the token should be included in the `Authorization` header as a `Bearer` token. diff --git a/docs/guides/index.md b/docs/guides/index.md deleted file mode 100644 index 5ca573ea4..000000000 --- a/docs/guides/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Shield Guides - -These guides provide short tutorials on setting up or using different aspects of Shield. diff --git a/docs/index.md b/docs/index.md index 9cce2cdee..0b26e25c4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,3 +9,6 @@ * [Events](events.md) * [Testing](testing.md) * [Customization](customization.md) + +Guides: +* [Protecting an API with Access Tokens](guides/api-tokens.md) From b37ce685f9f1be07d7509ff4978ab1ea036e5dc1 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 6 Sep 2022 22:30:11 -0500 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: MGatner --- docs/guides/api-tokens.md | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/docs/guides/api-tokens.md b/docs/guides/api-tokens.md index 5e522444e..ade3fa231 100644 --- a/docs/guides/api-tokens.md +++ b/docs/guides/api-tokens.md @@ -2,18 +2,6 @@ Access Tokens can be used to authenticate users for your own site, or when allowing third-party developers to access your API. When making requests using access tokens, the token should be included in the `Authorization` header as a `Bearer` token. -To issue tokens for users, the `UserModel` must use the `CodeIgniter\Shield\Authentication\Traits\HasAccessTokens` trait. The `UserModel` that ships with Shield already uses this trait. - -```php -use CodeIgniter\Shield\Authentication\Traits\HasAccessTokens -use CodeIgniter\Model; - -class UserModel extneds Model -{ - use HasAccessTokens; -} -``` - Tokens are issued with the `generateAccessToken()` method on the user. This returns a `CodeIgniter\Shield\Entities\AccessToken` instance. Tokens are hashed using a SHA-256 algorithm before being saved to the database. The access token returned when you generate it will include a `raw_token` field that contains the plain-text, un-hashed, token. You should display this to your user at once so they have a chance to copy it somewhere safe, as this is the only time this will be available. After this request, there is no way to get the raw token. The `generateAccessToken` method requires a name for the token. These are free strings and are often used to identify the user/device the token was generated from, like 'Johns MacBook Air'. @@ -22,11 +10,11 @@ The `generateAccessToken` method requires a name for the token. These are free s $routes->get('/access/token', static function() { $token = auth()->user()->generateAccessToken(request()->getVar('token_name)); - return ['token' => $token->raw_token]; + return json_encode(['token' => $token->raw_token]); }); ``` -You can access all of the users' tokens with the `accessTokens()` method on the user. +You can access all of the user's tokens with the `accessTokens()` method on that user. ```php $tokens = $user->accessTokens(); @@ -83,6 +71,6 @@ $routes->group('api', ['filter' => 'tokens'], function($routes) { $routes->get('users', 'UserController::list', ['filter' => 'tokens:users-read']); ``` -When the filter runs, it checks the `Authorization` header for a `Bearer` value that has the raw token. It then looks hashes the raw token and looks it up in the database. Once found, it can determine the correct user, which will then be available through an `auth()->user()` call. +When the filter runs, it checks the `Authorization` header for a `Bearer` value that has the raw token. It then hashes the raw token and looks it up in the database. Once found, it can determine the correct user, which will then be available through an `auth()->user()` call. Note: Currently only a single scope can be used on a route filter. If multiple scopes are passed in, only the first one is checked. From 144aa32bb4eda62e77ba60b5f80bb8ebeccd4406 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 7 Sep 2022 08:21:09 -0500 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: kenjis --- docs/guides/api-tokens.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/guides/api-tokens.md b/docs/guides/api-tokens.md index ade3fa231..183470ac5 100644 --- a/docs/guides/api-tokens.md +++ b/docs/guides/api-tokens.md @@ -4,7 +4,7 @@ Access Tokens can be used to authenticate users for your own site, or when allow Tokens are issued with the `generateAccessToken()` method on the user. This returns a `CodeIgniter\Shield\Entities\AccessToken` instance. Tokens are hashed using a SHA-256 algorithm before being saved to the database. The access token returned when you generate it will include a `raw_token` field that contains the plain-text, un-hashed, token. You should display this to your user at once so they have a chance to copy it somewhere safe, as this is the only time this will be available. After this request, there is no way to get the raw token. -The `generateAccessToken` method requires a name for the token. These are free strings and are often used to identify the user/device the token was generated from, like 'Johns MacBook Air'. +The `generateAccessToken()` method requires a name for the token. These are free strings and are often used to identify the user/device the token was generated from, like 'Johns MacBook Air'. ```php $routes->get('/access/token', static function() { @@ -31,9 +31,10 @@ Access tokens can be given `scopes`, which are basically permission strings, for return $user->generateAccessToken('token-name', ['users-read'])->raw_token; ``` -NOTE: At this time, scope names should avoid using a colon (:) as this causes issues with the route filters being correctly recognized. +> **Note** +> At this time, scope names should avoid using a colon (`:`) as this causes issues with the route filters being correctly recognized. -When handling incoming requests you can check if the token has been granted access to the scope with the `tokenCan` method. +When handling incoming requests you can check if the token has been granted access to the scope with the `tokenCan()` method. ```php if ($user->tokenCan('users-read')) { @@ -73,4 +74,5 @@ $routes->get('users', 'UserController::list', ['filter' => 'tokens:users-read']) When the filter runs, it checks the `Authorization` header for a `Bearer` value that has the raw token. It then hashes the raw token and looks it up in the database. Once found, it can determine the correct user, which will then be available through an `auth()->user()` call. -Note: Currently only a single scope can be used on a route filter. If multiple scopes are passed in, only the first one is checked. +> **Note** +> Currently only a single scope can be used on a route filter. If multiple scopes are passed in, only the first one is checked. From 113312f2a5fd9252b1a12cc50cc3c37a9c319e7a Mon Sep 17 00:00:00 2001 From: MGatner Date: Thu, 8 Sep 2022 06:05:57 -0400 Subject: [PATCH 5/5] Update docs/guides/api-tokens.md --- docs/guides/api-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/api-tokens.md b/docs/guides/api-tokens.md index 183470ac5..7575ed804 100644 --- a/docs/guides/api-tokens.md +++ b/docs/guides/api-tokens.md @@ -8,7 +8,7 @@ The `generateAccessToken()` method requires a name for the token. These are free ```php $routes->get('/access/token', static function() { - $token = auth()->user()->generateAccessToken(request()->getVar('token_name)); + $token = auth()->user()->generateAccessToken(service('request')->getVar('token_name)); return json_encode(['token' => $token->raw_token]); });