mstilkerich / rcmcarddav

CardDAV plugin for RoundCube Webmailer
GNU General Public License v2.0
258 stars 81 forks source link

Use `id_token` instead of `access_token` to authenticate users #354

Closed azmeuk closed 3 years ago

azmeuk commented 3 years ago

Hi. We can see in the following piece of code that rcmcarddav uses the access_token provided by identity servers to authenticate users against the carddav server:

https://github.com/mstilkerich/rcmcarddav/blob/512bcb61fc7f64bb57ef5f6a521d12a580c4b003/src/Config.php#L149-L153

However some carddav server authentications services, like nextcloud-oidc-login need a JWT so it can check the aud claim that would be expected to find in id_token. Given what you say here I suppose Keycloak set a JWT in the access_token token endpoint response, so your setup works well (Do you confirm?). However canaille set a regular token in access_token (and not a JWT), thus rcmcarddav gives nextcloud a token it cannot read. This seems to be the regular way to do, as the OIDC specification shows.

I suggest detecting the presence of id_token in $_SESSION['oauth_token'] and if present, use it instead of access_token. Currently roundcube is not ready for this, but I have opened a discussion there: https://github.com/roundcube/roundcubemail/issues/8214

I volunteer to provide a patch if it is OK with you.

mstilkerich commented 3 years ago

Hello,

from my understanding of the OAUTH2 and OIDC specs:

From the keycloak setup described by the nextcloud-oidc-login, the ID token will not contain the nextcloud client ID in the intended audience, but the access token will. For your reference, I have added samples of an access token and an ID token below.

So to my understanding, it is correct to use the access token to authenticate with the resource server, not the ID token. You should add the nextcloud client ID to the intended audience of the access token, not the ID token.

Access Token

{
   "acr" : "1",
   "allowed-origins" : [
      "https://forge.mike2k.de"
   ],
   "aud" : [
      "owncloud",
      "nextcloud",
      "account"
   ],
   "auth_time" : 1633095894,
   "azp" : "roundcube",
   "email" : "foo@bar.com",
   "email_verified" : false,
   "exp" : 1633096195,
   "family_name" : "Bar",
   "given_name" : "Foo",
   "iat" : 1633095895,
   "iss" : "http://localhost:8080/auth/realms/forge",
   "jti" : "64eb4d9e-8b90-425e-9cf6-bcaa95d55135",
   "name" : "Foo Bar",
   "preferred_username" : "foo",
   "realm_access" : {
      "roles" : [
         "default-roles-forge",
         "offline_access",
         "uma_authorization"
      ]
   },
   "resource_access" : {
      "account" : {
         "roles" : [
            "manage-account",
            "manage-account-links",
            "view-profile"
         ]
      }
   },
   "scope" : "openid owncloud nextcloud email profile",
   "session_state" : "d6b47813-b6ac-4aa3-9841-2f3ba5fa8c04",
   "sub" : "61a81d0e-e273-4153-8bc9-29424bc70006",
   "typ" : "Bearer"
}

ID Token

   "acr" : "1",
   "at_hash" : "xyz",
   "aud" : "roundcube",
   "auth_time" : 1633097065,
   "azp" : "roundcube",
   "email" : "foo@bar.com",
   "email_verified" : false,
   "exp" : 1633097365,
   "family_name" : "Bar",
   "given_name" : "Foo",
   "iat" : 1633097065,
   "iss" : "http://localhost:8080/auth/realms/forge",
   "jti" : "1f21761f-3f27-4d6a-8aaa-e5d85d6db1b2",
   "name" : "Foo Bar",
   "preferred_username" : "foo",
   "session_state" : "9571afec-4659-45d2-97b9-bfd1a7f8d269",
   "sub" : "61a81d0e-e273-4153-8bc9-29424bc70006",
   "typ" : "ID"
}
mstilkerich commented 3 years ago

Concerning the second part of your question: Yes, keycloak provides a JWT also for the access token (see above). In case of an opaque access token like in your case, the resource server would have to check with the userinfo introspection endpoint to acquire the needed information on the token, particularly the scope / audience in this case.

azmeuk commented 3 years ago

Concerning the second part of your question: Yes, keycloak provides a JWT also for the access token

I think this is the source of the misunderstanding. The access_token being a JWT appears to be a Keycloak convention, but there is nothing in the OAuth specification imposing that the access token should be a JWT, or that it should carry any payload (There is 0 mention of JWT in the rfc6749 and this is illustrated by the OIDC spec example I linked earlier). I understand that JWT are from the OIDC realm, not OAuth.

