facile-it / php-openid-client

PHP OpenID Client
36 stars 7 forks source link

[Question] Basic usage / sample code fails #18

Closed killua-eu closed 3 years ago

killua-eu commented 3 years ago

Hey,

I retested the 0.2 release. It seems I get lost along the lines that get the access token.

// Get access token

/** @var ServerRequestInterface::class $serverRequest */
$serverRequest = $request; // passing slim's default $serverRequestInterface from nyholm 
$callbackParams = $authorizationService->getCallbackParams($serverRequest, $client);
$tokenSet = $authorizationService->callback($client, $callbackParams);
$idToken = $tokenSet->getIdToken(); // Unencrypted id_token
$accessToken = $tokenSet->getAccessToken(); // Access token
$refreshToken = $tokenSet->getRefreshToken(); // Refresh token

$claims = $tokenSet->claims(); // IdToken claims (if id_token is available)

//print_r($idToken);
//rint_r($accessToken);
//print_r($serverRequest);
print_r($callbackParams); // returns `array()`
print_r($tokenSet); // returns `Facile\OpenIDClient\Token\TokenSet Object ( [attributes:Facile\OpenIDClient\Token\TokenSet:private] => Array ( ) [claims:Facile\OpenIDClient\Token\TokenSet:private] => Array ( ) ) `
print_r($claims); // returns `array()`
die();

Any ideas why this might be failing? Working against keycloak 12.04

thomasvargiu commented 3 years ago

Probably the default server response mode is fragment, you should ask for parameters in query string when the IdP redirectes to your app, because fragment can't be read on the server side.

So you auth request should include response_mode=query:

$redirectAuthorizationUri = $authorizationService->getAuthorizationUri(
    $client,
    ['response_mode' => 'query'] // include it
);

Try this and let me know.

killua-eu commented 3 years ago

Yep, this helped a bit :) Is there a complete login / refresh / logout example somewhere? I crammed all the code below under a single path

public function keycloak($request, $response) {

    $oid['server']               = 'https://id.lxd';
    $oid['realm']                = 't1';
    $oid['user']['name']         = 'x';
    $oid['user']['pass']         = 'x';
    $oid['client']['id']         = 'new-client';
    $oid['client']['secret']     = '*********';
    $oid['uri']['base']          = $oid['server'] . '/auth';
    $oid['uri']['realm']         = $oid['uri']['base'] . '/realms/' . $oid['realm'];
    $oid['uri']['admin']         = $oid['uri']['base'] . '/admin/realms/' . $oid['realm'];
    $oid['uri']['authorization'] = $oid['uri']['realm'] . '/protocol/openid-connect/auth';
    $oid['uri']['accesstoken']   = $oid['uri']['realm'] . '/protocol/openid-connect/token';
    $oid['uri']['userinfo']      = $oid['uri']['realm'] . '/protocol/openid-connect/userinfo';
    $oid['uri']['logout']        = $oid['uri']['realm'] . '/protocol/openid-connect/logout';
    $oid['uri']['discovery']     = $oid['uri']['realm'];
    $oid['uri']['jwks']          = $oid['uri']['realm'] . '/protocol/openid-connect/certs';

$issuer = (new IssuerBuilder())->build($oid['uri']['discovery']);
// print("<pre>".print_r($issuer,true)."</pre>");
$clientMetadata = ClientMetadata::fromArray([
    'client_id' => $oid['client']['id'],
    'client_secret' => $oid['client']['secret'],
    'token_endpoint_auth_method' => 'client_secret_basic', // the auth method to the token endpoint
    'redirect_uris' => [
        'https://glued.lxd/keycloak-login',    
    ],
]);
//print("<pre>".print_r($clientMetadata,true)."</pre>");

$client = (new ClientBuilder())
    ->setIssuer($issuer)
    ->setClientMetadata($clientMetadata)
    ->build();
//print("<pre>".print_r($client,true)."</pre>");

// Authorization
$authorizationService = (new AuthorizationServiceBuilder())->build();
//print("<pre>".print_r($authorizationService,true)."</pre>");

$redirectAuthorizationUri = $authorizationService->getAuthorizationUri(
    $client,
    ['response_mode' => 'query'] // include it
);
print("<pre>".print_r($redirectAuthorizationUri,true)."</pre>");

$redirectauthorizationService = (new AuthorizationServiceBuilder())->build();
//print("<pre>".print_r($redirectauthorizationService,true)."</pre>");

// you can use this uri to redirect the user
header('Location: '.$redirectAuthorizationUri);

// Get access token
/** @var ServerRequestInterface::class $serverRequest */
$serverRequest = $request; // get your server request
$callbackParams = $authorizationService->getCallbackParams($serverRequest, $client);
$tokenSet = $authorizationService->callback($client, $callbackParams);

$idToken = $tokenSet->getIdToken(); // Unencrypted id_token
$accessToken = $tokenSet->getAccessToken(); // Access token
$claims = $tokenSet->claims(); // IdToken claims (if id_token is available)
$refreshToken = $tokenSet->getRefreshToken(); // Refresh token

// print_r($callbackParams);
//print_r($tokenSet);
print_r($claims);
//print_r($idToken);
//rint_r($accessToken);

die('dieing a bit');
// Refresh token
$tokenSet = $authorizationService->refresh($client, $tokenSet->getRefreshToken());

// Get user info
$userInfoService = (new UserInfoServiceBuilder())->build();
$userInfo = $userInfoService->getUserInfo($client, $tokenSet);
}

