Dex: server: add option to turn off refresh token rotation

Created on 25 Jan 2017  路  7Comments  路  Source: dexidp/dex

Hi,

There are known problem with refresh token's usage that can be reproduced in following scenarios:
1) Client tries to refresh token and IDP successfully refreshes and generates new tokens, but client fails to accept tokens for example because of network issue or client failure.
2) Client of the IDP is some kind of User Agent, like smart device, browser or so and it makes a lot of concurrent requests to Resource Server. At some time Client determines that he need to refresh tokens and it possibly can do several refresh requests at the same time (for example in different browser tabs) and one will fail.

We have faced with such issues in production and we decided to make refresh tokens be usable several times (with some expiration period that starts when first refresh is done). This approach allows to handle issues from client side at it is not contradicts with OAuth2/OIDC1 RFC's.

Most helpful comment

I have a similar use case:

  1. Kubernetes Dashboard with oauth2-proxy
  2. kubectl with oidc config

They are always invalidating the refresh tokens of the other one, making it really a pain to use.

+1 for configuring the amount of active refresh tokens

All 7 comments

Dex intentionally does this per OAuth2 security recommendations[0]. It helps identify when refresh tokens are stolen.

I'll note that browsers can't (or shouldn't) take out refresh tokens. They should be using the implicit flow which only grants a bearer token.

I could see arguments for wanting to turn this off though. If someone wants to send a PR I wouldn't be opposed as long as it's feature flagged. Logic is here[1].

[0] https://tools.ietf.org/html/rfc6819#section-5.2.2.3
[1] https://github.com/coreos/dex/blob/48fcf66a35fd01a54daa27ddbde4df7885533a7c/server/handlers.go#L793-L808

We're constantly facing the same problems as in starting post. We solved 2) by using a lock on backend, so first connection does refresh, and consequent connections are waiting for it and are served once the refresh is finished.
But the 1) is still the pain, because dex may refresh token, but we don't know about it because of network problems, so users are getting log out. Is there a roadmap to solve an issue somehow?

This is a problem even in the simple case that you use different clients locally. For example, we have some people that use Lens with a Custom Config (it had some issues using OIDC with kube/config). Eventually either kubectl or Lens will request a new token and cause the other client to be locked out.

Being able to configure X number of active refresh tokens would be very helpful.

I have a similar use case:

  1. Kubernetes Dashboard with oauth2-proxy
  2. kubectl with oidc config

They are always invalidating the refresh tokens of the other one, making it really a pain to use.

+1 for configuring the amount of active refresh tokens

It is possible to use kubectl and dashboard together without increasing the refresh tokens amount. Dex allows only one session for one pair of user - static client. Use different users' credentials for kubectl and dashboards is insane, thus we will use different static clients.

Kubernetes documentation tells us, that to authenticate through OIDC we need to provide value for the --oidc-client-id flag. It will then accept only tokens that are issued by this static client. Kube-apiserver extracts client id from audience claim of id token.

Example:

{
  "iss": "https://dex..../",
  "sub": "...",
  "aud": "kubernetes",
  "exp": 1603294072,
  "iat": 1603294042,
   ...
}

According to RFC, the audience claim can be either string or array. Is it ok? How can the token be issued by two clients?
There is a mechanism of trusted peers and the special scope audience:server:client_id:( client-id ) to do it in Dex.

You need to register three static clients in Dex:

  1. For Kubernetes itself
- id: kubernetes
  redirectURIs: []
  name: 'Kubernetes'
  trustedPeers:
  - dashboard
  - kubectl-config-generator
  1. For the dashboard and oauth2-proxy
- id: dashboard
  redirectURIs: 
  - dashboard.example.com/oauth2proxy/callback
  name: 'Dashboard'
  1. For your kubectl configs generator
- id: kubectl-config-generator
  redirectURIs: 
  - kubectl-gen.example.com/callback
  name: 'Kubectl'

After that, you need to set up applications:

  • Kubernetes apiserver flags:
--oidc-client-id=kubernetes
--oidc-issuer-url=https://dex.example.com/
--oidc-username-claim=email
  • OAuth2 proxy flags:
--provider=oidc
--client-id=dashboard
--redirect-url=https://dashboard.example.com
--oidc-issuer-url=https://dex.example.com/
--set-authorization-header=true
--scope=groups email openid offline_access audience:server:client_id:kubernetes
  • Kubectl config generators settings depend on what you use in your environment.

Now tokens issued by kubectl-config-generator and dashboard static clients will look like in the example below, so Kubernetes will trust them.

{
  "iss": "https://dex..../",
  "sub": "...",
  "aud": ["kubernetes", "dashboard"],
  "exp": 1603294072,
  "iat": 1603294042,
   ...
}

I hope that this post will be useful and understandable. Feel free to correct me.

@nabokihms Thats it! A concise solution which works with the existing codebase. I have just tried it and I can report a success. Finally I can issue tokens which don't have to be valid for a year ;-)

Unfortunately, it will not solve the original post's scenario, but consider @longwa and my questions answered.

Definitely. There are three ways to solve concurrent request problems I thought about:

  • Use a lock on the client-side to make only one refresh request (like using Redis storage for oauth2-proxy, it locks your request because of its single-threaded nature).
  • Use a lock on the storage side and respond with the same refresh token to all concurrent requests
  • Do not update refresh tokens on every request

I don't think it is good to completely turn off rotation, but maybe we can add an option to switch to time-based rotation instead? Like rotate refresh token every day, or even invalidate them. It reduces security but gives some advantages.

  • You can use any storage you want (even Kubernetes)
  • You don't need to change client-side code

It's not a big deal for isolated environments and it will solve community pain.

Was this page helpful?
0 / 5 - 0 ratings