Skip to content

Commit 95b02fa

Browse files
committed
ApiToken docs and filter update.
1 parent 6d15d8c commit 95b02fa

File tree

5 files changed

+118
-1
lines changed

5 files changed

+118
-1
lines changed

docs/guides/api-tokens.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# API Tokens
2+
3+
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.
4+
5+
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.
6+
7+
```php
8+
use CodeIgniter\Shield\Authentication\Traits\HasAccessTokens
9+
use CodeIgniter\Model;
10+
11+
class UserModel extneds Model
12+
{
13+
use HasAccessTokens;
14+
}
15+
```
16+
17+
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.
18+
19+
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'.
20+
21+
```php
22+
$routes->get('/access/token', static function() {
23+
$token = auth()->user()->generateAccessToken(request()->getVar('token_name));
24+
25+
return ['token' => $token->raw_token];
26+
});
27+
```
28+
29+
You can access all of the users' tokens with the `accessTokens()` method on the user.
30+
31+
```php
32+
$tokens = $user->accessTokens();
33+
foreach($tokens as $token) {
34+
//
35+
}
36+
```
37+
38+
## Token Permissions
39+
40+
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.
41+
42+
```php
43+
return $user->generateAccessToken('token-name', ['users-read'])->raw_token;
44+
```
45+
46+
NOTE: At this time, scope names should avoid using a colon (:) as this causes issues with the route filters being correctly recognized.
47+
48+
When handling incoming requests you can check if the token has been granted access to the scope with the `tokenCan` method.
49+
50+
```php
51+
if ($user->tokenCan('users-read')) {
52+
//
53+
}
54+
```
55+
56+
### Revoking Tokens
57+
58+
Tokens can be revoked by deleting them from the database with the `revokeAccessToken($rawToken)` or `revokeAllAccessTokens()` methods.
59+
60+
```php
61+
$user->revokeAccessToken($rawToken);
62+
$user->revokeAllAccessTokens();
63+
```
64+
65+
## Protecting Routes
66+
67+
The first way to specify which routes are protected is to use the `tokens` controller filter.
68+
69+
For example, to ensure it protects all routes under the `/api` route group, you would use the `$filters` setting on `app/Config/Filters.php`.
70+
71+
```php
72+
public $filters = [
73+
'tokens' => ['before' => ['api/*']],
74+
];
75+
```
76+
77+
You can also specify the filter should run on one or more routes within the routes file itself:
78+
79+
```php
80+
$routes->group('api', ['filter' => 'tokens'], function($routes) {
81+
//
82+
});
83+
$routes->get('users', 'UserController::list', ['filter' => 'tokens:users-read']);
84+
```
85+
86+
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.
87+
88+
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.

docs/guides/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Shield Guides
2+
3+
These guides provide short tutorials on setting up or using different aspects of Shield.

src/Filters/TokenAuth.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function before(RequestInterface $request, $arguments = null)
4848
'token' => $request->getHeaderLine(setting('Auth.authenticatorHeader')['tokens'] ?? 'Authorization'),
4949
]);
5050

51-
if (! $result->isOK()) {
51+
if (! $result->isOK() || (! empty($arguments) && $result->extraInfo()->tokenCant($arguments[0]))) {
5252
return redirect()->to('/login');
5353
}
5454

tests/Authentication/Filters/AbstractFilterTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ static function ($routes): void {
6161
echo 'Open';
6262
});
6363
$routes->get('login', 'AuthController::login', ['as' => 'login']);
64+
$routes->get('protected-user-route', static function (): void {
65+
echo 'Protected';
66+
}, ['filter' => $this->alias . ':users-read']);
6467

6568
Services::injectMock('routes', $routes);
6669
}

tests/Authentication/Filters/TokenFilterTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,27 @@ public function testRecordActiveDate(): void
6363
// Last Active should be greater than 'updated_at' column
6464
$this->assertGreaterThan(auth('tokens')->user()->updated_at, auth('tokens')->user()->last_active);
6565
}
66+
67+
public function testFiltersProtectsWithScopes(): void
68+
{
69+
/** @var User $user1 */
70+
$user1 = fake(UserModel::class);
71+
$token1 = $user1->generateAccessToken('foo', ['users-read']);
72+
/** @var User $user2 */
73+
$user2 = fake(UserModel::class);
74+
$token2 = $user2->generateAccessToken('foo', ['users-write']);
75+
76+
// User 1 should be able to access the route
77+
$this->withHeaders(['Authorization' => 'Bearer ' . $token1->raw_token])
78+
->get('protected-user-route');
79+
80+
// Last Active should be greater than 'updated_at' column
81+
$this->assertGreaterThan(auth('tokens')->user()->updated_at, auth('tokens')->user()->last_active);
82+
83+
// User 2 should NOT be able to access the route
84+
$result = $this->withHeaders(['Authorization' => 'Bearer ' . $token2->raw_token])
85+
->get('protected-user-route');
86+
87+
$result->assertRedirectTo('/login');
88+
}
6689
}

0 commit comments

Comments
 (0)