cloudcogsio/oauth2-keycloak

Keycloak OAuth2 客户端

v0.2.1 2022-09-14 14:27 UTC

This package is auto-updated.

Last update: 2024-09-11 09:07:37 UTC


README

GitHub GitHub last commit

此软件包为 PHP League 的 OAuth 2.0 客户端提供 Keycloak OAuth 2.0 支持。

客户端使用 Keycloak 的 .well-known 服务端点来查询 OpenID 提供者元数据,以自动发现授权、令牌和公开密钥的端点。

安装

要安装,请使用 composer

composer require cloudcogsio/oauth2-keycloak

用法

用法与 The League 的 OAuth 客户端相同,使用 \Cloudcogs\OAuth2\Client\Provider\Keycloak 作为提供者。

通过 Keycloak OIDC JSON 文件进行配置

客户端可以通过传递从您的 Keycloak 服务器下载的 Keycloak OIDC JSON 文件进行配置。

  1. 转到您的 Keycloak 管理员
  2. 选择“客户端”选项
  3. 选择所需客户端的客户端 ID
  4. 选择“安装”选项卡
  5. 在“格式选项”下拉菜单中,选择“Keycloak OIDC JSON”
  6. 下载。默认文件名为“keycloak.json”

当使用 Keycloak OIDC JSON 文件时,仅需要文件和一个重定向 URI 来设置客户端。

使用 Keycloak OIDC JSON (keycloak.json) 进行提供者配置

$provider = new Keycloak([
    'config' => 'keycloak.json',
    'redirectUri' => 'https://example.com/callback-url'
]);

通过选项进行配置

客户端也可以通过传递(至少)authServerUrlrealm 选项来配置,这些选项用于端点自动发现。

您仍然需要引用 Keycloak 中的 OIDC JSON 配置来检索 clientIdclientSecret 的值。这些将是 resourcecredentials->secret

使用 authServerUrlrealm 选项进行提供者配置

$provider = new Keycloak([
    'authServerUrl' => 'https://:8080/auth/',
    'realm' => 'demo-realm',
    'clientId' => '{keycloak-resource}',
    'clientSecret' => '{keycloak-credentials-secret}',
    'redirectUri' => 'https://example.com/callback-url'
]);

授权码流

假设 $provider 已通过上述方法之一配置。

// If we don't have an authorization code then get one
if (!isset($_GET['code'])) {

    // Fetch the authorization URL from the provider; 
    $authorizationUrl = $provider->getAuthorizationUrl();

    // Get the state generated for you and store it to the session.
    $_SESSION['oauth2state'] = $provider->getState();

    // Redirect the user to the authorization URL.
    header('Location: ' . $authorizationUrl);
    exit;

// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {

    if (isset($_SESSION['oauth2state'])) {
        unset($_SESSION['oauth2state']);
    }

    exit('Invalid state');
    
} else {

    try {

        // Try to get an access token using the authorization code grant.
        $accessToken = $provider->getAccessToken('authorization_code', [
            'code' => $_GET['code']
        ]);

        // We have an access token, which we may use in authenticated
        // requests against the service provider's API.
        echo 'Access Token: ' . $accessToken->getToken() . "<br>";
        echo 'Refresh Token: ' . $accessToken->getRefreshToken() . "<br>";
        echo 'Expired in: ' . $accessToken->getExpires() . "<br>";
        echo 'Already expired? ' . ($accessToken->hasExpired() ? 'expired' : 'not expired') . "<br>";

        // Using the access token, we may look up details about the
        // resource owner.
        $resourceOwner = $provider->getResourceOwner($accessToken);

        var_export($resourceOwner->toArray());

        // The provider provides a way to get an authenticated API request for
        // the service, using the access token; it returns an object conforming
        // to Psr\Http\Message\RequestInterface.
        $request = $provider->getAuthenticatedRequest(
            'GET',
            'https://service.example.com/resource',
            $accessToken
        );

    } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {

        // Failed to get the access token or user details.
        exit($e->getMessage());

    }
}

刷新令牌

if ($existingAccessToken->hasExpired()) {
    $newAccessToken = $provider->getAccessToken('refresh_token', [
        'refresh_token' => $existingAccessToken->getRefreshToken()
    ]);

    // Purge old access token and store new access token to your data store.
}

客户端登出

客户端提供了一种方便处理登出操作的方法。

可以将重定向 URI 传递给该方法或使用客户端的 redirectUri 选项进行重定向。URI 必须配置在 Keycloak 中客户端定义的“有效的重定向 URI”字段中。

$url = "https://example.com/logout-url-redirect";
$provider->logoutAndRedirect($url);

资源所有者密码凭据授权

🛑 危险! 如果服务提供者支持授权码授权类型(见上方),我们建议不要使用此授权类型,因为这会加强 密码反模式,使用户认为可以信任第三方应用程序使用其用户名和密码。

尽管如此,在某些情况下,资源所有者密码凭据授权是可接受且有用的。

