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

OAuth2 single sign-on implementation (BE + FE) #430

Merged
merged 6 commits into from
Jan 23, 2018
Merged

OAuth2 single sign-on implementation (BE + FE) #430

merged 6 commits into from
Jan 23, 2018

Conversation

saibot94
Copy link

@saibot94 saibot94 commented Jan 15, 2018

This PR addresses feature request #354 .

The related elastic4play PR can be found here.

It adds a few pieces of functionality (described below) in order to allow smooth integration for any custom OAuth2 server that you may use.

UI changes

In order to support OAuth2 login, the currently existing login page wouldn't be sufficient. Once an SSO method is detected, the SSO login button is displayed on the UI:

sso-login

The changes include the possibility of automatically redirecting users to the organization's SSO page when accessing the root url of the site, through a config parameter.

Thus, accessing http://my-thehive-instance.com/ would redirect the user to the OAuth2 provider.

Backend changes

The OAuth2Srv is the bread and butter of the PR, providing a new auth method that can be added to the list of others.

A new endpoint is provided, allowing the use of the authenticate() method and not interfering with the other regular logins:

POST     /api/ssoLogin                            controllers.AuthenticationCtrl.ssoLogin()

The concept of a UserMapper is also introduced, allowing you to easily define a 1-1 mapping from the response that the OAuth2 to user fields, such that user. Two different simple UserMapper implementations (that made sense for us) are included:

  • SimpleUserMapper - creates users in a simple manner, providing them permissions according to what is set in the configuration
  • GroupUserMapper - may be specific only to our use case, but basically provides ACL's that can be configured. You provide a URL through which the groups to which a user belongs become available and configure the permissions associated with some specific groups, giving him the highest available permission.

Configuration changes

Currently, the only OAuth2 responseType available is code, returning a GET param in the form ?id=some-oauth-code-here.

Below is an example of how the config file's auth portion would look like, should one choose to setup OAuth and the GroupMapper:

auth {

  provider = [local, oauth2]

  oauth2 {
    # URL of the authorization server
    clientId = "hive-app-client-id"
    clientSecret = "client-secret"
    redirectUri = "https://my-hive-instance.com/index.html#login"
    responseType = "code"
    # You may need to change
    grantType = "authorization_code"
    # URL from where to get the access token
    authorizationUrl = "https://oauth.web.cern.ch/OAuth/Authorize"
    tokenUrl = "https://auth-site.com/OAuth/Token"
    # The endpoint from which to obtain user details using the OAuth token, after successful login
    userUrl = "https://auth-site.com/api/User"
    scope = "read:user"

  }

  sso {
    # Name of mapping class from user resource to backend user
    mapper = group
    autocreate = true
    defaultRoles = [""]
    autologin = true
    groups {
      url = "https://auth-site.com/api/Groups"
      mappings {
        it-dep = ["read", "write", "admin"]
      }
    }
  }
}

Comments, suggestions, improvements are always welcome.

PS: I'll change the version from Dependencies.scala once the elastic4play PR gets accepted first.

PS2: Once everything's OK on your side, as well, I'll submit a PR for the documentation as well, in order to keep it all consistent.

@To-om
Copy link
Contributor

To-om commented Jan 15, 2018

Great job !

@To-om
Copy link
Contributor

To-om commented Jan 19, 2018

I've add the possibility to configure how login, name, roles (and groups GroupUserMapper) is retrieve from OAuth2 server.

I used Cloudfoundry uaa to test OAuth2 authentication and user info contains user_name attribute, instead of username. So the Json conversion to SimpleJsonUser doesn't work.

You can now configure attribute mapping as follow :

 sso {
    mapper = simple
    defaultRoles = ["read"]
    attributes {
      login = "user_name"
      name = "name"
    }
 }

Can you check you can use it in your environment. I'm not able to test GroupUserMapper ?

@To-om To-om added this to the 3.1.0 (Cerana 1) milestone Jan 19, 2018
@saibot94
Copy link
Author

saibot94 commented Jan 19, 2018

Thanks for that!

I was actually thinking that it would've made more sense to allow the json mappings to be configurable.
I've tested the GroupUserMapper locally with our auth service and it's working fine (running it on the prod server gave a weird error), just had to add:

