dmstr/yii2-usuario-keycloak

此包的最新版本(3.2.6)没有可用的许可信息。

Yii2 usuario keycloak 插件

3.2.6 2024-07-01 13:34 UTC

README

安装

通过 composer 安装包

composer require dmstr/yii2-usuario-keycloak

有关 usuario 安装的说明,请参阅 usuario 文档

设置

要使用 Docker (compose) 运行 keycloak,请参阅 docker 文件夹中的 docker-compose.keycloak.yml

对于本地开发,您应该在 /etc/hosts 中添加 keycloak-local,如下所示:127.0.0.1 keycloak-local

您可能需要将 127.0.0.1 替换为您的 docker ip

配置

此部分配置是必须的。通过此配置,我们将 keycloak 添加为“社交网络”

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
        ]
    ]
]

启用当用户在应用程序中注销时从 keycloak 的前端渠道注销

use dmstr\usuario\keycloak\controllers\SecurityController;

return [
    'modules' => [
        'user' => [
            'controllerMap' => [
                'security' => [
                    'class' => SecurityController::class
                ]
            ]
        ]
    ] 
]

仅允许具有已验证电子邮件的用户登录

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.'));
                        }
                    }
                ]
            ]
        ]
    ]   
]

禁用当用户来自 keycloak 时发送欢迎消息

return [
    'modules' => [
        'user' => [
            'sendWelcomeMailAfterSocialNetworkRegistration' => false
        ]
    ] 
]

如果您不想允许身份切换。这被推荐,因为 TokenRoleRule 可能无法正确处理潜在的角色 RBAC

return [
    'modules' => [
        'user' => [
            'enableSwitchIdentities' => false
        ]
    ] 
]

当 keycloak 令牌过期时注销用户

这仅适用于 Web 应用程序,因此请根据需要添加您的配置并对用户组件进行一些轻微修改。您可以复制并使用此示例或扩展您现有的用户组件。

<?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());
                    }
                ]
            ]
        ]
    ] 
];

更改登录 URL 以使站点直接将您重定向到 keycloak 登录页面

return [
    'components' => [
        'user' => [
            'loginUrl' => '/user/security/auth?authclient=keycloak'
        ]
    ]
];

在 REST 调用中使用用户身份

我们建议使用来自 bizley/yii2jwtJwtHttpBearerAuth。您可以在您的用户中实现以下示例。

<?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)
    {
        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.");
    }
}

使用身份类

use app\models\User as UserModel;

return [
    'components' => [
        'user' => [
            'identityClass' => UserModel::class
        ]
    ]
]

生成 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()),
                ];
            }
        ]
    ]
];

如果您只想使用验证和解析,可以像这样配置 jwt 组件。

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()),
                ];
            }
        ]
    ]
];

与 Keycloak 结合使用时,KEYCLOAK_PUBLIC_KEY_FILE 的值应该是 Keycloak 公钥。

当使用 JwtHttpBearerAuth 时,请确保在控制器或模块的 behaviors 中将 cors 设置在 authenticator 之前,并将所有访问控制设置在之后。

自动提交社交账户注册确认表单

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()};');
                        }
                    }
                ]
            ]
        ]
    ] 
]

TokenRoleRule

此规则允许您根据用户在 keycloak 中的角色将角色分配给用户。如果您想将 keycloak 用作用户角色的单一来源,这将非常有用。请注意,keycloak 中的角色名称必须与角色匹配,并且应分配给任何已登录用户。