TheHive icon indicating copy to clipboard operation
TheHive copied to clipboard

[Bug] SSO with Azure AD times out

Open Kamforka opened this issue 2 years ago • 2 comments

Request Type

Bug

Work Environment

Question Answer
OS version (server) Ubuntu 20.04
OS version (client) Ubuntu 20.04
Virtualized Env. True
Dedicated RAM 16 GB
vCPU 4
TheHive version / git hash 4.1.11-1
Package Type Docker
Database Cassandra
Index type Lucene
Attachments storage Local
Browser type & version Firefox 78.15

Problem Description

I was trying to set up Azure AD SSO for TheHive following the official docs and added the following snippet to application.conf:

auth {
  providers = [
    {name: session}
    {name: basic, realm: thehive}
    {name: local}
    {name: key}
    {
        name: oauth2
        clientId: "<my-client-id>"
        clientSecret: "<my-client-secret>"
        redirectUri: "http://localhost:9050/api/ssoLogin"
        responseType: code
        grantType: "authorization_code"
        authorizationUrl: "https://login.microsoftonline.com/<my-tenant-id>/oauth2/v2.0/authorize"
        authorizationHeader: "Bearer "
        tokenUrl: "https://login.microsoftonline.com/<my-tenant-id>/oauth2/v2.0/token"
        userUrl: "https://graph.microsoft.com/v1.0/me"
        scope: ["User.Read"]
        userIdField: "mail"
      }
  ]
}

I do not know if there is a typo in the authorizationHeader field's "Bearer " value, since it has a trailing space, so I tried both with and without a space -> same error

I've created an app registration in the Azure portal and configured the redirect URL to match the one in the application config. The SSO button shows up at the login page however when I try to log in using it the token verification request times out. This is the captured network traffic after hitting the button:

302 GET https://thehive.test/api/ssoLogin
200 GET https://login.microsoftonline.com/<my-tenant-id>/oauth2/v2.0/authorize?state=0cbaf0de-...&scope=User.Read&redirect_uri=https%3A%2F%2Fthehive.test...%2Fapi%2FssoLogin&client_id=<my-client-id>&response_type=code
303 GET https://thehive.test/api/ssoLogin?code=0.ARA...&state=0cbaf0d...&session_state=38d45...#
308 GET https://thehive.test/?error=OAuth2+token+verification+failure+Request+timeout+to+login.microsoftonline.com%3A443+after+120000+ms

The container is deployed behind a proxy, however if I use curl to contact https://login.microsoftonline.com I get an OK response instead of a timeout.

What am I missing?

Kamforka avatar Oct 14 '21 17:10 Kamforka

Turns out that TheHive doesn't respect system proxies so I had to add the below options to the application call:

-Dhttp.proxyHost=<my-proxy-host> \
-Dhttp.proxyPort=<my-proxy-port> \
-Dhttps.proxyHost=<my-proxy-host> \
-Dhttps.proxyPort=<my-proxy-port> \
-Dhttp.nonProxyHosts="localhost|127.0.0.1|<other-non-proxy-hosts...>"  # note the quotes around the list of hosts

I tried to add these configs to application.conf but I can't seem to find the right keys, tried as simple as http.proxyHost: <my-proxy-host> and play.http.proxyHost: <my-proxy-host> but none of them worked. However I'd like it better to keep these options within the application config if possible, any clues?

Kamforka avatar Oct 15 '21 08:10 Kamforka

Hi,

I have a similar problem as you with Google and without proxy. I changed the two WS calls in /opt/TheHive/ScalliGraph/core/src/main/scala/org/thp/scalligraph/auth/OAuth2Srv.scala by curl call for test.

Since then it has been working fine.

  /**
    * Querying the OAuth2 server for a token
    * @param code the previously obtained code
    * @return
    */
  private def getAuthTokenFromCode(code: String, state: String): Future[String] = {
    logger.trace(s"""
                    |Request to ${OAuth2Config.tokenUrl} with
                    |  code:          $code
                    |  grant_type:    ${OAuth2Config.grantType}
                    |  client_secret: ${OAuth2Config.clientSecret}
                    |  redirect_uri:  ${OAuth2Config.redirectUri}
                    |  client_id:     ${OAuth2Config.clientId}
                    |  state:         $state
                    |""".stripMargin)
    WSClient
      .url(OAuth2Config.tokenUrl)
      .withHttpHeaders("Accept" -> "application/json")
      .post(
        Map(
          "code"          -> code,
          "grant_type"    -> OAuth2Config.grantType.toString,
          "client_secret" -> OAuth2Config.clientSecret,
          "redirect_uri"  -> OAuth2Config.redirectUri,
          "client_id"     -> OAuth2Config.clientId,
          "state"         -> state
        )
      )
      .transform {
        case Success(r) if r.status == 200 => Success((r.json \ "access_token").asOpt[String].getOrElse(""))
        case Failure(error)                => Failure(AuthenticationError(s"OAuth2 token verification failure ${error.getMessage}"))
        case Success(r)                    => Failure(AuthenticationError(s"OAuth2/token unexpected response from server (${r.status} ${r.statusText})"))
      }
  }

  /**
    * Client query for user data with OAuth2 token
    * @param token the token
    * @return
    */
  private def getUserData(token: String): Future[JsObject] = {
    logger.trace(s"Request to ${OAuth2Config.userUrl} with authorization header: ${OAuth2Config.authorizationHeader} $token")
    WSClient
      .url(OAuth2Config.userUrl)
      .addHttpHeaders("Authorization" -> s"${OAuth2Config.authorizationHeader} $token")
      .get()
      .transform {
        case Success(r) if r.status == 200 => Success(r.json.as[JsObject])
        case Failure(error)                => Failure(AuthenticationError(s"OAuth2 user data fetch failure ${error.getMessage}"))
        case Success(r)                    => Failure(AuthenticationError(s"OAuth2/userinfo unexpected response from server (${r.status} ${r.statusText})"))
      }
  }
/**
    * Querying the OAuth2 server for a token
    * @param code the previously obtained code
    * @return
    */
  private def getAuthTokenFromCode(code: String, state: String): Future[String] = {
    logger.trace(s" curl ${OAuth2Config.tokenUrl} -X POST  -d code=$code -d grant_type=${OAuth2Config.grantType} -d client_secret=${OAuth2Config.clientSecret} -d redirect_uri=${OAuth2Config.redirectUri} -d client_id=${OAuth2Config.clientId} -d state=$state")
val solr = Seq("curl", OAuth2Config.tokenUrl, "-X", "POST", "-H", "application/json", "-d", "code=" + code , "-d", "grant_type="+OAuth2Config.grantType, "-d", "client_secret="+OAuth2Config.clientSecret,"-d", "redirect_uri="+OAuth2Config.redirectUri, "-d", "client_id="+OAuth2Config.clientId, "-d", "state=" + state)
    val res = solr.!!
    val json =  Json.parse(res)

    Future((json \ "access_token").asOpt[String].getOrElse(""))
  }

  /**
    * Client query for user data with OAuth2 token
    * @param token the token
    * @return
    */
  private def getUserData(token: String): Future[JsObject] = {
    logger.trace(s"Request to ${OAuth2Config.userUrl} with authorization header: ${OAuth2Config.authorizationHeader} $token")
    val solr = Seq("curl", OAuth2Config.userUrl, "-X", "GET", "-H", "Authorization: " + s"${OAuth2Config.authorizationHeader} $token")
    val res = solr.!!
    logger.trace(s"Result is $res")
    val json =  Json.parse(res)
    Future(json.as[JsObject])
  }

sboulic avatar May 27 '22 16:05 sboulic