mapper = group 

attributes {

   groups = "groups"
   ...
}

I've pushed the commits that you've made onto my branches.

About the error, it seems to be from some new configuration that needs to be added:

Jan 19 12:01:01  thehive[6971]: Oops, cannot start the server.
Jan 19 12:01:01  thehive[6971]: Configuration error: Configuration error[Cannot load class services.StreamFilter]
Jan 19 12:01:01  thehive[6971]: at play.api.Configuration$.configError(Configuration.scala:155)
Jan 19 12:01:01  thehive[6971]: at play.api.Configuration.reportError(Configuration.scala:985)
Jan 19 12:01:01  thehive[6971]: at play.api.http.EnabledFilters.$anonfun$filters$2(HttpFilters.scala:90)
Jan 19 12:01:01  thehive[6971]: at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:234)
Jan 19 12:01:01  thehive[6971]: at scala.collection.mutable.ResizableArray.foreach(ResizableArray.scala:59)
Jan 19 12:01:01  thehive[6971]: at scala.collection.mutable.ResizableArray.foreach$(ResizableArray.scala:52)
Jan 19 12:01:01  thehive[6971]: at scala.collection.mutable.ArrayBuffer.foreach(ArrayBuffer.scala:48)
Jan 19 12:01:01  thehive[6971]: at scala.collection.TraversableLike.map(TraversableLike.scala:234)
Jan 19 12:01:01  thehive[6971]: at scala.collection.TraversableLike.map$(TraversableLike.scala:227)
Jan 19 12:01:01  thehive[6971]: at scala.collection.AbstractTraversable.map(Traversable.scala:104)
Jan 19 12:01:01  thehive[6971]: at play.api.http.EnabledFilters.liftedTree1$1(HttpFilters.scala:84)
Jan 19 12:01:01  thehive[6971]: at play.api.http.EnabledFilters.<init>(HttpFilters.scala:80)
Jan 19 12:01:01  thehive[6971]: at play.api.http.EnabledFilters$$FastClassByGuice$$5bec3932.newInstance(<generated>)
Jan 19 12:01:01  thehive[6971]: at com.google.inject.internal.DefaultConstructionProxyFactory$FastClassProxy.newInstance(DefaultConstructionProxyFactory.java:89)
at com.google.inject.internal.ConstructorInjector.provision(ConstructorInjector.java:111)
at com.google.inject.internal.ConstructorInjector.construct(ConstructorInjector.java:90)
at com.google.inject.internal.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:268)
at com.google.inject.internal.ProviderToInternalFactoryAdapter$1.call(ProviderToInternalFactoryAdapter.java:46)

@To-om
Copy link
Contributor

To-om commented Jan 19, 2018

