OKDP / okdp-spark-auth-filter

Oauth2/OIDC Authentication filter for Apache Spark Apps/History UIs
https://okdp.io
Apache License 2.0
3 stars 4 forks source link

request information when groups and offline_access scopes not supported #16

Closed SBatais closed 5 months ago

SBatais commented 5 months ago

Hello

I would like to know if okdp-spark-auth-filter can work with an IDP that doesn't support the scope groups and offline_access (array "scopes_supported" in issuer-uri/.well-known/openid-configuration), and be able to get groups and the refresh token information from an access token returned by the IDP when the user is authenticated.

Thanks in advance for the reply.

idirze commented 5 months ago

Hello,

Quick answer, in the current implementation: No

When the IDP does not support groups or roles, as they are not present in scopes_supported, it will not add the user groups in the JWT token response during the authentication phase and even the filter fails at startup time.

In the same way, if the IDP does not supports the offline_access, it will reject the refresh token requests (also depends on the IDP implementation as some of them, even when the offline_access is omitted it accepts to refresh the token).

Can you please, describe more the issue you are facing? Does your IDP supports a claim, other than groups or roles which can identify a group of users? If yes, we can include that claim so that it can be mapped using spark ACLs.

Also, we can support working with an IDP which does not support a refresh token or for which the feature is disabled. To work seamlessly, we will suppose the IDP continually refreshes the user token to avoid user disconnection/re-authentication during navigation when the token expires. Also, the filter should redirect the user back to his current page (instead of the home page) during request for new access token.

SBatais commented 5 months ago

Hello

Thanks for your reply.

Firstly here is some technical information of the used IDP and from .well-known/openid-configuration:

  "grant_types_supported": [
    "authorization_code",
    "password",
    "client_credentials",
    "refresh_token"
  ]

  "claims_supported": [
    "sub",
    "iss",
    "auth_time",
    "acr",
    "sid"
  ],

  "scopes_supported": [
    "openid",
    "profile",
    "email",
    "address",
    "phone"
  ]

My first issue is that the groups information are not retrieved. I will come back to the IDP provider to know if the groups information are available in the ID token or/and the access token. Once the user is authenticated, I have this message in the logs where we can see the groups is empty:

{"@timestamp":"2024-05-29T11:40:11.295Z","ecs.version":"1.2.0","log.level":"INFO","message":"Successfully authenticated user (<firstname> <lastname>): email <email address> sub <id-user> (roles: [], groups: [])","process.thread.name":"HistoryServerUI-26","log.logger":"io.okdp.spark.authc.OidcAuthFilter"}

My second issue is with the refresh token, and I need to test it deeper.

regards

idirze commented 5 months ago

Ok, thanks. Form the scopes_supported seems like your IDP is configured to authenticate users only and does not include authorization claims like roles or groups. The filter can be configured, through spark ACLs to authorize the users individually but not suited from the security perspective and complex users management.

In fact, yes, you can check with the IDP provider if it's possible to add groups or roles in the client scope and assign users to these groups.

For keycloak, for example, it's possible to enable the roles, groups or any custom claims by using Mappers. These claims then will be included in the JWT access token.

SBatais commented 5 months ago

Here is the reply of my IDP provider about groups:

_The groups information is return in the id_token and also in the accesstoken. Please do not forget to request the groups information by adding "groups" in the scope in your requests.

https://idp_provider_address/oauth2/authorize?client_id=client_id&redirect_uri=https://spark-history-address/home&response_type=code&scope=openid+profile+email+groups

idirze commented 5 months ago

OK, This can be done using the scope parameter or AUTH_SCOPE env variable

Example configuration:

env:
- name: AUTH_SCOPE
  value: openid+profile+email+groups
....
SBatais commented 5 months ago

I have already tested it, but I get this error because of the scope 'groups' not available in the supported groups of the IDP

The parameter 'scope|env: AUTH_SCOPE' contains an unsupported scopes '[groups]' by your oidc provider.\nThe supported scopes are: [openid, profile, email, address, phone]","error.stack_trace":"java.lang.IllegalArgumentException: The parameter 'scope|env: AUTH_SCOPE' contains an unsupported scopes '[groups]' by your oidc provider.\nThe supported scopes are: [openid, profile, email, address, phone]
idirze commented 5 months ago

Yes, actually the code enforces the check against the supported scopes reported by the IDP.

Your IDP reports, via the discovery endpoint, it does not support the groups scope and that's why it fails.

  "scopes_supported": [
    "openid",
    "profile",
    "email",
    "address",
    "phone"
  ]

I have published a v1.2.1 as a pre-release to remove the enforcement. You can download the jar from there, test and let us know if it works so that we can publish this version into maven central.

SBatais commented 5 months ago

I have tested with v1.2.1 and with AUTH_SCOPE="openid+profile+email+groups"

I get a warning when the scope is not in the supported_scopes list:

{"@timestamp":"2024-06-04T07:15:58.445Z","ecs.version":"1.2.0","log.level":"WARN","message":"The parameter 'scope|env: AUTH_SCOPE' contains an unsupported scopes '[groups]' by your oidc provider. The supported scopes are: [openid, profile, email, address, phone]","process.thread.name":"main","log.logger":"io.okdp.spark.authc.utils.PreconditionsUtils"}

and the groups information is retrieved once the user is authenticated:

