sebadob / rauthy

OpenID Connect Single Sign-On Identity & Access Management
https://sebadob.github.io/rauthy/
Apache License 2.0
307 stars 17 forks source link

feature request: Add the ability to fetch the `userinfo` endpoint with the `rauthy-client` #359

Closed kerkmann closed 4 months ago

kerkmann commented 5 months ago

Our backend would like to have the given_name, family_name and email, which aren't in the access_token. For that reason we would like to use the rauthy-client to fetch the userinfo from the /auth/v1/oidc/userinfo endpoint with the given bearer token. Does it make sense to implement something like that? The best approach would be an axum extractor, like the PrincipalOidc, which is doing the get request in the background.

sebadob commented 5 months ago

Yes that's exactly what the /userinfo is for.
It would be require one less network roundtrip though, if you take the information directly from the id_token, which includes all of this by default, if the client requested the correct scope during sign in. Especially when you need these more often, I would maybe think about fetching this information only once directly after the login and then store it in a cookie or something like that.

Edit:

If you only want to fetch the userinfo, it makes no sense to add the rauthy-client just for that purpose. The request is really simple - just add the access_token as Bearer in the Authorization header. You don't need more than that. If you use the rauthy-client anyway already, it will return the claims from the id_token, so you don't need the additional fetch.

kerkmann commented 5 months ago

The idea was more to outsource duplicate code. What you need to to is get that bearer token, do the get request and evaluate/parse it. If you have more endpoints which are using the userinfo it can be annoying.

I would not fetch the userinfo when the user is just using the PrincipalOidc. But if we have a seperate struct like UserinfoOidc, then the axum extractor could do the handling in the background (and in the future caching). :)

wdyt?

edit: Btw; as far as I know we also have no ability to get the Idtoken easily in the rauthy-client (or at least we have no validation). :D

sebadob commented 5 months ago

edit: Btw; as far as I know we also have no ability to get the Idtoken easily in the rauthy-client (or at least we have no validation). :D

Yes you do. The full id claims with additional data are being returned when a client does a callback:

https://github.com/sebadob/rauthy/blob/9f87af3dfb49b48300b885bf406f852579470193/rauthy-client/examples/axum/src/routes.rs#L65

The raw id_token is basically thrown away afterwards, because you usually don't need it anymore and do custom stuff for each app depending on the ID claims. You could serialize some data and put it inside an encrypted cookie. Then you would have everything available each time without additional lookups.
But, this is totally different how each app handles this.

The only thing we could do is maybe add a function for an existing PrincipalOidc like fetch_userinfo() which would do the request, but it would not avoid that much duplicate code, since you should avoid unnecessary network round trips. Adding this is pretty simple though, so yes we can do this.

Edit:

You could also of course set the full id_token as a cookie value when the callback returns it, so this could be fetched in an extractor each time as well. It is included in the returned token_set above.

kerkmann commented 5 months ago

Ah I understand. The problem right there is, we are not using the cookie in our application. Our flow is that our client is doing the authentication with a public client and stores the access token and id token in the local storage. Our backend doesn't know something about the frontend client.Then the frontend client is just using the access token for the backend requests. It's not doing the entire authorization circle again and in that case not storing a cookie. ^^"

That is our current implementation and the easiest one. But maybe it makes sense to do the auth flow again and store the cookie as well, I thought it would be the easiest and best solution there. :D

Edit: Our backend is really just a stupid axum backend, it's fetching the JWKS and validates it, no callback stuff is happening there. :3

sebadob commented 5 months ago

I see. I would not do an additional fetch in the backend. This would be wasted resources and a really bad UX. Usually, public clients are only used by SPA's that are simply not able to implement any backend code. If you would use a confidential client, all of this would actually be easier than the current approach imho.

If you want to stick with that flow, then yes you even are forced to do another roundtrip to userinfo at least once.
Another solution would be, that the client sends the id_token in an additional header as well, but then you have to validate it against the access token and make sure they are coming from the exact same auth flow to not introduce security issues. All of this would not be necessary with a confidential client, because you know exactly, that they match, when you get them from the /token endpoint and you have additional in-depth security because of the client secret.

For your use case though, where your backend is serving requests for a public client, it makes sense to add a fetch_userinfo() fn for the principal, absolutely.

kerkmann commented 5 months ago

One final question, just to be sure.

