├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config └── g4t-keycloak.php └── src ├── KeycloakGuard.php ├── KeycloakGuardServiceProvider.php └── KeycloakToken.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | # Laravel 4 specific 7 | bootstrap/compiled.php 8 | app/storage/ 9 | 10 | # Laravel 5 & Lumen specific 11 | public/storage 12 | public/hot 13 | 14 | # Laravel 5 & Lumen specific with changed public path 15 | public_html/storage 16 | public_html/hot 17 | 18 | storage/*.key 19 | .env 20 | Homestead.yaml 21 | Homestead.json 22 | /.vagrant 23 | .phpunit.result.cache 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HusseinAlaa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keycloak Guard Laravel Package 2 | The keycloak-guard-laravel package provides an integration between the Keycloak authentication server and a Laravel application. It allows you to use Keycloak as the authentication provider for your Laravel application. 3 | 4 | ![image](https://images.g2crowd.com/uploads/product/image/social_landscape/social_landscape_048daf32d4748a4dcd8a38617af4ff85/keycloak.png) 5 | 6 | 7 | Installation 8 | You can install the package using Composer: 9 | 10 | 11 | ```sh 12 | composer require g4t/keycloak 13 | ``` 14 | 15 | now publish config file 16 | run following command 17 | ```sh 18 | php artisan vendor:publish 19 | ``` 20 | and select `g4t\Keycloak\KeycloakGuardServiceProvider` provider 21 | 22 | # Configuration 23 | To configure the package, you need to add your Keycloak server details to your Laravel .env file: 24 | ```sh 25 | K_REALM_PUBLIC_KEY= 26 | K_LOAD_USER_FROM_DATABASE=true # get user data from database or keycloak 27 | K_USER_PROVIDER_CREDENTIAL=username # This setting specifies the unique column name in your user provider table that will be used to retrieve the user's credentials for authentication. 28 | K_TOKEN_PRINCIPAL_ATTRIBUTE=preferred_username # This setting specifies the key name for the attribute in the Keycloak token that will be used to check against the unique column specified in K_USER_PROVIDER_CREDENTIAL. The attribute should contain the user's unique identifier, such as a username or email address. 29 | K_TOKEN_EXPIRED=false # Enable this when you are sure that you have set the Keycloak server time correctly. 30 | ``` 31 | 32 | You also need to configure your Laravel application to use the keycloak guard. To do this, add the following to your `config/auth.php` file: 33 | 34 | ```sh 35 | 'guards' => [ 36 | // Other guards... 37 | 38 | 'keycloak' => [ 39 | 'driver' => 'keycloak', 40 | 'provider' => 'users', 41 | ], 42 | ], 43 | 44 | 'providers' => [ 45 | // Other providers... 46 | 47 | 'users' => [ 48 | 'driver' => 'keycloak', 49 | ], 50 | ], 51 | ``` 52 | 53 | # Usage 54 | Once the package is installed and configured, you can use the keycloak guard to authenticate users in your Laravel application. To authenticate a user, you can use the Auth::guard('keycloak')->attempt($credentials) method, where $credentials is an array of user credentials. 55 | 56 | For example: 57 | ```sh 58 | return auth('keycloak')->attempt([ 59 | 'url' => 'http://localhost:8080', 60 | 'realm' => 'realm-name', 61 | 'username' => 'username', 62 | 'password' => 1234, 63 | 'client_id' => 'client_id', 64 | 'client_secret' => 'client_secret', 65 | 'grant_type' => 'password', 66 | ]); 67 | ``` 68 | 69 | You can also use the auth('keycloak')->check() method to check if the user is authenticated: 70 | ```sh 71 | if (auth('keycloak')->check()) { 72 | // User is authenticated 73 | } else { 74 | // User is not authenticated 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "g4t/keycloak", 3 | "autoload": { 4 | "psr-4": { 5 | "g4t\\Keycloak\\": "src/" 6 | } 7 | }, 8 | "authors": [ 9 | { 10 | "name": "Hussein Alaa", 11 | "email": "hussein4alaa@gmail.com" 12 | } 13 | ], 14 | "license": "MIT", 15 | "minimum-stability": "stable", 16 | "require": { 17 | "firebase/php-jwt": "^6.3", 18 | "php": "^8.0" 19 | }, 20 | "extra": { 21 | "laravel": { 22 | "providers": [ 23 | "g4t\\Keycloak\\KeycloakGuardServiceProvider" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "71230db3ef3c513b9050bbf287d6105a", 8 | "packages": [ 9 | { 10 | "name": "firebase/php-jwt", 11 | "version": "v6.4.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/firebase/php-jwt.git", 15 | "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/firebase/php-jwt/zipball/4dd1e007f22a927ac77da5a3fbb067b42d3bc224", 20 | "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^7.1||^8.0" 25 | }, 26 | "require-dev": { 27 | "guzzlehttp/guzzle": "^6.5||^7.4", 28 | "phpspec/prophecy-phpunit": "^1.1", 29 | "phpunit/phpunit": "^7.5||^9.5", 30 | "psr/cache": "^1.0||^2.0", 31 | "psr/http-client": "^1.0", 32 | "psr/http-factory": "^1.0" 33 | }, 34 | "suggest": { 35 | "ext-sodium": "Support EdDSA (Ed25519) signatures", 36 | "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" 37 | }, 38 | "type": "library", 39 | "autoload": { 40 | "psr-4": { 41 | "Firebase\\JWT\\": "src" 42 | } 43 | }, 44 | "notification-url": "https://packagist.org/downloads/", 45 | "license": [ 46 | "BSD-3-Clause" 47 | ], 48 | "authors": [ 49 | { 50 | "name": "Neuman Vong", 51 | "email": "neuman+pear@twilio.com", 52 | "role": "Developer" 53 | }, 54 | { 55 | "name": "Anant Narayanan", 56 | "email": "anant@php.net", 57 | "role": "Developer" 58 | } 59 | ], 60 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 61 | "homepage": "https://github.com/firebase/php-jwt", 62 | "keywords": [ 63 | "jwt", 64 | "php" 65 | ], 66 | "support": { 67 | "issues": "https://github.com/firebase/php-jwt/issues", 68 | "source": "https://github.com/firebase/php-jwt/tree/v6.4.0" 69 | }, 70 | "time": "2023-02-09T21:01:23+00:00" 71 | } 72 | ], 73 | "packages-dev": [], 74 | "aliases": [], 75 | "minimum-stability": "dev", 76 | "stability-flags": [], 77 | "prefer-stable": true, 78 | "prefer-lowest": false, 79 | "platform": { 80 | "php": "^8.0" 81 | }, 82 | "platform-dev": [], 83 | "plugin-api-version": "2.3.0" 84 | } 85 | -------------------------------------------------------------------------------- /config/g4t-keycloak.php: -------------------------------------------------------------------------------- 1 | env('K_REALM_PUBLIC_KEY', null), 5 | 6 | 'user_provider_credential' => env('K_USER_PROVIDER_CREDENTIAL', 'username'), 7 | 8 | 'token_principal_attribute' => env('K_TOKEN_PRINCIPAL_ATTRIBUTE', 'preferred_username'), 9 | 10 | 'leeway' => env('K_LEEWAY', 0), 11 | 12 | 'load_user_from_database' => env('K_LOAD_USER_FROM_DATABASE', false), 13 | 14 | 'make_keycloak_token_expired' => env('K_TOKEN_EXPIRED', false), 15 | 16 | ]; 17 | -------------------------------------------------------------------------------- /src/KeycloakGuard.php: -------------------------------------------------------------------------------- 1 | config = config('g4t-keycloak'); 28 | $this->provider = $provider; 29 | $this->decodedToken = null; 30 | $this->user = null; 31 | $this->authenticate(); 32 | } 33 | 34 | /** 35 | *Authenticates the user by decoding the bearer token and validating it against the user provider credential. 36 | *@return string|null Returns an error message if authentication fails, otherwise returns null. 37 | */ 38 | private function authenticate() 39 | { 40 | try { 41 | $token = request()->bearerToken(); 42 | $this->decodedToken = KeycloakToken::decode( 43 | $token, 44 | $this->config['realm_public_key'], 45 | $this->config['leeway'] 46 | ); 47 | $this->validate([ 48 | $this->config['user_provider_credential'] => $this->decodedToken->{$this->config['token_principal_attribute']} 49 | ]); 50 | } catch (\Exception $e) { 51 | return $e->getMessage(); 52 | } 53 | 54 | } 55 | 56 | 57 | public function check() 58 | { 59 | return !is_null($this->user()); 60 | } 61 | 62 | 63 | public function guest() 64 | { 65 | return is_null($this->user()); 66 | } 67 | 68 | 69 | /** 70 | * Checks if the decoded JWT token has expired. 71 | * 72 | * @return bool Returns true if the token has expired or if there is no decoded token available, false otherwise. 73 | */ 74 | public function checkTokenExpiration() 75 | { 76 | if (!$this->decodedToken) { 77 | return true; 78 | } 79 | 80 | $expirationDate = Carbon::createFromTimestampMs($this->decodedToken->exp); 81 | 82 | return $expirationDate->isPast(); 83 | } 84 | 85 | 86 | /** 87 | * Get the authenticated user or return null if the user is not authenticated or the token is expired. 88 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 89 | */ 90 | public function user() 91 | { 92 | if ($this->config['make_keycloak_token_expired'] && $this->checkTokenExpiration()) { 93 | return null; 94 | } 95 | 96 | return $this->config['load_user_from_database'] ? $this->user : $this->getUserFromKeycloak(); 97 | } 98 | 99 | 100 | /** 101 | * Validates the user credentials and sets the authenticated user. 102 | * 103 | * @param array $credentials The user credentials to validate. 104 | * 105 | * @return bool Returns true if the user credentials are valid and the user is authenticated, or false otherwise. 106 | */ 107 | public function id() 108 | { 109 | $user = $this->user(); 110 | return $user ? $user->getAuthIdentifier() : null; 111 | } 112 | 113 | 114 | /** 115 | * Validate a user's credentials and attempt to set the authenticated user. 116 | * 117 | * @param array $credentials The credentials to use for the validation. 118 | * 119 | * @return bool Returns true if the validation succeeds and the user is set, false otherwise. 120 | */ 121 | public function validate($credentials = []) 122 | { 123 | $user = $this->provider->retrieveByCredentials($credentials); 124 | if ($user) { 125 | $this->setUser($user); 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | /** 132 | * Set the authenticated user. 133 | * 134 | * @param Authenticatable $user The authenticated user. 135 | * 136 | * @return void 137 | */ 138 | public function setUser(Authenticatable $user) 139 | { 140 | $this->user = $user; 141 | } 142 | 143 | 144 | /** 145 | * Attempt to authenticate the user with Keycloak using the provided credentials. 146 | * 147 | * @param array $credentials The credentials required to authenticate the user. 148 | * The 'url' and 'realm' keys are mandatory, other keys will be used as form parameters. 149 | * @return mixed The response from Keycloak containing the access token or an error message if the credentials are invalid. 150 | */ 151 | public function attempt(array $credentials) 152 | { 153 | $requiredKeys = ['url', 'realm']; 154 | foreach ($requiredKeys as $key) { 155 | if (!array_key_exists($key, $credentials)) { 156 | return response()->json(["message" => "{$key} required"], Response::HTTP_UNPROCESSABLE_ENTITY); 157 | } 158 | } 159 | 160 | $url = $credentials['url'] . "/realms/" . $credentials['realm'] . "/protocol/openid-connect/token"; 161 | $credentialsWithoutKeys = Arr::except($credentials, $requiredKeys); 162 | 163 | $response = Http::asForm()->post($url, $credentialsWithoutKeys)->json(); 164 | 165 | return $response; 166 | } 167 | 168 | 169 | /** 170 | * Returns the user object extracted from the decoded Keycloak token. 171 | * If no token has been decoded, returns null. 172 | * The user object contains the following properties: 173 | * - id: the unique identifier of the user 174 | * - name: the full name of the user 175 | * - username: the preferred username of the user 176 | * - given_name: the given name (first name) of the user 177 | * 178 | * @return object|null The user object or null if no token has been decoded 179 | */ 180 | public function getUserFromKeycloak() 181 | { 182 | if (!$this->decodedToken) { 183 | return null; 184 | } 185 | 186 | return (object) [ 187 | 'id' => $this->decodedToken->sub, 188 | 'name' => $this->decodedToken->name, 189 | 'username' => $this->decodedToken->preferred_username, 190 | 'given_name' => $this->decodedToken->given_name, 191 | ]; 192 | } 193 | 194 | 195 | 196 | } 197 | -------------------------------------------------------------------------------- /src/KeycloakGuardServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([__DIR__.'/../config/g4t-keycloak.php' => config_path('g4t-keycloak.php')], 'config'); 18 | $this->mergeConfigFrom(__DIR__.'/../config/g4t-keycloak.php', 'g4t-keycloak'); 19 | } 20 | 21 | 22 | /** 23 | * Register services. 24 | */ 25 | public function register() 26 | { 27 | Auth::extend('keycloak', function ($app, $name, array $config) { 28 | return new KeycloakGuard(Auth::createUserProvider($config['provider'])); 29 | }); 30 | } 31 | 32 | } 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/KeycloakToken.php: -------------------------------------------------------------------------------- 1 |