When I die(), I get redirected to the auth page and I get to print the $claims. Uncommenting the refresh token part fails with

Argument 2 passed to Facile\OpenIDClient\Service\AuthorizationService::refresh() must be of the type string, null given, called in /srv/varwwwhtml/oidtest/Core/Controllers/AuthController.php on line 158
thomasvargiu commented 3 years ago

You can't have those value at that time, I suggest you to read something on how OpenID Connect works.

You need to pages, one to redirect the user to the login page, and another one where Keycloak will redirect your user after the login with the code to request an access code.

So, when you have your configured services, you should have two functions:

Login page:

function login(AuthorizationService $authorizationService, Client $client)
{
    $redirectAuthorizationUri = $authorizationService->getAuthorizationUri(
        $client,
        ['response_mode' => 'query']
    );
    header('Location: ' . $redirectAuthorizationUri);
    exit;
}

The user will be redirect to the SSO login page, and if he's not logged in, the SSO will ask him to login, then the user will be redirect to your redirect uri (callback) of your app:

Redirect page:

function callback(AuthorizationService $authorizationService, Client $client, ServerRequestInterface $request)
{
    $callbackParams = $authorizationService->getCallbackParams($request, $client);
    $tokenSet = $authorizationService->callback($client, $callbackParams);

    $idToken = $tokenSet->getIdToken(); // Unencrypted id_token
    $accessToken = $tokenSet->getAccessToken(); // Access token
    $claims = $tokenSet->claims(); // IdToken claims (if id_token is available)
    $refreshToken = $tokenSet->getRefreshToken(); // Refresh token

    // All the previous variables should be populated now
}

This is where the SSO redirect the user including che callback params. Now you have your code to request the access token with a server-to-server request to the SSO.

killua-eu commented 3 years ago

Hey @thomasvargiu , my bad, I should not attempt to do anything meaningful late in the night. I split up things accordingly. I'm getting an exception The following claims are mandatory: at_hash. (0) (that bubbles up as Facile \ JoseVerifier \ Exception \ InvalidTokenException, Invalid token provided). the at_hash really isn't issued by keycloak in the refresh token. The only relevant info I found concerning this was https://stackoverflow.com/questions/60818373/configure-keycloak-to-include-an-at-hash-claim-in-the-id-token ... not sure at which point/how to insert the response_type=id_token token ... when I slipped this into

    $redirectAuthorizationUri = $authorizationService->getAuthorizationUri(
        $this->oidc, ['response_type'=>'id_token token']);

, I got the nonce MUST be provided for implicit and hybrid flows exception. Using ['response_type'=>'id_token token use_nonce'] didn't help. I'm at loss now again.

killua-eu commented 3 years ago

To be specific, the at_hash exception comes with $tokenSet = $authorizationService->refresh($client, $refreshToken);

thomasvargiu commented 3 years ago

Ok, right.

Try this:

$redirectAuthorizationUri = $authorizationService->getAuthorizationUri($this->oidc, [
    'response_mode' => 'query'
    'response_type' => 'code id_token token'
]);

With this parameters you should have all you want and it should work. But it depends on what you need.

If you require id_token in the response_type the Identity Provider (IdP) will provide you an id_token and you can use $tokenSet->claims() to read the user infos in the id_token JWT without to ask to the UserInfo service.

If you only want to authenticate an user (and then maybe open a session) you just need code id_token.  code is to enable the code authorization flow (the default one and one of the most secure flow), id_token is to receive the JWT with the user info without to request these infos from another service (like Keyclock itself).

Requiring token in the response_type you're asking for an access token (and refresh token).