R使用AWS Cognito进行Shiny身份验证

时间:2017-04-19 20:25:16

标签: r authentication shiny amazon-cognito shiny-server

我正在将R Studio Server与R Shiny结合使用,在Ubuntu 16.04上运行。一切正常。我想保护R Shiny仪表板(用户名+ pw),我正在考虑建立一个与AWS Cognito通信的小网页来验证用户。

我找不到任何关于此组合的文档(Shiny + Cognito),但确实找到了一些关于R Shiny Authentication(使用NGINX + Auth0)和使用Cognito(例如与NodeJS结合使用)的文档)。

Shiny和Cognito(例如PHP或Node JS)的组合是否合乎逻辑且安全?什么是最好的方法:一个带有一些PHP的简单网页,或一个包含Shiny的Node JS应用程序?

我意识到这个问题相当广泛,但是因为我确信我不是唯一一个在这个问题上走来走去的人,所以我仍然要求每个人都能从可能的解决方案中获益。

2 个答案:

答案 0 :(得分:2)

这是我已实施的设置的说明。这是结合使用AWS Cognito和AWS特定功能。

上下文:我有一堆闪亮的应用程序,打包在容器中(通常使用asachet/shiny-basethese Dockerfiles中的一个作为基础)。我想私下托管它们,并控制谁可以访问它们。

下面的设置是闪亮代理的替代方法。实际上,它不需要任何类型的闪亮服务器。每个应用程序仅依靠shiny。每个容器都公开一个端口(例如EXPOSE 3838),并仅通过runApp(".", host="0.0.0.0", port=3838)启动。伸缩策略负责根据需要启动和停止容器。身份验证逻辑与应用程序代码完全脱钩。

我的云设置是:

  • 应用程序负载平衡器(ALB)用作用户入口点。您必须使用HTTPS侦听器来设置身份验证。我只是将HTTP流量重定向到HTTPS。
  • 每个应用程序的弹性容器服务(ECS)任务+服务。这可确保我的应用程序配置充分并完全独立运行。每个应用程序都可以具有独立的扩展策略,因此每个应用程序都具有适当的流量资源。您甚至可以将应用程序配置为自动启动/停止以节省大量资源。显然,这些应用必须是私有的,即只能从ALB访问。
  • 每个ECS都有一个不同的ALB目标组,因此对app1.example.com的请求将转发到app1,将app2.example.com转发到app2,依此类推。所有这些都在ALB规则。在这里我们可以轻松添加身份验证。

我有一个允许用户访问应用程序的用户帐户的Cognito“用户池”。这可用于在流量级别而非应用程序级别限制对应用程序的访问。

为此,您首先需要在Cognito用户池中创建一个客户端应用程序。对于app1,我将使用“授权代码授予”流以openid范围和app1.example.com/oauth2/idpresponse作为回调URL来创建Cognito客户端应用。

完成此操作后,您只需进入ALB规则并添加身份验证作为转发的先决条件:

ALB rule example

从现在开始,必须先验证app1.example.com上的流量,然后才能将其转发到app1。未经身份验证的请求将被重定向到Cognito托管用户界面(类似example.auth.eu-west-2.amazoncognito.com),以输入其凭据。您可以在Cognito设置中自定义托管UI的外观。

有用的链接

要将R代码包装在容器中:

使用ALB设置Cognito身份验证:

答案 1 :(得分:0)

您可以利用AWS Cognito API进行身份验证。我写了一篇有关here的帖子。

为使该答案独立,以下是详细信息。基本上,您需要做的是在应用程序的global.r文件中使用以下代码:

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))

}

server.r中,您可以这样使用它:

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's name as stored and returned by cognito
                          email = NULL)  # user's 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)
    })
}

ui.r看起来像这样:

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")))
    )
)