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

Support for openid connect SSO and Bearer Tokens #6494

Merged
merged 25 commits into from
Nov 9, 2022

Conversation

davidblasby
Copy link
Contributor

@davidblasby davidblasby commented Aug 9, 2022

This PR adds support for OAuth2 OpenId Connect Single Sign-on and also support for OIDC Bearer tokens.

This has been tested with Azure AD and with KeyCloak (backed by Azure AD). See the documentation changes in geonetwork/doc#201.

  1. Support for "normal" OIDC login. See documentation for details.
  2. Support for "Bearer Tokens". See documentation for details. This allow the use of the OIDC Access Token to be used by API calls from Desktop or Web Services. Simple attach the token to the header of your requests.

try {
URI redirectUri = new URI(redirectURL);
if (redirectUri != null && !redirectUri.isAbsolute()) {
response.sendRedirect(redirectUri.toString());

Check warning

Code scanning / CodeQL

URL redirection from remote source

Potentially untrusted URL redirection due to [user-provided value](1).
username = org.apache.commons.lang.StringUtils.left(username, 256); //first max 256 chars
}

if (username != null && username.length() > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to !org.apache.commons.lang.StringUtils.isBlank(username)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed (new commit)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed - new commit.

username = org.apache.commons.lang.StringUtils.left(username, 256); //first max 256 chars
}

if (username != null && username.length() > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to !org.apache.commons.lang.StringUtils.isBlank(username)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

* <p>
* NOTE: to be consistent with the rest of Azure, we use the Groups OID (guid) NOT its name.
*/
public class MSGraphUserRolesResolver implements UserRolesResolver {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class seems not using the internet proxy configuration if a proxy server is configured, to review.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm - I don't think the internal spring library is using the proxy either.

I don't think I can support that right now - and there's insufficient examples in the code base.

will not change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josegar74 - no action on this. Not sure what to do...

@ianwallen
Copy link
Contributor

@davidblasby
I have tried to make this work on my localhost and I can get it to authenticate with keycloak however it always returns 401. I don't have time to fully debug the issue at this time but here is my configuration - maybe you will see something simple that I missed.

This is the configuration for the environment variables in intellij.
image

And this is an example of the resource access that exists in my keycloak token (This is how it was setup when I was using keycloak drivers)

  "resource_access": {
    "catalogue": {
      "roles": [
        "Administrator",
        "a:Reviewer",
        "b:Editor",
        "a:Editor",
        "b:Reviewer"
      ]
    }
  },

believe the issue may be related to the roles I have assigned?
I don't understand the role assignment in the new openidconnect plugin.

Documentation says to use the following.

#. On the `Roles` tab, create some roles: Administrator, Editor, Reviewer, RegisteredGuest

I understand how RegisteredGuest and Administrator work as those roles are not dependent on the groups that a users is assigned to. But Editor and Reviewer are dependent on the groups that the user is assigned to so I don't understand what it means to be an "Editor" in this context. Generally geonetwork would use the highest role for the groups. i.e. if a user was a editor on group a and a reviewer on group b then it would identify the user as a reviewer. And if the user was an editor on group a and a registeredUser on group b then they would be identified as editor.
But with the new logic, if a user is an Editor on group a and the roles tab identifies the user as a Reviewer then will the system show the Reviewer options and fail because the user is not really a reviewer for any group?

@davidblasby
Copy link
Contributor Author

davidblasby commented Sep 9, 2022

@ianwallen - RE: you get 401s

This should work with the "a:Editor" - in the same way as the old keycloak provider (I reused the code there). I haven't done any extensive testing of it, however. You should still be able to login and shouldn't be getting 401s.

I could be giving the incorrect permissions, but I give the user the "normal" permission (like RegisteredUser and Editor) and then give them the group permissions. Most of that is in the OIDCRoleProcessor.

Perhaps we can share a screen and debug next week?

Perhaps @josegar74 can confirm, but I think there's two sets of permissions. The "generic" permissions, and then group-specific permissions. Jose - can you explain how to the permissions system works for group (and non-group) permissions?

@davidblasby
Copy link
Contributor Author

@ianwallen - added the OPENIDCONNECT_SERVERMETADATA_CONFIG_URL environment variable (to download the config JSON from idp server on GN startup) and updated doc.

I this this just needs a review by @josegar74 (see tags above).

@ianwallen
Copy link
Contributor

@davidblasby , I was able to finally get the authentication to work here are the issues that I have encountered.

you have the following in the documentation.

#null/empty -> use all in server
openidconnect_Configuration.scopes=${OPENIDCONNECT_SCOPES:#{''}}
If I don't change the setting, I get the 401 error. I had to change this to "openid profile email" for it to work in my configuration - I have not attempted different combinations. But I'm assuming that if I only have openid then it may not be able to get the profile information.

The other issue I had was that the role assignments are based on the IdToken instead of the AccessToken. My keycloak configuration did not have the roles in the IdToken and based on the following https://auth0.com/blog/id-token-access-token-what-is-the-difference/
image
it seems like the role should be in the accessToken (default in keycloak) because it is used to determine if the user has access to something. I was able to configure keycloak to include the role in the idToken but it is not added in the idToken by default - which tells me that it probably should not be using the roles from the idToken and it should check the accessToken instead.

@davidblasby
Copy link
Contributor Author

@ianwallen

Hi, I don't think that roles should be in the Access Token. In fact, you shouldn't ever parse the access token - it doesn't have to be a JWT. It's only required to be readable/usable by the server that produced them.
NOTE: the Bearer token does assume that the parse the access token as a JWT, but just for validation purposes.

Azure AD doesn't put roles in the access token - you can request that it puts them in the ID Token (see my documentation - you have to turn this on inside Azure). Since, for Bearer-token-only access (which is an Azure-generated AccessToken), I use the MSGraphRolesResolver (graph.microsoft.com) to get roles.

I also included UserInfoAccessTokenRolesResolver, which allows you to get roles from the userinfo endpoint - which is how I use bearer tokens from keycloak.

There's also code there for getting roles from an ID token.

I should probably default the scopes (OPENIDCONNECT_SCOPES) to "openid profile email".

@davidblasby
Copy link
Contributor Author

I updated so the default scopes are "openid email profile" (norma), and "openid email profile offline_access" (for bearer tokens).

@davidblasby
Copy link
Contributor Author

@ianwallen @josegar74 - I think this is ready to merge???

@josegar74
Copy link
Member

@davidblasby

Perhaps @josegar74 can confirm, but I think there's two sets of permissions. The "generic" permissions, and then group-specific permissions. Jose - can you explain how to the permissions system works for group (and non-group) permissions?

  • Administrator permission is global (not assigned per group)
  • Other permissions are applied per group. GeoNetwork doesn't support to be for example Editor in all groups.

@ianwallen
Copy link
Contributor

@ianwallen

Hi, I don't think that roles should be in the Access Token. In fact, you shouldn't ever parse the access token - it doesn't have to be a JWT. It's only required to be readable/usable by the server that produced them. NOTE: the Bearer token does assume that the parse the access token as a JWT, but just for validation purposes.

I'm not an expert? I tried to find information on best practices for group token and I only found the following.
https://devforum.okta.com/t/best-practices-groups-claim-in-id-token-or-access-token/6532

Azure AD doesn't put roles in the access token - you can request that it puts them in the ID Token (see my documentation - you have to turn this on inside Azure). Since, for Bearer-token-only access (which is an Azure-generated AccessToken), I use the MSGraphRolesResolver (graph.microsoft.com) to get roles.

I also included UserInfoAccessTokenRolesResolver, which allows you to get roles from the userinfo endpoint - which is how I use bearer tokens from keycloak.

I don't understand why you seem to indicate AD doesn't put the roles in the access token. To my knowledge it does not put it in any token unless configured as shown here
https://docs.cribl.io/stream/3.3/usecase-azure-ad/#configure-token-and-claims. It can be added to ID token, Access token or SAML.

I have not used the role token from Azure so I may not be understanding your configuration.

We currently use keycloak, and I would have expected that I could simply changed geonetwork configuration from the keycloak drivers to the this openidconnect driver without making any changes to the keycloak configuration however that was not the case. If you are correct that the ID token should be used, then does this mean that keycloak configuration has a bug because it uses the access token?

It is odd that the keycloak drivers defaults to using the access token but the oauth driver default to using the id token. I'm not sure if there should be a configuration parameter for both keycloak and openidconnect drivers indicating where the group token comes from so that it is more clear otherwise I think this can get confusing.

@davidblasby
Copy link
Contributor Author

I'm not an expert? I tried to find information on best practices for group token and I only found the following. https://devforum.okta.com/t/best-practices-groups-claim-in-id-token-or-access-token/6532

From that group;

If your application calls your own API endpoints using the access token, then it’s best to have the groups claim inside the access token, so that the endpoints will check the user’s permissions prior to displaying the response.
If your application check the user’s permissions prior to creating the local session, then it’s best to have the groups claim inside the ID token so that the session is created successfully and user is granted proper permissions.

We're using the permissions in a local session, so we should be using the ID token.

I tried looking at those instructions (azure ad) for putting the groups in the access token - I tried a long time ago (many many times) and couldnt get it to work, and I tried today and wasn't able to get then in the access token. Perhaps it only works for oauth2 requests (which don't have an ID token) or for saml requests of some type. Most of the online documentation for azure ad oidc follows what I put in the GN documentation for setting up Azure AD - modify the manifest to get it to put tokens in the ID token.

I think the keycloak driver is specialized to talk to keycloak and does whats easiest for keycloak. If you look at what the keycloak driver is doing, it basically doing an openid request to keycloak. I expect its putting the groups in the access token so it easier to work with with oauth2 (i.e. not oidc - and only has an access token). Since its controlling both the server and client, it can do anything it wants.

I could modify it so that it could grab roles from the access token - but that's probably non-trivial todo. And it would make sense to allow use of the other providers (userinfo, MS Graph) as well.

I can put a big note in the documentation saying that it should be using the ID token for roles. However, i think its pretty explicit (it one of the first things mentioned). The Azure AD and Keycloak sections both explain how to setup so it puts the roles in the id token.

@ianwallen
Copy link
Contributor

I could modify it so that it could grab roles from the access token - but that's probably non-trivial todo. And it would make sense to allow use of the other providers (userinfo, MS Graph) as well.

Thank you for looking into it some more. I agree that this may be extra work and I think we can submit a separate PR in the future to make this configurable.

I will do one final test on my local system with the new changes and if the user profile changes work then I will approve your PR.

Copy link
Contributor

@ianwallen ianwallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I identified a few typos that should be easy to fix.

I also noticed some inconsistencies with the naming where it is using both camel case and _ separator. Generally I would expect it to only use one.

i.e. openidconnect_Configuration expecting OIDCConfiguration, openIDConnectConfiguration, oidc_configuration,...

Here are a couple others examples that are inconsistent but there are lots in the code.
openidconnect_UserService
openidconnect_DefaultAuthorizationCodeTokenResponseClient
openidconnect_bearer_tokenvalidator
openidconnect_bearer_rolesprovider

These should all be easy to change if required.

I'm not sure if naming standards are enforced in this project so if @josegar74 wants to approved the PR with these naming issues then I will approve as well

@davidblasby
Copy link
Contributor Author

@ianwallen - I changed the name of all those beans so they don't have "_" in the name.

@josegar74 - I will send you a private message for connecting to the geocat keycloak and Azure AD for testing.

@davidblasby
Copy link
Contributor Author

@ianwallen - I think, once jose has a chance to test, we are ready to merge!

Copy link
Contributor

@ianwallen ianwallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ianwallen - I think, once jose has a chance to test, we are ready to merge!

I good with the latest changes so I will approve - thank you

@davidblasby
Copy link
Contributor Author

Minor change for group - its was using the wrong isGroupUpdate()

@davidblasby
Copy link
Contributor Author

Added a new environment variable OPENIDCONNECT_LOGSENSITIVE_INFO for logging information. This is pretty much required when setting up so you can trouble shoot.

See docs.

@josegar74 josegar74 merged commit 37bd851 into geonetwork:main Nov 9, 2022
davidblasby pushed a commit to davidblasby/core-geonetwork that referenced this pull request Jun 9, 2023
josegar74 added a commit that referenced this pull request Jun 13, 2023
* missed file in #6494

* jose feedback

Co-authored-by: Jose García <josegar74@gmail.com>

---------

Co-authored-by: david.blasby <david.blasby@geocat.net>
Co-authored-by: Jose García <josegar74@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants