OHDSI / ROhdsiWebApi

An R package for interfacing with a WebAPI instance
https://ohdsi.github.io/ROhdsiWebApi
10 stars 17 forks source link

Support WebAPI security #35

Closed schuemie closed 3 years ago

schuemie commented 4 years ago

It is currently not possible to connect to a WebAPI that has security enabled. This may require some changes in WebAPI as well.

schuemie commented 4 years ago

Seems to already be discussed here

alondhe commented 4 years ago

There's a post on the WebAPI git that I think should be relevant here: https://github.com/OHDSI/WebAPI/issues/1473#issuecomment-600103652

schuemie commented 4 years ago

Jinx!

alondhe commented 4 years ago

@schuemie beat me to it. Of course :)

gowthamrao commented 4 years ago

thank you for working on this!

Could you please confirm -without this enhancement, we cannot use ROhdsiWebApi (even with a bearer token?)

gowthamrao commented 4 years ago

@ablack3 would you like to redo the PR based on the develop branch for v1.0.0 release?

ablack3 commented 4 years ago

@gowthamrao Yes!

ablack3 commented 4 years ago

My WebAPI instance disappeared when I updated windows so it will take me some time to get everything set up again.

ablack3 commented 4 years ago

Talking with Konstantin about the the best way to add this. Since we want to support all types of authentication I think it is work thinking through how this should work. In every case we need to pass a header to the http requests. Sometimes that header is a token and other times it is a cookie. I think it would be if the user of this package did not need to manage the token or cookie. The user authenticates by running an authenticate() function which will allow the user to supply an auth method or guess the appropriate auth method if none is supplied. If authentication is successful an authHeader is created and saved in a temp file location that will be deleted when the R session ends. This header then automatically gets added to every httr call. If the user gets a 401 error (unauthorized) on any call then a message prompting the user to call the authenticate() function will be displayed.

Or something like that. There is this idea of a function's or package's API (i.e. how the user of the package interacts with the functions) that I think is very important to think about.

ablack3 commented 4 years ago

I've been looking at implementing oauth and learning more about API security in the process. A couple questions to throw out in case anyone knows more about this stuff.

OAuth2 Terminology: Protected resource = WebApi Client application = ROhdsiWebApi Resource owner = the user Authorization server = google oauth server

Should ROhdsiWebApi have its own client ID & secret or should it use the client ID & secret of the Atlas instance? It seems to me like ROhdsiWebApi should have a separate client id and secret since it is a essentially a different native application accessing WebApi. I think we would need to set up a single client id and secret for all ROhdsiWebApi installations and hard code it into the package. ROhdsiWebApi would then use these to authenticate to the authorization server to get the access token (bearer token). However one thing I'm not sure about is if WebApi should be able to verify the access token sent by ROhdsiWebApi. Would every WebApi installation with oauth enabled be able to verify access tokens created using the plan above without additional configuration?

ablack3 commented 4 years ago

Here is an example using Google's Identity Aware Proxy (IAP). It involves first creating a service account on Google Cloud Platform and relies on changes to ROhdsiWebApi in https://github.com/OHDSI/ROhdsiWebApi/pull/148

example_service_account.json - Created by Google for the service account

{
  "type": "service_account",
  "project_id": "my-project-id",
  "private_key_id": "...",
  "private_key": "...",
  "client_email": "...",
  "client_id": "...",
  "auth_uri": "...",
  "token_uri": "...",
  "auth_provider_x509_cert_url": "...",
  "client_x509_cert_url": "..."
}

R script

#' Get a token to be used with ROhdsiWebApi using an IAP service account
#' 
#' @param jsonPath File path to the service account json downloaded from google cloud platform
#' @param iapClientId The client id (e.g. "3434303000332-aaafdauuksdjjjjjjpalaaj3.apps.googleusercontent.com")
#'
#' @return A string containing a Bearer token
#' @export
fetchIapToken <- function(jsonPath, iapClientId) {
  iapJson <- jsonlite::read_json(jsonPath)

  iat <- floor(as.numeric(Sys.time()))
  claim <- jose::jwt_claim(iss = iapJson$client_email,
                           aud = "https://www.googleapis.com/oauth2/v4/token",
                           exp = iat + 3600,
                           iat = iat,
                           target_audience = iapClientId)
  secretKey <- openssl::read_key(iapJson$private_key)
  jwt <- jose::jwt_encode_sig(claim = claim, key = secretKey, size = 256, header = NULL)
  body <- list(grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion = jwt)
  response <- httr::POST('https://oauth2.googleapis.com/token', 
                         httr::content_type("application/x-www-form-urlencoded"),
                         # verbose(), # uncomment this to get more info regarding token request
                         encode = "form",
                         body = body)
  idToken <- httr::content(response, as = "parsed")
  authHeader <- paste("Bearer", idToken$id_token)
  stopifnot(is.character(authHeader))
  authHeader
}

# A simple example 
library(ROhdsiWebApi)
baseUrl <- "https://atlas-url.com/WebAPI"

# Get the Bearer token
authHeader <- fetchIapToken(jsonPath = "example_service_account.json",
                            iapClientId = "numbers-lettersAndNumbers.googleusercontent.com")

# Associate the bearer token with the baseUrl in the package environment
setAuthHeader(baseUrl, authHeader)

# Use ROhdsiWebApi functions as normal
getCdmSources(baseUrl)
alondhe commented 3 years ago

Hi @ablack3 -- I added a working example from our Active Directory setup here: https://github.com/OHDSI/WebAPI/issues/1473#issuecomment-780915439

so setAuthHeader() would take the baseUrl and the bearer token, no need to modify the other function calls, right? And then the function code would include an add_headers(Authorization = bearer) parameter for GETs.

Edit: just saw your PR, looks like you already are handling AD auth. Nice!

gowthamrao commented 3 years ago

Completed here https://github.com/OHDSI/ROhdsiWebApi/commit/af06e4d95bceb4d3fc14f1c38cddfe66eb93a069