So I think the safe behavior would be to expect access token NOT being JWT, as it is not required by the specifications, and other identity provider does not necessarily generate JWT access tokens. As nextcloud-oidc-login expects a JWT to authenticate users, I think rcmcarddav should just try to authenticate with the id_token instead of the access_token (or maybe id_token and then access_token if it fails?).

OAUTH2 only knows access tokens. These are used by the client (here: roundcube/rcmcarddav) to authenticate with resource servers (here: nextcloud).

:+1:

OIDC adds an ID token, but this token is only meant to be used by the client application to authenticate with the OIDC provider (e.g. to retrieve user information, access tokens for specific resource servers)

This is my understanding too. ID token can be used for three purposes:

The OIDC spec doesn't say much about resource servers, but it is built on top of OAUTH2. Consequently, the access tokens are used to authenticate with resource servers.

Ok with that. But as I said earlier, token payloads and claims (including aud) are not part of the OAuth specs. So maybe the issue is that nextcloud-oidc-login expects a JWT? Perhaps it could try to read the access token as a JWT, but if this fails, try to get the information about?

The intended audience of an ID token therefore is the client, not a resource service.

Ok, but nextcloud is both a client and a resource service, ain't it? A client because it has its own client ID and authenticate users against the server, and a resource service because it knows about users addressbooks, and can be requested by others clients for those information.

The intended audience of an access token is the resource server(s).

Ok, but as I just said, this should not make a difference here.

So what would you think of trying both id_token and access_token to authenticate users? Or add a configuration option to let the user choose which token should be used for authentication?

mstilkerich commented 3 years ago

I understand that OAUTH2 does not define any semantics concerning the token. I posted the keycloak tokens as a confirmation to your assumption that keycloak provides an access token as a JWS, and to show details on the aud claims contained in id token and access token.

However, with an opaque access token, the resource server has to perform validation of the token by talking to the auth server (here: OIDC provider), either by some proprietary mechanism or using the token introspection endpoint (RFC7662). In case of a JWT, the resource server has the additional option to perform local validation by verification of the JWT signature and content.

Concerning the WebDAV API, the role of nextcloud is that of a resource server. In other constellations, particularly when logging into the nextcloud Web UI, nextcloud would indeed take the role of a client, but not when serving as CardDAV service to a roundcube client. The original supported use case of the nextcloud-oidc-login app was the latter - in that case, it is valid for the app to assume retrieving an ID token in JWT format. However, with the WebDAV API and its role as a resource server, I think it is wrong to expect the access token to be in JWT format.

So IMO the solution would be in the nextcloud-oidc-login app - in case it detects that the provided token is not a valid JWT, it should fall back to querying the token introspection endpoint to retrieve information on the user and verify the validity of the provided token. The used library jumbojett/openid-connect-php includes an API for token introspection, so it might not be a big effort to implement. Maybe @pulsejet / @sirkrypt0 can share his opinion on this.

I'm a bit uneasy concerning the use of the id token for authentication with the CardDAV service just because it works, because I'm not sure about the security implications. The spec says the access token is meant as a means of authorization evidence with the resource servers. Googling for that topics yields answers from "don't use the ID token for auth" to "I think it's okay if the resource server and the client belong to the same realm of trust / are operated by the same party". Concrete concerns I found are related to confidentiality of personal data within the ID token that the resource server might not be authorized to see. Therefore, I would rather not forward the ID token, unless I understand it is a sane thing to do, ideally from official documentation / specs.

Making a configuration option for the end user would shift responsibility, but it also complicates configuration - I guess many end users won't know the difference between the two types of tokens. Also with keycloak, the ID token would not work as the target audience check would fail on the resource server side.

azmeuk commented 3 years ago

I submitted a patch to nextcloud-oidc-login that allows WebDAV authentication with regular access tokens.

For the debugging I tried to use the Force immediate synchronization with server button in the carddav preference panel to force a reload, but I am not sure requests produced this way can authenticate with a token. Can they?

mstilkerich commented 3 years ago

Hello, yes, as long as you are logged into roundcube via oauth, rcmcarddav can use the access token. It has no other way to authenticate with the Carddav server.

If you turn on http debug logging, you can observe the requests in the carddav_http.log

azmeuk commented 3 years ago