try {

    // Try to get an access token using the resource owner password credentials grant.
    $accessToken = $provider->getAccessToken('password', [
        'username' => 'myuser',
        'password' => 'mysupersecretpassword'
    ]);
    
	$resourceOwner = $provider->getResourceOwner($accessToken);
	
	var_export($resourceOwner->toArray());

} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {

    // Failed to get the access token
    exit($e->getMessage());

}

客户端凭据授权

当您的应用程序代表自己访问其控制或拥有的服务提供者中的资源时,它可以使用 客户端凭据 授权类型。

当您私有地存储应用程序的凭据且从未将其(例如,通过网页浏览器等)暴露给最终用户时,客户端凭据授权类型是最好的。此授权类型类似于资源所有者密码凭据授权类型,但它不请求用户的用户名或密码。它仅使用服务提供者颁发给您的客户端的客户端 ID 和客户端密钥。

try {

    // Try to get an access token using the client credentials grant.
    $accessToken = $provider->getAccessToken('client_credentials');

} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {

    // Failed to get the access token
    exit($e->getMessage());

}

附加说明

OpenID Connect 发现端点

默认情况下,此客户端使用 .well-known/openid-configuration 端点在提供 authServerUrlrealm 选项以创建客户端后,发现 Keycloak 服务器的所有其他端点。

这由 cloudcogsio\oauth2-openid-connect-discovery 库处理。请参阅 https://github.com/cloudcogsio/oauth2-openid-connect-discovery

// Get the discovered configurations from the provider instance
$discovered = $provider->Discovery();

// Access standard OpenID Connect configuration via supported methods
$issuer = $discovered->getIssuer();
$supported_grants = $discovered->getGrantTypesSupported();
$authorization_endpoint = $discovered->getAuthorizationEndpoint();

// Or overloading for Keycloak specific configuration
$check_session_iframe = $discovered->check_session_iframe;

// Cast to string to obtain the raw JSON discovery response
// All available properties for overloading can be seen in the JSON object.
$json_string = (string) $discovered;

Keycloak 公钥

在端点发现过程中,会检索并本地缓存 Keycloak 域的公钥。这是为了解码访问令牌,然后将解码后的令牌作为附加值添加到 \Cloudcogs\OAuth2\Client\Provider\Keycloak\ResourceOwner 对象中。

公钥缓存

JWK 缓存由 cloudcogsio\oauth2-openid-connect-discovery 安装的 \Laminas\Cache\Storage\Adapter\FileSystem 实例处理。

您可以为 Keycloak 域的公钥存储提供自己的 \Laminas\Cache\Storage\Adapter\* 实例。

令牌内省

默认情况下,使用缓存的公钥在本地解码 accessToken。解码后的数据被填充并可在 \Cloudcogs\OAuth2\Client\Provider\Keycloak\ResourceOwner 对象中获取。

此操作由客户端自动执行,无需额外配置。

通过 Keycloak 服务器进行令牌内省

可以使用 Keycloak 令牌内省端点对 Keycloak 服务器发布的所有令牌(accessToken、refreshToken 等)进行内省。

客户端提供了一个 introspectToken(string $token) 方法来执行此操作。

// Decode the access token
$access_token = $AccessToken->getToken();
$data = $provider->introspectToken($access_token);

// Decode the refresh token
$refresh_token = $AccessToken->getRefreshToken();
$data = $provider->introspectToken($refresh_token);

自定义访问令牌类

此存储库的 custom-access-token 分支实现了一个自定义的 \Cloudcogs\OAuth2\Client\Provider\Keycloak\AccessToken 类,它扩展了基类 \League\OAuth2\Client\Token\AccessToken

Keycloak 提供了 refresh_expires_in 属性。此自定义类添加了检查和检测 refreshToken 有效性的额外方法。其工作原理与基类提供用于检查和检测 accessToken 有效性的工作原理相同。

AccessToken.php

namespace Cloudcogs\OAuth2\Client\Provider\Keycloak;

use League\OAuth2\Client\Token\AccessToken as LeagueAccessToken;

class AccessToken extends LeagueAccessToken
{
    protected $refresh_expires;
    
    public function __construct(array $options)
    {
        parent::__construct($options);
        
        /**
         * Determine if the refresh token expires and set expiry time
         */
        if (array_key_exists("refresh_expires_in", $options)) 
        {
            if (!is_numeric($options['refresh_expires_in'])) {
                throw new \InvalidArgumentException('refresh_expires_in value must be an integer');
            }
            
            $this->refresh_expires = $options['refresh_expires_in'] != 0 ? $this->getTimeNow() + $options['refresh_expires_in'] : 0;
        }
    }
    
    public function getRefreshExpires()
    {
        return $this->refresh_expires;
    }
    
    public function hasRefreshExpired()
    {
        $expires = $this->getRefreshExpires();
        
        if (empty($expires)) {
            throw new \RuntimeException('"refresh_expires" is not set on the token');
        }
        
        return $expires < time();
    }
}

注意:目前,thephpleague/oauth2-client 的基 AbstractProvider 类不支持自定义 AccessToken 类。

在使用自定义 Access Token 类(如上面提供的类)之前,需要更改方法签名。请参阅 thephpleague/oauth2-client#897

许可协议

MIT 许可协议 (MIT)。有关更多信息,请参阅 许可文件