R Brilliant Authentication Using AWS Cognito

I am using R Studio Server in combination with R Shiny running on Ubuntu 16.04. Everything works fine, I want to secure the R Shiny dashboards (username + pw) and I am thinking of creating a small web page that communicates with AWS Cognito to validate users.

I cannot find any documentation on this combination (Shiny + Cognito), but I find quite detailed documentation on both R Shiny Authentication (using NGINX + Auth0) and using Cognito (for example, in combination with NodeJS).

Is the combination of Shiny and Cognito (e.g. PHP or Node JS) logical and secure? What's the best thing to do: a simple web page with PHP or a Node JS application with Shiny included?

I realize this question is pretty broad, but since I'm sure I'm not the only one hanging around with these questions, I'm still asking so that everyone can benefit from possible solutions.

+10


source to share


2 answers


Here's a description of the customization I implemented. This is leveraged by AWS Cognito along with AWS specific features.

Context: I have some shiny applications packed in containers (usually using asachet/shiny-base

either or one of these Dockerfile

s
as a base). I want to host them privately and control who can access them.

The setup below is an alternative to the shiny proxy. In fact, he doesn't need any shiny server. Every application just relies on shiny

. Each of the containers provides a port (for example EXPOSE 3838

) and is simply started with runApp(".", host="0.0.0.0", port=3838)

. Scale policies will take care of starting and stopping containers as needed. The authentication logic is completely decoupled from the application code.

My cloud setup:

  • The Application Load Balancer (ALB) is used as the user's entry point. You must use an HTTPS listener to set up authentication. I am just redirecting HTTP traffic to HTTPS.
  • Elastic Container Service (ECS) task + service per application. This will ensure that my applications are properly provisioned and run completely independently. Each application can have an independent scaling policy, so each application has the necessary resources for its traffic. You can even set apps to start / stop automatically to save a lot of resources. Obviously, applications must be private, i.e. available only from ALB.
  • Each ECS has its own ALB target group, so requests to are app1.example.com

    redirected to app1

    , app2.example.com

    to app2

    , etc. All of this is set out in the ALB rules. We can easily add authentication here.

I have a Cognito "user pool" with user accounts that are allowed to access applications. This can be used to restrict access to an application at the traffic level, rather than at the application level.

To do this, you first need to create a client application in your Cognito user pool. For, app1

I would create a Cognito client application using the Scoped Authorization Code flow openid

and app1.example.com/oauth2/idpresponse

as the callback url.

After that, you can simply go into ALB rules and add authentication as a prerequisite for forwarding:



ALB rule example

From this point on, traffic app1.example.com

must not be authenticated before being sent to app1

. Unauthenticated requests will be redirected to the Cognito Hosted UI (sort of example.auth.eu-west-2.amazoncognito.com

) to enter their credentials. You can customize the look of the hosted UI in the Cognito settings.

useful links

To package R code in a container:

To set up Cognito authentication with ALB:

+3


source


You can use AWS Cognito API for authentication. I wrote a post about this here .

To make this answer self-contained, here are the details in a nutshell. essentially, you need to use this code in global.r

your application file :

base_cognito_url <- "https://YOUR_DOMAIN.YOUR_AMAZON_REGION.amazoncognito.com/"
app_client_id <- "YOUR_APP_CLIENT_ID"
app_client_secret <- "YOUR_APP_CLIENT_SECRET"
redirect_uri <- "https://YOUR_APP/redirect_uri"

library(httr)

app <- oauth_app(appname = "my_shiny_app",
                 key = app_client_id,
                 secret = app_client_secret,
                 redirect_uri = redirect_uri)
cognito <- oauth_endpoint(authorize = "authorize",
                          access = "token",
                          base_url = paste0(base_cognito_url, "oauth2"))


retrieve_user_data <- function(user_code){

  failed_token <- FALSE

  # get the token
  tryCatch({token_res <- oauth2.0_access_token(endpoint = cognito,
                                              app = app,
                                              code = user_code,
                                              user_params = list(client_id = app_client_id,
                                                                 grant_type = "authorization_code"),
                                              use_basic_auth = TRUE)},
           error = function(e){failed_token <<- TRUE})

  # check result status, make sure token is valid and that the process did not fail
  if (failed_token) {
    return(NULL)
  }

  # The token did not fail, go ahead and use the token to retrieve user information
  user_information <- GET(url = paste0(base_cognito_url, "oauth2/userInfo"), 
                          add_headers(Authorization = paste("Bearer", token_res$access_token)))

  return(content(user_information))

}

      

In server.r

you use it like this:



library(shiny)
library(shinyjs)

# define a tibble of allwed users (this can also be read from a local file or from a database)
allowed_users <- tibble(
  user_email = c("user1@example.com",
                 "user2@example.com"))

