Skip to content
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

Merged
merged 5 commits into from
Nov 28, 2024

Conversation

julien-nc
Copy link
Member

@julien-nc julien-nc commented Oct 30, 2024

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:

  • The login token is stored in the Php session
  • It is refreshed when needed
  • If it can't be refreshed, we try to reauthenticate (which might be transparent for the user if the IdP session is still alive)
  • We listen to the ExchangedTokenRequestedEvent
  • We exchange the login token when receiving the event and place the result in the event

cc @individual-it @SagarGi

@julien-nc julien-nc added enhancement New feature or request 3. to review labels Oct 30, 2024
@julien-nc julien-nc force-pushed the enh/925/token-exchange-service branch 2 times, most recently from 25e3aba to d27cde4 Compare November 4, 2024 14:39
@SagarGi
Copy link

SagarGi commented Nov 5, 2024

hi @julien-nc thanks for the implementation. i will checkout this PR.

Copy link

@SagarGi SagarGi left a 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);
Copy link

@SagarGi SagarGi Nov 6, 2024

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member Author

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;

Copy link
Member Author

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 {

Copy link

@SagarGi SagarGi Nov 6, 2024

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.

Copy link
Member Author

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;

Copy link

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!

@julien-nc
Copy link
Member Author

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?

Yes and yes

@SagarGi
Copy link

SagarGi commented Nov 7, 2024

@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?
CC @individual-it

@individual-it
Copy link

@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? CC @individual-it

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.

@DaphneMuller
Copy link

@SagarGi @juliusknorr any chance this can be reviewed soon?

['provider_id' => $loginToken->getProviderId()],
);
return new Token($tokenData);
} catch (\Exception|\Throwable $e) {

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"
}

Copy link
Member Author

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.

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');
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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') {
Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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 👍

Copy link
Member

@juliusknorr juliusknorr left a 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

@julien-nc julien-nc force-pushed the enh/925/token-exchange-service branch from 572e928 to 803ff21 Compare November 27, 2024 10:13
@julien-nc julien-nc force-pushed the enh/925/token-exchange-service branch from 803ff21 to fba2265 Compare November 27, 2024 10:31
@nabim777
Copy link

nabim777 commented Nov 28, 2024

I have reviewed on behalf of @SagarGi . After checking manually the PR, it meets the expectations and requirements. LGTM 👍

@julien-nc julien-nc merged commit d18772d into main Nov 28, 2024
43 of 45 checks passed
@julien-nc julien-nc deleted the enh/925/token-exchange-service branch November 28, 2024 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3. to review enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants