Skip to content

Commit

Permalink
Implement: 'scopes', 'hasScope' and 'hasAnyScopes' method (#107)
Browse files Browse the repository at this point in the history
* Implement 'scopes', 'hasScope' and 'hasAnyScopes' method. Issue: #106

* Update README.md. Issue: #106

* Add Tests. Issue: #106

* minor fix

* Fix coverage, test Auth::scopes()

* minor fix

* Add test 'test_check_user_no_scopes'

* Add scopes() doc
  • Loading branch information
vlauciani committed Feb 26, 2024
1 parent 042483a commit cc051f5
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 2 deletions.
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,15 @@ Simple Keycloak Guard implements `Illuminate\Contracts\Auth\Guard`. So, all Lara

## Keycloak Guard methods

#### Token
`token()`
_Returns full decoded JWT token from authenticated user._

```php
$token = Auth::token() // or Auth::user()->token()
```

<br>

#### Role
`hasRole('some-resource', 'some-role')`
_Check if authenticated user has a role on resource_access_

Expand Down Expand Up @@ -287,6 +287,42 @@ Auth::hasAnyRole('myapp-frontend', ['myapp-frontend-role1', 'myapp-frontend-role
Auth::hasAnyRole('myapp-backend', ['myapp-frontend-role1', 'myapp-frontend-role2']) // false
```

#### Scope
Example decoded payload:
```json
{
"scope": "scope-a scope-b scope-c",
}
```

`scopes()`
_Get all user scopes_

```php
array:3 [
0 => "scope-a"
1 => "scope-b"
2 => "scope-c"
]
```

`hasScope('some-scope')`
_Check if authenticated user has a scope_

```php
Auth::hasScope('scope-a') // true
Auth::hasScope('scope-d') // false
```

`hasAnyScope(['scope-a', 'scope-c'])`
_Check if the authenticated user has any of the scopes_

```php
Auth::hasAnyScope(['scope-a', 'scope-c']) // true
Auth::hasAnyScope(['scope-a', 'scope-d']) // true
Auth::hasAnyScope(['scope-f', 'scope-k']) // false
```

# Contribute

You can run this project on VSCODE with Remote Container. Make sure you will use internal VSCODE terminal (inside running container).
Expand Down
44 changes: 44 additions & 0 deletions src/KeycloakGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,48 @@ public function hasAnyRole($resource, array $roles)

return false;
}

/**
* Get scope(s)
* @return array
*/
public function scopes(): array
{
$scopes = $this->decodedToken->scope ?? null;

if ($scopes) {
return explode(' ', $scopes);
}

return [];
}

/**
* Check if authenticated user has a especific scope
* @param string $scope
* @return bool
*/
public function hasScope(string $scope): bool
{
$scopes = $this->scopes();

if (in_array($scope, $scopes)) {
return true;
}

return false;
}

/**
* Check if authenticated user has a any scope
* @param array $scopes
* @return bool
*/
public function hasAnyScope(array $scopes): bool
{
return count(array_intersect(
$this->scopes(),
is_string($scopes) ? [$scopes] : $scopes
)) > 0;
}
}
65 changes: 65 additions & 0 deletions tests/AuthenticateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,71 @@ public function test_prevent_cross_roles_resources_with_any_role()
$this->assertFalse(Auth::hasAnyRole('myapp-backend', ['myapp-frontend-role1', 'myapp-frontend-role2']));
}

public function test_check_user_has_scope()
{
$this->buildCustomToken([
'scope' => 'scope-a scope-b scope-c',
]);

$this->withKeycloakToken()->json('GET', '/foo/secret');
$this->assertTrue(Auth::hasScope('scope-a'));
}

public function test_check_user_no_has_scope()
{
$this->buildCustomToken([
'scope' => 'scope-a scope-b scope-c',
]);

$this->withKeycloakToken()->json('GET', '/foo/secret');
$this->assertFalse(Auth::hasScope('scope-d'));
}

public function test_check_user_has_any_scope()
{
$this->buildCustomToken([
'scope' => 'scope-a scope-b scope-c',
]);

$this->withKeycloakToken()->json('GET', '/foo/secret');
$this->assertTrue(Auth::hasAnyScope(['scope-a', 'scope-c']));
}

public function test_check_user_no_has_any_scope()
{
$this->buildCustomToken([
'scope' => 'scope-a scope-b scope-c',
]);

$this->withKeycloakToken()->json('GET', '/foo/secret');
$this->assertFalse(Auth::hasAnyScope(['scope-f', 'scope-k']));
}

public function test_check_user_scopes()
{
$this->buildCustomToken([
'scope' => 'scope-a scope-b scope-c',
]);

$this->withKeycloakToken()->json('GET', '/foo/secret');

$expectedValues = ["scope-a", "scope-b", "scope-c"];
foreach ($expectedValues as $value) {
$this->assertContains($value, Auth::scopes());
}
$this->assertCount(count($expectedValues), Auth::scopes());
}

public function test_check_user_no_scopes()
{
$this->buildCustomToken([
'scope' => null,
]);

$this->withKeycloakToken()->json('GET', '/foo/secret');
$this->assertCount(0, Auth::scopes());
}

public function test_custom_user_retrieve_method()
{
config(['keycloak.user_provider_custom_retrieve_method' => 'custom_retrieve']);
Expand Down

0 comments on commit cc051f5

Please sign in to comment.