function(input, output, session){

   # initialize authenticated reactive values ----
   # In addition to these three (auth, name, email)
   # you can add additional reactive values here, if you want them to be based on the user which logged on, e.g. privileges.
   user <- reactiveValues(auth = FALSE, # is the user authenticated or not
                          name = NULL, # user name as stored and returned by cognito
                          email = NULL)  # user email as stored and returned by cognito

   # get the url variables ----
   observe({
        query <- parseQueryString(session$clientData$url_search)
        if (!("code" %in% names(query))){
            # no code in the url variables means the user hasn't logged in yet
            showElement("login")
        } else {
            current_user <- retrieve_user_data(query$code)
            # if an error occurred during login
            if (is.null(current_user)){
                hideElement("login")
                showElement("login_error_aws_flow")
                showElement("submit_sign_out_div")
                user$auth <- FALSE
            } else {
                # check if user is in allowed user list
                # for more robustness, use stringr::str_to_lower to avoid case sensitivity
                # i.e., (str_to_lower(current_user$email) %in% str_to_lower(allowed_users$user_email))
                if (current_user$email %in% allowed_users$user_email){
                    hideElement("login")
                    showElement("login_confirmed")
                    showElement("submit_sign_out_div")

                    user$auth <- TRUE
                    user$email <- current_user$email
                    user$name <- current_user$name

                    # ==== User is valid, continue prep ====

                    # show the welcome box with user name
                    output$confirmed_login_name <-
                        renderText({
                            paste0("Hi there!, ",
                                    user$name)
                        })

                    # ==== Put additional login dependent steps here (e.g. db read from source) ====

                    # ADD HERE YOUR REQUIRED LOGIC
                    # I personally like to select the first tab for the user to see, i.e.:
                    showTab("main_navigation", "content_tab_id", select = TRUE) 
                    # (see the next chunk for how this tab is defined in terms of ui elements)

                    # ==== Finish loading and go to tab ====

                } else {
                    # user not allowed. Only show sign-out, perhaps also show a login error message.
                    hideElement("login")
                    showElement("login_error_user")
                    showElement("submit_sign_out_div")
                }
            }
        }
    })

   # This is where you will put your actual elements (the server side that is) ----
   # For example:

    output$some_plot <- renderPlot({
        # *** THIS IS EXTREMELY IMPORTANT!!! ***
        validate(need(user$auth, "No privileges to watch data. Please contact support."))
        # since shinyjs is not safe for hiding content, make sure that any information is covered
        # by the validate(...) expression as was specified. 
        # Rendered elements which were not preceded by a validate expression can be viewed in the html code (even if you use hideElement).

        # only if user is confirmed the information will render (a plot in this case)
        plot(cars)
    })
}

      

And ui.r

it looks like this:

library(shiny)
library(shinyjs)

fluidPage(
    useShinyjs(), # to enable the show/hide of elements such as login and buttons
    hidden( # this is how the logout button will like:
        div(
            id = "submit_sign_out_div",
            a(id = "submit_sign_out",
              "logout",
              href = aws_auth_logout,
              style = "color: black; 
              -webkit-appearance: button; 
              -moz-appearance: button; 
              appearance: button; 
              text-decoration: none; 
              background:#ff9999; 
              position: absolute; 
              top: 0px; left: 20px; 
              z-index: 10000;
              padding: 5px 10px 5px 10px;"
              )
            )
    ),
    navbarPage(
        "Cognito auth example",
        id = "main_navigation",
        tabPanel(
            "identification",
            value = "login_tab_id",
            h1("Login"),
            div(
                id = "login",
                p("To login you must identify with a username and password"),
                # This defines a login button which upon click will redirect to the AWS Cognito login page
                a(id = "login_link",
                  "Click here to login",
                  href = aws_auth_redirect,
                  style = "color: black;
                  -webkit-appearance: button;
                  -moz-appearance: button;
                  appearance: button;
                  text-decoration: none;
                  background:#95c5ff;
                  padding: 5px 10px 5px 10px;")
            ),
            hidden(div(
                id = "login_error_aws_flow",
                p("An error has occurred."),
                p("Please contact support")
            )),
            hidden(
                div(
                    id = "login_confirmed",
                    h3("User confirmed"),
                    fluidRow(
                        textOutput("confirmed_login_name")),
                    fluidRow(
                        p("Use the menu bar to navigate."),
                        p(
                            "Don't forget to logout when you want to close the system."
                        )
                    )
                )
            ),
        ),
        tabPanel("Your actual content", 
                 value = "content_tab_id",
                 fluidRow(plotOutput("some_plot")))
    )
)

      

0


source







All Articles