From cc051f5c9178954a2bb7dd56a12f6189fa0e2d58 Mon Sep 17 00:00:00 2001 From: Valentino Lauciani Date: Mon, 26 Feb 2024 23:38:24 +0100 Subject: [PATCH] Implement: 'scopes', 'hasScope' and 'hasAnyScopes' method (#107) * Implement 'scopes', 'hasScope' and 'hasAnyScopes' method. Issue: https://github.com/robsontenorio/laravel-keycloak-guard/issues/106 * Update README.md. Issue: https://github.com/robsontenorio/laravel-keycloak-guard/issues/106 * Add Tests. Issue: https://github.com/robsontenorio/laravel-keycloak-guard/issues/106 * minor fix * Fix coverage, test Auth::scopes() * minor fix * Add test 'test_check_user_no_scopes' * Add scopes() doc --- README.md | 40 +++++++++++++++++++++-- src/KeycloakGuard.php | 44 ++++++++++++++++++++++++++ tests/AuthenticateTest.php | 65 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4193e0c..5794d08 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,7 @@ Simple Keycloak Guard implements `Illuminate\Contracts\Auth\Guard`. So, all Lara ## Keycloak Guard methods +#### Token `token()` _Returns full decoded JWT token from authenticated user._ @@ -248,8 +249,7 @@ _Returns full decoded JWT token from authenticated user._ $token = Auth::token() // or Auth::user()->token() ``` -
- +#### Role `hasRole('some-resource', 'some-role')` _Check if authenticated user has a role on resource_access_ @@ -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). diff --git a/src/KeycloakGuard.php b/src/KeycloakGuard.php index 11d7bda..1a06cec 100644 --- a/src/KeycloakGuard.php +++ b/src/KeycloakGuard.php @@ -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; + } } diff --git a/tests/AuthenticateTest.php b/tests/AuthenticateTest.php index 0ab2996..b88de50 100644 --- a/tests/AuthenticateTest.php +++ b/tests/AuthenticateTest.php @@ -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']);