The services.StreamFilter has been renamed into org.elastic4play.services.EventFilter (done in #414). Your reference.conf must be updated, or use directly the branch saibot94-feature-354.

I think that this feature is ready to be merged.
Thank you for your contribution.

@saibot94
Copy link
Author

That was it, apparently I was overriding those values in my application.conf file.

I confirm that I've tested the changes on the prod instance using the Group mapper, everything is working as expected.

Should I also create a PR for TheHiveDocs in the following days, documenting the functionality?

@To-om
Copy link
Contributor

To-om commented Jan 22, 2018

I would be great if you write some doc about this. Thanks

@To-om To-om merged commit 0d8a45e into TheHive-Project:develop Jan 23, 2018
To-om added a commit that referenced this pull request Jan 23, 2018
@To-om To-om removed the wip label Jan 23, 2018
@saibot94 saibot94 deleted the feature-354 branch January 24, 2018 16:11
@saibot94 saibot94 restored the feature-354 branch March 5, 2018 07:05
@nusantara-self
Copy link

nusantara-self commented Mar 13, 2019

Hello @saibot94 and @To-om,
First of all, thank you so much for your hard work on this feature!

I'm currently setting up the OAuth2.0 authentication. I manage to contact the authorization entity to get a token in the URL, it works successfully. However I do not succeed on the authentication part related to TheHive. (Authentication Failure)

Could you please enlighten me on how to use the UserMapper feature?

I am not sure how the "mappings" and "attributes" fields should be used in order to make it work.
Here is what I have in mind, but I can't seem to get it right.

sso {
    mapper = simple
    autocreate = true
    defaultRoles = [""]
    autologin = true
    attributes {
      login = "uid"
      name = "displayName"
    mappings {
      user1_uid = ["read", "write", "admin"]
      user2_uid = ["read", "write"]
      }
    }

Thanks in advance for helping me out!

@To-om
Copy link
Contributor

To-om commented Mar 14, 2019

If I remember correctly, if the user data retrieved from OAuth2 server contains the role (read, write, ...) you should use "mapper=simple":

sso {
    mapper = simple
    autocreate = true
    defaultRoles = []
    autologin = true
    attributes {
        login = "uid"
        name = "displayName"
        roles = "roleAttribute" // name of the attributes containing user roles
    }
}

If user data contains groups, you need to map groups to roles and you should use "mapper=group":

sso {
    mapper = group
    autocreate = true
    defaultRoles = []
    autologin = true
    attributes {
        login = "uid"
        name = "displayName"
        groups = "groups" // name of the attribute containing the user groups
    }
    groups.mappings { // map groups to roles
        mygroup1 = ["read"]
        mygroup2 = ["read", "write"]
        mygroup3 = ["read", "write", "admin"]
    }
}

@nusantara-self
Copy link

nusantara-self commented Mar 18, 2019

Hi @To-om,

Thank you so much for such a quick answer. This is so great of you and it really helped me understand how to configure it.

The OAuth2 works like a charm. Once I arrive at the login page into TheHive, I can successfully connect via SSO. Immediately after, I get an authorization code in the URL which is valid, as I tested with postman. I can easily retrieve user informations to then point to the right fields in the conf file.

Therefore, my configuration is as such :

sso {
        mapper = group
        autocreate = true
        defaultRoles = ["read"]
        #autologin = true
        attributes {
                login = "user_id"
                name = "displayName"
                groups = "role"
                }
        groups.mappings {
                role1_name= ["read", "write", "admin"]
                role2_name= ["read", "write"]
                }
        }

I've tried numerous scenarios, even using the simple mapper where anybody should be able to access the app without success.

No matter if the user exists already or not in the database, I get this error in the logs.

2019-03-18 10:16:16,498 [ERROR] from org.elastic4play.controllers.Authenticated in application-akka.actor.default-dispatcher-8 - Authentication failure:
        session: AuthenticationError User session not found
        pki: AuthenticationError Certificate authentication is not configured
        key: AuthenticationError Authentication header not found
        init: AuthenticationError Use of initial user is forbidden because users exist in database
2019-03-18 10:16:16,521 [INFO] from org.elastic4play.ErrorHandler in application-akka.actor.default-dispatcher-8 - GET /api/user/current returned 401
org.elastic4play.AuthenticationError: Authentication failure
at org.elastic4play.controllers.Authenticated.$anonfun$getContext$4(Authenticated.scala:254)
        at scala.concurrent.Future.$anonfun$flatMap$1(Future.scala:303)
        at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:37)
        at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
        at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:55)
        at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:91)
        at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
        at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:81)
        at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:91)
        at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:40)
        at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:44)
        at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
        at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
        at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
        at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

I am not too sure where to look at right now, my current username in thehive's elasticsearch matches the "uid" of the OAuth2 retrieved user information, which I thought could be a reason why it fails. Though, with an autocreate=true and an unregistered user in thehive's database that has permissions thanks to groups.mappings, it still fails to either create the user or connect/get past the login page.

@davinerd
Copy link