When I submitted the preference panel form, in carddav_http.log I can see this kind of messages. No 'Authorization: Bearer' header found. I suppose this form tries to do a Basic authentication instead of a bearer. Maybe it worth adding an option to allow Bearer authentication from this panel?

mstilkerich commented 3 years ago

There is no need for a configuration option, it is supposed to work. You should get an error message if it does not. In the logs, can you check that the request leading to the "No Authorization header found" response is not followed by the same request again, this time with the proper Authorization header? It is normal that there will be a 401 first, to negotiate the supported Auth schemes with the server. But in the next request, rcmcarddav should try with the propere authentication.

mstilkerich commented 3 years ago

I guess this issue can be closed then? If there is something left, please reopen.

azmeuk commented 2 years ago

Hi. Since we last talked, nextcloud-oidc-login now does work with non JWT access tokens. The whole setup is functional, and this is great!

However now I find my identity server receive a lot of requests, and among others, on the introspection endpoint. Using the id_token would allow nextcloud to just check the integrity of the token instead of making a further request to the IDP, and adding some stress to it.

roundcube/roundcubemail#8214 has been fixed in roundcube 1.5.2 so the id_token is available for plugins, and rcmcarddav could take benefit of this.

Therefore, I would rather not forward the ID token, unless I understand it is a sane thing to do, ideally from official documentation / specs.

Is the upstream answer reassuring to you?

Making a configuration option for the end user would shift responsibility, but it also complicates configuration - I guess many end users won't know the difference between the two types of tokens. Also with keycloak, the ID token would not work as the target audience check would fail on the resource server side.

I suggest keeping the current behavior by default, but let advanced users configure this.

The id_token aud claim is documented and it should contain identifier for other clients. I don't really know about Keycloak, but if it chooses to set those identifiers in the access_token aud claim and not in the id_token aud claim, it looks like it is getting away from the spec. I think this is OK for tools in the OIDC ecosystem to adapt on famous IPD implementations, but also that the basic spec should be supported too.

I am still interested in providing a patch.

What do you think?

mstilkerich commented 2 years ago

Hi,

My stance on this

My understanding (from reading the OAUTH2/OIDC spec) is that what OIDC adds on top of OAUTH2 is primarily concerned with providing identity information to the client. This profile information contains extended user information (email address, phone number, address) that is not normally needed by the backend servers and therefore should not be passed.

On the other hand, we have an access token from OAUTH2, which is meant to enable the client to access the granted subset of the user's resources and the resource servers on behalf of the user. Which resources and backend services the access token provides access to can be restricted based on aud and scope claims in the access token.

I see that it is possible to put all claims (e.g. aud/scope) needed for a token to be accepted as an access token by a resource server into the ID token, and therefore use it as an access token. But still I believe this is not the way the tokens are meant to be used. At least, we disclose more information than needed on the personal data of the user to the resource servers.

So I think the proper solution to achieve what you want (avoid token introspection by the resource servers) is to use JWT also for the access token. I understand that your ID server implementation appear to not allow that, but IMO this is what should be addressed, using the ID token to gain access to resources seems like a workaround. Personally, I wonder why the decision was made to use something else than a JWT for the access token representation when you have all the infrastructure / code needed for it already in place for issuing ID tokens. Anyway, in keycloak it is possible to define very precisely the claims contained in an access token issues to a client. Using a JWT for the access token is not moving away from the OAUTH2 specs, since they make no definition of the access token representation. But the JWT specs (RFC 7519) could be used here for the representation of the access token, so I would not see this as the ecosystem adapting to a famous implementation.

Thoughts on your setup

Why are you so focused on rcmcarddav concerning the request load? Depending on the configured refresh time (you can define this as an admin in the config), there should only be occasional communication to the CardDAV server from roundcube. I would expect a lot more communication between roundcube and the IMAP server, and there also roundcube will use the access token and not the id token for authentication.

Thoughts on an option in rcmcarddav

While it is now possible for rcmcarddav to access to ID token, it is not as simple as using the access token. Roundcube keeps the access token in the session, so rcmcarddav can access it whenever needed. The ID token is not kept in the session but only provided to the plugin when the token is acquired from the ID server. So rcmcarddav would have to take care of storing the token somewhere itself. Apart from that, I am frankly reluctant to add new configuration options to the code base, which I am trying to move to a state where pretty much all of the features are also tested. This would be a feature requiring a special test setup in the test environment. So all in all, it would be a lot of work for what I believe is a workaround for the inability of your ID server to use JWT for the access token, which is what you should have if you want to use localized token validation.