Skip to content

Instantly share code, notes, and snippets.

@hadley
Last active March 8, 2024 06:16
Show Gist options
  • Star 43 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save hadley/144c406871768d0cbe66b0b810160528 to your computer and use it in GitHub Desktop.
Save hadley/144c406871768d0cbe66b0b810160528 to your computer and use it in GitHub Desktop.
Sketch of shiny + oauth
library(shiny)
library(httr)
# OAuth setup --------------------------------------------------------
# Most OAuth applications require that you redirect to a fixed and known
# set of URLs. Many only allow you to redirect to a single URL: if this
# is the case for, you'll need to create an app for testing with a localhost
# url, and an app for your deployed app.
if (interactive()) {
# testing url
options(shiny.port = 8100)
APP_URL <- "http://localhost:8100/"
} else {
# deployed URL
APP_URL <- "https://servername/path-to-app"
}
# Note that secret is not really secret, and it's fine to include inline
app <- oauth_app("shinygithub",
key = "51d46f96810d1fd182a2",
secret = "66eec8782825eeb61007dbef32f91afc9c3587aa",
redirect_uri = APP_URL
)
# Here I'm using a canned endpoint, but you can create with oauth_endpoint()
api <- oauth_endpoints("github")
# Always request the minimal scope needed. For github, an empty scope
# gives read-only access to public info
scope <- ""
# Shiny -------------------------------------------------------------------
has_auth_code <- function(params) {
# params is a list object containing the parsed URL parameters. Return TRUE if
# based on these parameters, it looks like auth codes are present that we can
# use to get an access token. If not, it means we need to go through the OAuth
# flow.
return(!is.null(params$code))
}
ui <- fluidPage(
# Your regular UI goes here, for when everything is properly auth'd
verbatimTextOutput("code")
)
# A little-known feature of Shiny is that the UI can be a function, not just
# objects. You can use this to dynamically render the UI based on the request.
# We're going to pass this uiFunc, not ui, to shinyApp(). If you're using
# ui.R/server.R style files, that's fine too--just make this function the last
# expression in your ui.R file.
uiFunc <- function(req) {
if (!has_auth_code(parseQueryString(req$QUERY_STRING))) {
url <- oauth2.0_authorize_url(api, app, scope = scope)
redirect <- sprintf("location.replace(\"%s\");", url)
tags$script(HTML(redirect))
} else {
ui
}
}
server <- function(input, output, session) {
params <- parseQueryString(isolate(session$clientData$url_search))
if (!has_auth_code(params)) {
return()
}
# Manually create a token
token <- oauth2.0_token(
app = app,
endpoint = api,
credentials = oauth2.0_access_token(api, app, params$code),
cache = FALSE
)
resp <- GET("https://api.github.com/user", config(token = token))
# TODO: check for success/failure here
output$code <- renderText(content(resp, "text"))
}
# Note that we're using uiFunc, not ui!
shinyApp(uiFunc, server)
@sourourarbi
Copy link

it's not worked for me

@blakiseskream
Copy link

@hadley this worked fantastic thank you, launched a prototype on heroku here- https://oauth-shiny.herokuapp.com

I made a modification where the token is saved to the environment, but a stop_for_status(resp) before launching UI in order to avoid running any of the server function.

Is there a reason why you would want the # TODO: check for success/failure here in the server vs the uiFunc (see below)?

uiFunc <- function(req) {
  if (!has_auth_code(parseQueryString(req$QUERY_STRING))) {
    url <- oauth2.0_authorize_url(api, app, scope = scope)
    redirect <- sprintf("location.replace(\"%s\");", url)
    tags$script(HTML(redirect))
  } else {
     # Manually create a token
     token <<- oauth2.0_token(
         app = app,
         endpoint = api,
         credentials = oauth2.0_access_token(api, app, parseQueryString(req$QUERY_STRING)$code),
         cache = FALSE
       )
     # validate before launching UI
     resp <- GET("https://api.github.com/user", config(token = token))
     stop_for_status(resp)
     ui
  }
}

to avoid resetting the token you then refer to it directly in the server,

server <- function(input, output, session) {
  params <- parseQueryString(isolate(session$clientData$url_search))
  if (!has_auth_code(params)) {
    return()
  }
  
  # Use the token created above
  resp <- GET("https://api.github.com/user", config(token = token))
  output$code <- renderText(content(resp, "text"))
}

btw, thank you as always for being an amazing resource

@ashbaldry
Copy link

This has worked for me and have managed to get it working using a different type of authentication, thanks very much for that!

My one problem is that the return URL I get doesn't involve a query but instead a hash and am struggling to find the # equivalent of req$QUERY_STRING. This is the first time I have seen the UI used as a function so struggling to find the location I need.

@vincentliu98
Copy link

vincentliu98 commented Jul 28, 2018

@ashbadry I am in the same situation as you are. The token that I want to retrieve is from an url starting with a pound sign. I have tried many ways but they all didn't work.

These are couple problems I have:

  1. I am unable to retrieve the url with # sign from global environment
  2. I tried to move the authentication page in the beginning part of the server, the authentication page won't appear.

I don't understand how req$QUERY_STRING works, and there is no such thing as req$URL_HASH. Is there any solution to solve this problem?

@debshila
Copy link

Hello,
I am trying to implement the oauth2.0 flow for a flex dashboard on a Mac Osx (10.14.5), R (3.5.3), and RStudio (1.2.1326). While currently I am able to put in my username and password, it just gets stuck on the flexdashboard loading screen.This is cross posted on Stackoverflow. Below is a minimally reproducible example of what I tried.

---
title: "Question"
output: 
    flexdashboard::flex_dashboard:
        orientation: rows
        vertical_layout: fill
fontsize: 12
runtime: shiny
---
```{r oauth1}    
options(shiny.host = '0.0.0.0', shiny.port = 8100, shiny.trace = TRUE)  

APP_URL <- "https://localhost:8100/"

if (interactive()) {
   # testing url
   cat(file=stderr(), "starting in interactive mode!")
   APP_URL <- "http://localhost:8100/"
 } else {
   # deployed URL
   cat(file=stderr(), "starting in non-interactive mode!")
   APP_URL <- example_url
}

app <- oauth_app("Accounts",
                 key = KEY,# Add key value here
                 secret = SECRET,# Add secret value here
                 redirect_uri = APP_URL
)

api <- oauth_endpoint(
  authorize = "https://example_url/oauth/authorize",
  access = "https://example_url/oauth/token"
)

scope <- ""

has_auth_code <- function(params) {
  urlParams = parseQueryString(isolate(session$clientData$url_search))
  browser()
  }

```

```{r oauth2}
# Manually create a token
token <- oauth2.0_token(
    app = app,
    endpoint = api,
    cache = FALSE)
)
save(token, file="ox_oauth")

params <- parseQueryString(isolate(session$clientData$url_search))

resp <-GET("https://example_url_1/api/user", config(token = token))
#stop_for_status(resp)

cat(file=stderr(), "Looking for response")
cat(file=stderr(), resp)
```

```{r ui}
uiFunc <- function(req) {
  cat(file=stderr(), "starting UI function")
  if (!has_auth_code(parseQueryString(req$QUERY_STRING))) {
    url <- oauth2.0_authorize_url(api, app, scope = scope)
    cat(file=stderr(), url)
    redirect <- sprintf("location.replace(\"%s\");", url)
    tags$script(HTML(redirect))
  } else {
    ui
  }
}
```

```
Page 1
=====================================================================
row {data-width=800 data-height=100 .tabset .tabset-fade}
-----------------------------------------------------------------------

### Section 1

```{r pg1sec1}
selectizeInput(
    "A_Type",
    label = "Assignment type",
    choices = c("HW", "R"),
    multiple = TRUE,
    # selectize = TRUE,
    options = list(placeholder = "Select assignment type(s)"),
    width = '98%',
    selected = ""
)
```

```{r og1sec1.1}
observeEvent(input$A_Type, {
    x <- input$A_Type
    updateSelectizeInput(session, "A_Type2",
                        selected = x)
})
```
```
Page 2
=====================================================================
row {data-width=800 data-height=100 .tabset .tabset-fade}
-----------------------------------------------------------------------