Another question: when configuring mapper = group, TheHive expects the field groups.url to be set as mandatory (I've an Invalid URL exception in log files).

But it shouldn't be the case, based on your examples.

Am I missing something?

@nusantara-self
Copy link

nusantara-self commented Apr 2, 2019

Hi @davinerd,

I do not have any errors related to URLs with the configuration I used. Are you sure the Invalid URL doesn't come from the OAuth2's redirect URL or any other parameter and not the sso field?

Make sure to verify fields such as :

oauth2 {
        redirectUri = "https://domain.com/index.html#login"
        authorizationUrl = "https:/domain.com/authorization.url"
        tokenUrl = "https://domain.com/token.url"
        userUrl = "https://domain.com/user.url"
        scope = "scope field"
        }

Feel free to share more logs and info on your application.conf it it doesn't help.

As for my situation, I still haven't fixed it.
Everything related to oauth2 works as expected in postman, however it seems like it fails to get any authentication header or certificate for authentication which may be a reason why. Please, let me know if anybody has any clues on this.

@ananth07reddy
Copy link

I am having the same issue with invalid url. I am using a 3.4.0-RC1 Hive veversion, please find my logs below:

[error] o.e.s.a.MultiAuthSrv - Authentication failure
java.lang.IllegalArgumentException: Invalid URL
at play.api.libs.ws.ahc.StandaloneAhcWSClient.validate(StandaloneAhcWSClient.scala:84)
at play.api.libs.ws.ahc.StandaloneAhcWSClient.url(StandaloneAhcWSClient.scala:42)
at play.api.libs.ws.ahc.AhcWSClient.url(AhcWSClient.scala:37)
at services.mappers.GroupUserMapper.$anonfun$getUserFields$2(GroupUserMapper.scala:43)
at scala.Option.fold(Option.scala:158)
at services.mappers.GroupUserMapper.getUserFields(GroupUserMapper.scala:43)
at services.mappers.MultiUserMapperSrv.getUserFields(MultiUserMapperSrv.scala:27)
at services.OAuth2Srv.$anonfun$getOrCreateUser$1(OAuth2Srv.scala:123)
at scala.Option.fold(Option.scala:158)
at services.OAuth2Srv.withOAuth2Config(OAuth2Srv.scala:66)
Caused by: java.lang.IllegalArgumentException: could not be parsed into a proper Uri, missing scheme
at play.shaded.ahc.org.asynchttpclient.uri.Uri.create(Uri.java:40)
at play.shaded.ahc.org.asynchttpclient.uri.Uri.create(Uri.java:32)
at play.api.libs.ws.ahc.StandaloneAhcWSClient.validate(StandaloneAhcWSClient.scala:81)
at play.api.libs.ws.ahc.StandaloneAhcWSClient.url(StandaloneAhcWSClient.scala:42)
at play.api.libs.ws.ahc.AhcWSClient.url(AhcWSClient.scala:37)
at services.mappers.GroupUserMapper.$anonfun$getUserFields$2(GroupUserMapper.scala:43)
at scala.Option.fold(Option.scala:158)
at services.mappers.GroupUserMapper.getUserFields(GroupUserMapper.scala:43)
at services.mappers.MultiUserMapperSrv.getUserFields(MultiUserMapperSrv.scala:27)
at services.OAuth2Srv.$anonfun$getOrCreateUser$1(OAuth2Srv.scala:123)
[info] o.e.ErrorHandler - POST /api/ssoLogin?code=fc0c6142-4056-4017-9ee0-d181a1d27b86.3c566da0-3eb0-4ed2-83c4-6f942fc4462f.ecde927e-733e-4477-b0ff-93238254eaa6 returned 401
org.elastic4play.AuthenticationError: Authentication failure
at org.elastic4play.services.auth.MultiAuthSrv$$anonfun$authenticate$6.applyOrElse(MultiAuthSrv.scala:71)
at org.elastic4play.services.auth.MultiAuthSrv$$anonfun$authenticate$6.applyOrElse(MultiAuthSrv.scala:67)
at scala.concurrent.Future.$anonfun$recoverWith$1(Future.scala:413)
at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:37)
at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:55)
at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:91)
at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:81)
at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:91)

$scope.ssoLogin = function (code) {
AuthenticationSrv.ssoLogin(code, function(data, status, headers) {
var redirectLocation = headers().location;
if(angular.isDefined(redirectLocation)) {
Copy link

Choose a reason for hiding this comment

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

@saibot94 Could you explain what does this line? I mean: I understand that it takes the Location header of the response and redirect to it, but I don't understand why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants