-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement token exchange mechanism for other apps #974
Conversation
25e3aba
to
d27cde4
Compare
hi @julien-nc thanks for the implementation. i will checkout this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @julien-nc I manually checked the PR.
I have a question for this implementation.
As far as i have checked it you are implementing as, when ever we need an exchanged token we need to emit the event?
And if we do so then i think every time we require an exchanged token we always generate a new one?
$bodyArray, | ||
['provider_id' => $loginToken->getProviderId()], | ||
); | ||
return new Token($tokenData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To create Object of Token the $tokenData
must have the id_token
key. But after a token is exchanged the response does not have id_token
(as far as i have tried for OIDC provider keycloak
) included leading to throw an error. And also do we even require to save id_token
? AFAIK it is only needed for authentication?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm in my tests the exchanged token has the same "shape" as the login token, it contains an id token and an access token. So it might depend on the client config on the IdP side. We can make it so the Token object does not require an id_token.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The id_token might be used for anything, not just the login. It contains user info indeed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you try to change line 41 of lib/Model/Token.php
to:
$this->idToken = $tokenData['id_token'] ?? null;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and line 54 to:
public function getIdToken(): ?string {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm in my tests the exchanged token has the same "shape" as the login token, it contains an id token and an access token. So it might depend on the client config on the IdP side. We can make it so the Token object does not require an id_token.
Sure, I think we do not require the id token to be stored for client to client communication for oidc based authentication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and 32 to:
private ?string $idToken;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes i did and its working!
Yes and yes |
@julien-nc We need token for each request i guess does that mean we emit that event and get token every time we make request to targeted client. would not be it too much? |
yes I would do it exactly like that. The integration app emits the event every time it needs the token, that way only the oidc app has to care about expiration time, renewals, caching of tokens etc. |
@SagarGi @juliusknorr any chance this can be reviewed soon? |
['provider_id' => $loginToken->getProviderId()], | ||
); | ||
return new Token($tokenData); | ||
} catch (\Exception|\Throwable $e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, if the audience is invalid, the code throws an error:
Failed to exchange token, error in the exchange request.
It would be better if it throws a more specific error for an invalid audience, as this would make it clearer and more helpful, especially for displaying in the UI.
On API, when the audience is invalid, we get status code 400
and the response:
{
"error": "invalid_client",
"error_description": "Audience not found"
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. The TokenExchangeFailedException
class now has 2 additional methods:
public function getError(): ?string;
public function getErrorDescription(): ?string;
They both return a string only if the exchange request fails and returns a JSON object with the error
and error_description
keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! that's great!
public function getToken(bool $refreshIfExpired = true): ?Token { | ||
$sessionData = $this->session->get(self::SESSION_TOKEN_KEY); | ||
if (!$sessionData) { | ||
$this->logger->debug('[TokenService] getToken: no session data'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be logged at a higher level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it will flood the logs since this one is produced not only when there's a token exchange request but also when checking the login token in Application.php. If the current user is not authenticated via user_oidc, there will never be any session data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right then debug is also fine.
$this->logger->debug('[TokenService] checkLoginToken: user not logged in'); | ||
return; | ||
} | ||
if ($this->config->getUserValue($currentUser->getUID(), Application::APP_ID, 'had_token_once', '0') !== '1') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I miss understanding on why we need this extra user value check, maybe you can elaborate on that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user never had any login token before, it means it's not using user_oidc to authenticate so we know we can skip the login token check.
Do you mean that it is enough to just do nothing if there is no login token in the session?
We need to know if there is no login token available
- because user_oidc was not used to log in (we can silently skip the check)
- OR because the session has died/disappeared (then we need to reauthenticate)
Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, makes sense then 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two minor comments, otherwise that code and general structure looks good.
Actually this is nice as it could simplify logic in https://github.com/nextmcloud/nmc_spica/blob/main/lib/Service/TokenService.php where a similar thing has been done
…eep it fresh Signed-off-by: Julien Veyssier <[email protected]>
Signed-off-by: Julien Veyssier <[email protected]>
Signed-off-by: Julien Veyssier <[email protected]>
Signed-off-by: Julien Veyssier <[email protected]>
572e928
to
803ff21
Compare
…token exchange fails Signed-off-by: Julien Veyssier <[email protected]>
803ff21
to
fba2265
Compare
I have reviewed on behalf of @SagarGi . After checking manually the PR, it meets the expectations and requirements. LGTM 👍 |
Some integration apps (like integration_openproject) might need to make backend-to-backend requests to other services that use the same IdP as Nextcloud. If those services accept Oidc access tokens as a Bearer tokens for authentication, they might only accept the ones that were delivered for them (for their audience).
Token exchange is a mechanism to exchange a token that was delivered for an audience (for a specific Oidc client) against another token for another audience.
The idea here is to keep the token exchange logic in user_oidc so it saves the effort to reimplement it in all the apps that need it.
In short:
ExchangedTokenRequestedEvent
cc @individual-it @SagarGi