### Section 1

```{r pg2sec1}
selectizeInput(
    "A_Type2",
    label = "Assignment type",
    choices = c("HW", "R"),
    multiple = TRUE,
    width = '98%'
)
```

I get this output:

Waiting for authentication in browser...
    Press Esc/Ctrl + C to abort
    Please point your browser to the following url: 
        https://example_url/oauth/authorize?client_id=KEY&redirect_uri=XXX%2F&response_type=code&state=XXX

When I paste this url in a browser window, I am led through the oauth flow (able to add in username, pwd) and then I get to the flexdashboard loading page and just get stuck there.

Would appreciate any input. Thanks!

@mbacou
Copy link

mbacou commented Aug 14, 2019

@blakiseskream's (@hadley) approach above works well with Google OAuth, but I'm trying to figure how to avoid the ERROR: Unauthorized (HTTP 401) on page refresh, which you can observe at https://oauth-shiny.herokuapp.com as well. This breaks Shiny Server's "reload" feature.

The exact error on my end when I reload the app page is Warning: Error in oauth2.0_access_token: Bad Request (HTTP 400). Failed to get an access token. I have to remove all URL params and go through the OAuth flow again for the app to load again.

# ui.R

app <- oauth_app("my-app",
  key = "XXX",
  secret = "XXX",
  redirect_uri = "https://mydomain.com/my-app"
)
api <- oauth_endpoints("google")
scope <- "openid email profile"
params <- list(prompt="select_account")
has_auth_code <- function(params) return(!is.null(params$code))

# Main ----
uiFunc <- function(req) {
  if (interactive()) {
    # Skip OAuth flow
    return(ui)
  }
  if (!has_auth_code(parseQueryString(req$QUERY_STRING))) {
    # OAuthorize
    url <- oauth2.0_authorize_url(api, app, scope=scope, query_extra=params)
    redirect <- sprintf("location.replace(\"%s\");", url)
    tags$script(HTML(redirect))
  } else {
    # Manually create a token
    token <- oauth2.0_token(
      endpoint = api,
      app = app,
      credentials = oauth2.0_access_token(api, app, parseQueryString(req$QUERY_STRING)$code, user_params=params),
      cache = TRUE
    )
    # Get user details
    user <- GET("https://openidconnect.googleapis.com/v1/userinfo", config(token=token))
    stop_for_status(user)
    user <<- content(user, as="parsed")
    return(ui)
  }
}

@mariafb
Copy link

mariafb commented Oct 20, 2020

not working for me - i'm getting the following error:
Error in curl::curl_fetch_memory(url, handle = handle) :
Could not resolve host: github.com
Anyone can help me ?

@mariafb
Copy link

mariafb commented Oct 21, 2020

How do we get the access_token from redirect URL implicit grant flow? In your example, you use the 'code'.

@phileas-condemine
Copy link

Thanks for the great source of inspiration.
I forked and made some modifications to handle both simple user/password authentication (admin / azerty) and github SSO
https://gist.github.com/phileas-condemine/4c57ce92e044adb5d9ce6ea155b02e24
@hadley, still I don't quite understand the uiFunc magic, is there a doc that explains this feature (other that bookmarking) ?
Also this tags$script(HTML(redirect)) seems very powerful but also a bit dangerous as it resets reactiveVal and other objects defined in the server function definition, right ?

@PraveenBeedanal
Copy link

I am using ui.R and server. R files for the ShinyApp. But i don't get the "Session" object in the server file once the app gets redirected.
May i know, what is the problem in uiFunc()?

@aleszib
Copy link

aleszib commented Aug 24, 2023

First, thank you very much for this. I have been successfully using this for some time.

For someone with the same problems, this works nicely if it shiny app is run "Externa" (in RStudio). However, if it is run in Viewer Pane, the code does not work as expected, as instead of opening just one page/tab with authentication page, new authentication pages keep poping up until I kill the R process.

@liqi6811
Copy link

liqi6811 commented Mar 8, 2024

@aleszib I have the same issue as you, I use the code for Azure authentication, the new authentication pages keep on popping up. How did you manage to solve the problem? Thanks.

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