Search code examples
roauth-2.0github-actionshttr

web browser authentication in GitHub Actions for Oauth token


I'm trying to use GitHub Actions to query the GitHub API using an OAuth 2.0 token through an R script. It works fine on my local machine when I run the code, where a browser window pops up indicating "Waiting for authentication in browser..." that I can manually close. When run through GitHub Actions, the workflow hangs at the "Waiting for authentication in browser..." since it's on a remote machine.

I'm using a custom R script with the httr library. The API credentials are stored as secrets for the repository I'm trying to query.

library(httr)

gh_key <- Sys.getenv('GH_KEY')
gh_secret <- Sys.getenv('GH_SECRET')

# setup app credentials
myapp <- oauth_app(appname = "data-in-r",
                   key = gh_key,
                   secret = gh_secret)

# get oauth credentials
github_token <- oauth2.0_token(oauth_endpoints('github'), app = myapp, cache = F)

# use api
gtoken <- config(token = github_token)

# get list of remote files in data folder
req <- GET("https://api.github.com/repos/tbep-tech/piney-point/contents/data", gtoken)

When the script is run through GitHub Actions, it looks like as below, where I had to manually cancel the workflow since it was hung at the browser step.

enter image description here

Is there a workaround for skipping the browser step so it will run on GitHub Actions? The offending repo is here.


Solution

  • You cannot use the 3-legged variant of OAuth2.0 (aka "web-application flow") in a headless environment like Github Actions.

    If you want to use OAuth (I list other possibilities below), then you need to utilize what gitlab calls the "device-flow". See github documentation.

    In this flow, there is no redirect to a given URL, so the app does not need a browser window. Instead it displays a code to the user. The user must the enter that code on a fixed URL (https://github.com/login/device). As soon as this is done, the app can request the authentication token. (So the app must keep polling until the user has entered the code).

    Unfortunately, httr does not have nice wrapper functions for this variant, so you have to do the calls yourself. It can work like this:

    library(httr)
    
    
    
    app_id <- "*redacted*"
    
    
    r <- POST("https://github.com/login/device/code", 
              body = list(
                client_id = app_id,
                scope = "user repo delete_repo" #Scope must be given! https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
    ))
    
    
    device_code <- content(r)$device_code
    
    print(paste0("Enter the code  ", content(r)$user_code, "  at ", content(r)$verification_uri))
    
    
    ## NOTE: In reality this request has to run in a loop, until the user has entered the code und the request succeeds.
    ##       For the demo we can execute it manually after the code has been entered.
    r <- POST("https://github.com/login/oauth/access_token", 
              body = list(
                client_id = app_id,
                device_code = device_code,
                grant_type = "urn:ietf:params:oauth:grant-type:device_code"
              ))
    
    token <- content(r)$access_token
    
    ## create and delete a private testrepository to check if everything worked
    r <- 
      POST("https://api.github.com/user/repos",
           add_headers(Authorization = paste("token", token)),
           body = list(name = "testrepo",
                     private = TRUE,
                     auto_init = FALSE), 
           encode = "json")
    
    
    r <- DELETE(paste0("https://api.github.com/repos/", content(r)$full_name), 
           add_headers(Authorization = paste("token", token)))
    

    I have seen that there is httr2, and that it offers convenience functions for this flow. I have however never used it and do not know if it already works reliable. See here.

    Since this flow still requires user interaction, you may be better of with one of the following variants (I do not know if they fit your use case.):

    1. Basic Auth: You can define what github calls a "personal access token" beforehand. With this token you can authenticate without further interaction. Creation is described here. In R you can use it most easily together with httr::authenticate.

    2. GITHUB_TOKEN: Github automatically creates special secrets specifically Github Actions. These can be used to execute actions in the containing repository. For more info see here.