dmstr/yii2-usuario-keycloak
Composer 安装命令:
composer require dmstr/yii2-usuario-keycloak
包简介
Yii2 usuario keycloak plugin
README 文档
README
Installation
Install the package via composer
composer require dmstr/yii2-usuario-keycloak
For the installation of usuario see usuario docs
Setup
To run a keycloak using Docker (compose) please see docker-compose.keycloak.yml in the docker folder
For local development you should add keycloak-local to your /etc/hosts like this: 127.0.0.1 keycloak-local
You may need to replace 127.0.0.1 with your docker ip
Configuration
This part of config is mandatory. With this we add keycloak as a "social network"
KEYCLOAK_CLIENT_NAME=Keycloak KEYCLOAK_CLIENT_ID=app # See credentials tab in example realms app client KEYCLOAK_CLIENT_SECRET= KEYCLOAK_ISSUER_URL=http://keycloak-local:8080/realms/example
use yii\authclient\Collection; use Da\User\AuthClient\Keycloak; return [ 'components' => [ 'authClientCollection' => [ 'class' => Collection::class, 'clients' => [ 'keycloak' => [ 'class' => Keycloak::class, 'title' => getenv('KEYCLOAK_CLIENT_NAME'), 'clientId' => getenv('KEYCLOAK_CLIENT_ID'), 'clientSecret' => getenv('KEYCLOAK_CLIENT_SECRET'), 'issuerUrl' => getenv('KEYCLOAK_ISSUER_URL') ] ] ], 'user' => [ // So that the session do not get mixed up 'enableAutoLogin' => false ] ] ]
Enable front channel logout from keycloak when user logs out in app
use dmstr\usuario\keycloak\controllers\SecurityController; return [ 'modules' => [ 'user' => [ 'controllerMap' => [ 'security' => [ 'class' => SecurityController::class ] ] ] ] ]
Only allow login to users with verified emails
use Da\User\Event\SocialNetworkAuthEvent; use dmstr\usuario\keycloak\controllers\SecurityController; use yii\web\ForbiddenHttpException; return [ 'modules' => [ 'user' => [ 'controllerMap' => [ 'security' => [ 'class' => SecurityController::class, 'on ' . SocialNetworkAuthEvent::EVENT_BEFORE_AUTHENTICATE => function (SocialNetworkAuthEvent $event) { if (isset($event->getClient()->getUserAttributes()['email_verified']) && $event->getClient()->getUserAttributes()['email_verified'] === false) { throw new ForbiddenHttpException(Yii::t('usuario-keycloak', 'Account is not verified. Please confirm your registration email.')); } } ] ] ] ] ]
Disabled the sending of a welcome message when a user is from keycloak
return [ 'modules' => [ 'user' => [ 'sendWelcomeMailAfterSocialNetworkRegistration' => false ] ] ]
If you do not want to allow identity switching. This is recommended because potential RBAC Roles with the TokenRoleRule may not work correctly
return [ 'modules' => [ 'user' => [ 'enableSwitchIdentities' => false ] ] ]
Logout the user if the keycloak token is expired
This only works in a web application so add your config accordingl and needs some slight modifications to your user component. You can copy and use this example or extend your existing user compoent.
<?php namespace app\components; use Yii; use yii\base\InvalidConfigException; /** * @property-read string|null $authSource */ class User extends yii\web\User { protected const AUTH_SOURCE_CLIENT_ID_SESSION_KEY = 'authSourceClientId'; /** * @throws InvalidConfigException */ public function setAuthSource(string $clientId): void { Yii::$app->getSession()->set(self::AUTH_SOURCE_CLIENT_ID_SESSION_KEY, $clientId); } /** * Returns the name of the auth client with which the user has authenticated himself. * * - null means not authenticated. * - 'app' means, not authenticated via an auth client * * @return string|null */ public function getAuthSource(): ?string { if ($this->getIsGuest()) { return null; } return Yii::$app->getSession()->get(self::AUTH_SOURCE_CLIENT_ID_SESSION_KEY, 'app'); } } ?>
use app\components\User; use Da\User\AuthClient\Keycloak; use Da\User\Event\SocialNetworkAuthEvent; use dmstr\usuario\keycloak\controllers\SecurityController; use yii\base\Exception; use yii\base\InvalidArgumentException; use yii\web\Application; return [ 'on ' . Application::EVENT_BEFORE_REQUEST => function () { $user = Yii::$app->getUser(); $keycloakClientId = 'keycloak'; if ($user && !$user->getIsGuest() && Yii::$app->getUser()->getAuthSource() === $keycloakClientId) { try { $jwt = Yii::$app->jwt; /** @var Keycloak $keycloak */ $keycloak = Yii::$app->authClientCollection->getClient($keycloakClientId); // Check if token is valid if (!$jwt->validate($keycloak->getAccessToken()->getToken())) { // If token is invalid log out the user throw new Exception('Access token invalid.'); } } catch (Exception $exception) { Yii::error($exception->getMessage()); // Logout user if token cannot be revalidated or is revoked $user->logout(); } } }, 'components' => [ 'user' => [ 'class' => User::class ] ], 'modules' => [ 'user' => [ 'controllerMap' => [ 'security' => [ 'class' => SecurityController::class, 'on ' . SocialNetworkAuthEvent::EVENT_AFTER_AUTHENTICATE => function (SocialNetworkAuthEvent $event) { // Save the auth client info to differentiate afterward from which auth client the user was authenticated Yii::$app->getUser()->setAuthSource($event->getClient()->getId()); } ] ] ] ] ];
Change the login url so the site redirect you directly to the keycloak login page
return [ 'components' => [ 'user' => [ 'loginUrl' => '/user/security/auth?authclient=keycloak' ] ] ];
User identity to use in rest calls
We suggest to use the JwtHttpBearerAuth from bizley/yii2jwt for this. You can
use the following example to implement it in your user
<?php namespace app\models; use bizley\jwt\JwtHttpBearerAuth; use Da\User\Model\SocialNetworkAccount; use Lcobucci\JWT\Token\Plain; use yii\base\NotSupportedException; use Yii; class User extends \Da\User\Model\User { /** * @inheritdoc */ public static function findIdentityByAccessToken($token, $type = null) { // use dmstr\usuario\keycloak\behaviors\JwtAutoProvisionAuth if you want to auto creat the user. Module must be configured. See section `JwtAutoProvisionAuth` if ($type === JwtHttpBearerAuth::class) { /** @var Plain $jwtToken */ $jwtToken = Yii::$app->jwt->getParser()->parse((string)$token); $claims = $jwtToken->claims(); $userClientId = $claims->get('sub'); /** @var SocialNetworkAccount|null $socialAccount */ $socialAccount = SocialNetworkAccount::find()->andWhere([ 'provider' => 'keycloak', 'client_id' => $userClientId ])->one(); if ($socialAccount) { return static::find() ->whereId($socialAccount->user_id) ->andWhere(['blocked_at' => null]) ->andWhere(['NOT', ['confirmed_at' => null]]) ->andWhere(['gdpr_deleted' => 0]) ->one(); } return null; } throw new NotSupportedException("Type '$type' is not implemented."); } }
Using the identity class
use app\models\User as UserModel; return [ 'components' => [ 'user' => [ 'identityClass' => UserModel::class ] ] ]
Generate the keys for the jwt
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
# Don't add passphrase
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
KEYCLOAK_PRIVATE_KEY_FILE=file:///path/to/jwtRS256.key KEYCLOAK_PUBLIC_KEY_FILE=file:///path/to/jwtRS256.key.pub
use bizley\jwt\Jwt; use Lcobucci\JWT\Validation\Constraint\IssuedBy; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\LooseValidAt; use Lcobucci\Clock\SystemClock; return [ 'components' => [ 'jwt' => [ 'class' => Jwt::class, 'signer' => Jwt::RS256, 'signingKey' => [ 'key' => getenv('KEYCLOAK_PRIVATE_KEY_FILE'), 'method' => Jwt::METHOD_FILE, ], 'verifyingKey' => [ 'key' => getenv('KEYCLOAK_PUBLIC_KEY_FILE'), 'method' => Jwt::METHOD_FILE, ], 'validationConstraints' => function (Jwt $jwt) { $config = $jwt->getConfiguration(); return [ new SignedWith($config->signer(), $config->verificationKey()), new IssuedBy(getenv('KEYCLOAK_ISSUER_URL')), new LooseValidAt(SystemClock::fromUTC()), ]; } ] ] ];
if you only want to use validation and parsing you can configure the jwt component like this.
use bizley\jwt\JwtTools; use Lcobucci\JWT\Validation\Constraint\IssuedBy; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\LooseValidAt; use Lcobucci\Clock\SystemClock; return [ 'components' => [ 'jwt' => [ 'class' => JwtTools::class, 'validationConstraints' => function (JwtTools $jwt) { return [ new SignedWith($jwt->buildSigner(Jwt::RS256), InMemory::plainText(getenv('KEYCLOAK_PUBLIC_KEY_FILE'))), // You could also use this line if you do not want to use a separate public key file // new SignedWith($jwt->buildSigner(Jwt::RS256), InMemory::plainText(KeycloakHelper::publicKeyFromIssuer(getenv('KEYCLOAK_ISSUER_URL')))), new IssuedBy(getenv('KEYCLOAK_ISSUER_URL')), new LooseValidAt(SystemClock::fromUTC()), ]; } ] ] ];
In combination with a Keycloak, the value KEYCLOAK_PUBLIC_KEY_FILE should be that from the Keycloak Public Key
When using the JwtHttpBearerAuth ensure that cors is before the authenticator in the behaviors of your controller
or module and all access controll stuff is after.
Auto submit social account registration confirm form
use Da\User\Controller\RegistrationController; use ActionEvent; return [ 'modules' => [ 'user' => [ 'controllerMap' => [ 'registration' => [ 'class' => RegistrationController::class, 'on ' . RegistrationController::EVENT_BEFORE_ACTION => function (ActionEvent $event) { if ($event->action->id === 'connect') { // You may need to change the form id but this is the default $event->action->controller->view->registerJs('if ($(".has-error").length === 0){$("form#User").submit()};'); } } ] ] ] ] ]
Add a JWT Component to be able to parse JWT Tokens
use bizley\jwt\JwtTools; use Lcobucci\Clock\SystemClock; use Lcobucci\JWT\Validation\Constraint\LooseValidAt; return [ 'components' => [ 'jwt' => [ 'class' => JwtTools::class, 'validationConstraints' => function (JwtTools $jwt) { return [ new LooseValidAt(SystemClock::fromUTC()) ]; } ] ] ]
TokenRoleRule
This rule allows you to assign roles to users based on the roles they have in keycloak. This is useful if you want to use keycloak as a single source of truth for your user roles. Note that the role names in keycloak must match the roles in RBAC and should be assigned to the "Default" Role so they get evaluated for all logged in users.
The TokenRoleRule Rule can be configured to work with different Keycloak configurations.
Default configuration:
$authClientId$
is the name of the client in the main.php used to connect to an IDP. It defaults to 'keycloak'
$rbacRolesClaimName$
claim where the Roles are saved in the Access Token. Keycloak defaults to 'realm_access.roles'
$jwtComponent$
JWT component used to parse JWT Tokens. Defaults to 'jwt'
$authCollectionComponent$
Auth collection of clients. Defaults to 'authClientCollection'
$tokenParam$
Parameter used to extract the Token used for role checking, defaults to 'access_token'
Configuration
The parameters mentioned above can be configured like this
use dmstr\usuario\keycloak\auth\TokenRoleRule; 'container' => [ 'definitions' => [ TokenRoleRule::class => [ 'rbacRolesClaimName' => 'roles', ... ] ] ]
JwtAutoProvisionAuth
JwtAutoProvisionAuth is an authentication filter that automatically creates user accounts when someone logs in with a valid JWT token from auth client. If a user with the token's email doesn't exist in the
system, it creates a new user account and links it to the Keycloak identity; if the user already exists, it just connects the auth client account to the existing user. This allows seamless user onboarding
where people can access the application immediately using their auth client credentials without manual account creation.
It is auto configured to use a jwt component named jwt and a auth client named keycloak
Example usage in a yii\rest\Controller or yii\base\Module
use dmstr\usuario\keycloak\behaviors\JwtAutoProvisionAuth public function behaviors(): array { $behaviors = parent::behaviors(); $behaviors['authenticator'] = [ 'class' => JwtAutoProvisionAuth::class, 'jwt' => 'jwt', // your JWT component ID 'authClientId' => 'keycloak', // your auth client ID 'debug' => false ]; return $behaviors; }
Front Channel Logout
For this to work you must use a yii\web\MultiFieldSession session e.g. yii\web\DbSession
Web config for extra fields write callback in session
'session' => [ 'writeCallback' => function () { try { $sid = Yii::$app->authClientCollection->getClient('keycloak')->getAccessToken()?->getParam('sid'); return [ 'keycloak_sid' => $sid ]; } catch (ClientErrorResponseException $e) { Yii::error($e->getMessage()); } return []; } ]
Console config for migrations if you use yii\web\DbSession
'controllerMap' => [ 'migrate' => [ 'migrationPath' => [ '@vendor/dmstr/yii2-usuario-keycloak/src/migrations' ] ] ]
Optional but recommended: If you use codemix\localeurls\UrlManager as an url manager add this to you web config prevent unnecassary redirects
'urlManager' => [ 'ignoreLanguageUrlPatterns' => [ '#user/security/front-channel-logout#' => '#user/security/front-channel-logout#', ] ]
In your keycloak add this as your front channel logout url in the client settings: https://your-domain/user/security/front-channel-logout
Custom IDP Hint
Used to automatically redirect to an existent Broker. Current default in SecurityController is set to the Keycloak default kc_idp_hint.
To set a custom param overwrite idp_hint_param in SecurityController.
To use this feature, add the hint to the URL after the authclient /user/security/auth?authclient=keycloak&kc_idp_hint=broker-idp-alias
dmstr/yii2-usuario-keycloak 适用场景与选型建议
dmstr/yii2-usuario-keycloak 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 13.5k 次下载、GitHub Stars 达 3, 最近一次更新时间为 2023 年 01 月 13 日, 在 PHP 生态内属于活跃度较高的组件。
我们在过去多个企业项目中使用过 dmstr/yii2-usuario-keycloak 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 dmstr/yii2-usuario-keycloak 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
统计信息
- 总下载量: 13.5k
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 3
- 点击次数: 4
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: Unknown
- 更新时间: 2023-01-13