Imagine I have a Application (let's call it frontend), which is capable of doing server side rendering and also capable of doing hidden backend stuff.

And I have another application called Shop, which is just a rest backend api, and is of course capable of doing hidden backend stuff.

Then I could authenticate with the frontend with credentials and also write a secret cookie for id token validation, right? That also means that the shop could use the same secret cookie for the id token, right? Would both clients need separate client credentials (which I think should be the case)?

And also one last question: That architecture would make it more secure, but the entire REST api would then be bounded to the browser. The idea behind a RESTful api is to be independent of the browser (concrete the cookie itself). This would mean that the full state is held by the client (which in my opinion is a big negative impact for a RESTful backend).

Source: https://softwareengineering.stackexchange.com/questions/141019/should-cookies-be-used-in-a-restful-api

Thank you very much for your time and I appreciate it! :heart:

sebadob commented 5 months ago

Then I could authenticate with the frontend with credentials and also write a secret cookie for id token validation, right? That also means that the shop could use the same secret cookie for the id token, right?

That depends on your environment and your cookie settings.
You should always use Lax and where possible Strict cookies. When your different APIs and backends are running behind a reverse proxy and have the same origin, then yes you can access the cookies from each other.

Would both clients need separate client credentials (which I think should be the case)?

No. If they are running in different environments, you could improve the security a little bit in case your secret leaks at one place, so the other client is still "safe". But then you would have to use 2 different clients which means you need to maintain 2 different access tokens, because the aud / azp claim contains the client_id and this MUST always be validated. Without this validation, you could for instance use a valid access token which was meant for a completely different client. You must never skip this validation, which would fail, if you have separate clients.

And also one last question: That architecture would make it more secure, but the entire REST api would then be bounded to the browser. The idea behind a RESTful api is to be independent of the browser (concrete the cookie itself). This would mean that the full state is held by the client (which in my opinion is a big negative impact for a RESTful backend).

As so often, that depends.
For instance Rauthy's admin API is fully secured by a session cookie + CSRF token from local storage. This means it is bound to the browser by this definition. But, each endpoint can decide dynamically, if it uses the session cookies, or maybe an API Key from an Authorization header. So it really depends on how you build your API.

From a technical standpoint, Cookies are the most secure thing we can use these days for authentication from the browser. In the backend this does not matter. But even in the backend you can simply append a Cookie to your request, if you like, so your API would not be "bound" to the browser, even if it requests cookies. Rauthy's integration tests are a good example. I am not using a headless browser or something like that to test everything, only standard Rust test functions. If you append your data correctly, you have no issues.

Only using a token from a local storage can be attacked easily, if your user has a malicious browser extension. These can access the local storage for each website your are visiting, which is a security nightmare imho. This does not apply to the session storage, but saving your token there means you need to do a fresh login each time you close the browser / tab.

The only really safe method that needs at least multiple vulnerabilities is using secure http only cookie + csrf token from local storage appended to request headers with JS / WASM.

At the end of the day, a cookie being sent to the backend is only another HTTP header value.

kerkmann commented 5 months ago

Thanks, that was a really good answer. I thought a lot about the architecture and on long term we will use SSR with cookies. But right now our application is most of the time a standard SPA (we do some SSR, but that's just for SEO stuff, like a product page). The backend itself is just a REST-Api. Mixing things up wouldn't help. The trade off for more security isn't worth it right now. The REST Backend is completely stateless and the Frontend (which is most of the time a SPA, and only for some endpoints a SSR) scale up quite easily, increasing the worker amount isn't such a big deal there and it's also helping to split between the client and the backend. That's the reason why we choose the local storage. But I'll definitely keep that in mind for the future. :3

But I am open for changes, maybe I still understand it wrong. :D I think my knowledge increased a lot, thanks to you. :)

btw; I think you can assign the issue to me if you like. I would add a fetch_userinfo to the rauthy-client, which shouldn't take too much time. Then this issue could be closed.

sebadob commented 5 months ago

There are ways to use local storage in a stateless way without the risk of evil extensions peeking at your data.

What I did for another application for instance was, that I stored the token encrypted. Then, even if an extension or XSS would be able to grab it, it was basically useless. Then I did a single roundtrip to the backend right after login which generated a random encryption key for me and at the same time, stored it encrypted again in an http only cookie. I would then use this key to store the token encrypted and only keep this key in memory.

When the app would be opened the next day, it only needed to get this key from the backend once with the very first render. It would just read the existing cookie, decrypt it and return my encryption key.
This seems a bit tedious at first, but it made it possible to enhance the security for a pretty huge legacy backend application that just expected the token, but wanted a bit more protection against such stuff. In the end it was way less work to do this than to modify hundreds of existing endpoints.

There are a lot of tricky things and footguns when dealing with such stuff. But for most applications you dont need the highest amount of security. Its always a tradeoff...

btw; I think you can assign the issue to me if you like. I would add a fetch_userinfo to the rauthy-client, which shouldn't take too much time. Then this issue could be closed.

Thanks. I would have done it today, since this is a tiny thing, but I just did not have the time to do it.

kerkmann commented 4 months ago

I am still working on it, sorry if it takes longer than expected. If someone needs them asap, ping me. Otherwise it's low prio. :)