{"@timestamp":"2024-06-04T07:20:11.748Z","ecs.version":"1.2.0","log.level":"INFO","message":"Successfully authenticated user (firstname lastname): email email_adress sub userid (roles: [], groups: [group1, group2])","process.thread.name":"HistoryServerUI-20","log.logger":"io.okdp.spark.authc.OidcAuthFilter"}
SBatais commented 5 months ago

I would like to know if it's possible to do the same for the scope offline_access

idirze commented 5 months ago

The check was removed as well for the offline_access scope.

What is the error you are getting when you add the scope?

env:
- name: AUTH_SCOPE
  value: openid+profile+email+groups+offline_access
...
SBatais commented 5 months ago

I have also tested with scope=....+offline_access

After few hours, I get an error saying that the refresh token should not be null or blank:

{"@timestamp":"2024-06-04T11:46:52.452Z","ecs.version":"1.2.0","log.level":"INFO","message":"The user address_email token was expired, renewing ... ","process.thread.name":"HistoryServerUI-1437","log.logger":"io.okdp.spark.authc.OidcAuthFilter"}

{"@timestamp":"2024-06-04T11:46:52.453Z","ecs.version":"1.2.0","log.level":"WARN","message":"/","process.thread.name":"HistoryServerUI-1437","log.logger":"org.sparkproject.jetty.server.HttpChannel","error.type":"java.lang.NullPointerException","error.message":"The parameter <refresh_token> should not be null or blank","error.stack_trace":"java.lang.NullPointerException: The parameter <refresh_token> should not be null or blank\n\tat io.okdp.spark.authc.utils.PreconditionsUtils.checkNotNull(PreconditionsUtils.java:41)\n\tat io.okdp.spark.authc.provider.impl.PKCEAuthorizationCodeAuthProvider.refreshToken(PKCEAuthorizationCodeAuthProvider.java:153)\n\tat io.okdp.spark.authc.OidcAuthFilter.lambda$doFilter$2(OidcAuthFilter.java:218)\n\tat io.okdp.spark.authc.utils.exception.Try.onException(Try.java:34)\n\tat io.okdp.spark.authc.OidcAuthFilter.doFilter(OidcAuthFilter.java:219)\n\tat org.sparkproject.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:193)\n\tat org.sparkproject.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1626)\n\tat org.sparkproject.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:552)\n\tat org.sparkproject.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)\n\tat org.sparkproject.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1440)\n\tat org.sparkproject.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)\n\tat org.sparkproject.jetty.servlet.ServletHandler.doScope(ServletHandler.java:505)\n\tat org.sparkproject.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)\n\tat org.sparkproject.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1355)\n\tat org.sparkproject.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)\n\tat org.sparkproject.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:772)\n\tat org.sparkproject.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:234)\n\tat org.sparkproject.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)\n\tat org.sparkproject.jetty.server.Server.handle(Server.java:516)\n\tat org.sparkproject.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:487)\n\tat org.sparkproject.jetty.server.HttpChannel.dispatch(HttpChannel.java:732)\n\tat org.sparkproject.jetty.server.HttpChannel.handle(HttpChannel.java:479)\n\tat org.sparkproject.jetty.server.HttpConnection.onFillable(HttpConnection.java:277)\n\tat org.sparkproject.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)\n\tat org.sparkproject.jetty.io.FillInterest.fillable(FillInterest.java:105)\n\tat org.sparkproject.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104)\n\tat org.sparkproject.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:338)\n\tat org.sparkproject.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:315)\n\tat org.sparkproject.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:173)\n\tat org.sparkproject.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131)\n\tat org.sparkproject.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:409)\n\tat org.sparkproject.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883)\n\tat org.sparkproject.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034)\n\tat java.base/java.lang.Thread.run(Unknown Source)\n"}
idirze commented 5 months ago

Ok, have disabled the offline_access in case the IDP does not support it or disabled. You can test with the new jar in the pre-release and let us know.

SBatais commented 5 months ago

@idirze : where can I find the new jar ?

idirze commented 5 months ago

It's with the same name in the assets of the pre release

https://github.com/OKDP/okdp-spark-auth-filter/releases/tag/v1.2.1

SBatais commented 5 months ago

I have re-tested with v1.2.1 and with AUTH_SCOPE="openid+profile+email+groups++offline_access". I don't have anymore the error "The parameter should not be null or blank", and it seems that the automatic re-authentication is working fine, without any action from the user.

Here are the logs when the token expired:

{"@timestamp":"2024-06-05T11:18:37.615Z","ecs.version":"1.2.0","log.level":"INFO","message":"The user _address_email_ token was expired, removing cookie and attempt to re-authenticate ... ","process.thread.name":"HistoryServerUI-26","log.logger":"io.okdp.spark.authc.OidcAuthFilter"}

{"@timestamp":"2024-06-05T11:18:40.555Z","ecs.version":"1.2.0","log.level":"INFO","message":"Successfully authenticated user (_fistname lastname_): email _address_email_ sub _userid_ (roles: [], groups: [group1, group2])","process.thread.name":"HistoryServerUI-2471","log.logger":"io.okdp.spark.authc.OidcAuthFilter"}

Thanks @idirze for your support

idirze commented 5 months ago

@SBatais, thank you for the feedback and feel free to rise any issue, contribution or any request information. We have just published the new version with the fixes in Maven Central

You can download the 1.2.1 jar from there.

So, i close the issue as resolved.