【问题标题】:R Shiny authentication using AWS Cognito使用 AWS Cognito 的 R Shiny 身份验证
【发布时间】:2017-09-16 06:08:51
【问题描述】:

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

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

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

我意识到这个问题相当广泛,但由于我确定我不是唯一一个带着这个问题四处走动的人,所以我仍然会问,以便每个人都可以从可能的解决方案中受益。

【问题讨论】:

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


【解决方案1】:

这是我实施的设置的描述。这是使用 AWS Cognito 以及 AWS 特定的功能。

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

下面的设置是闪亮代理的替代方案。事实上,它不需要任何闪亮的服务器。每个应用程序仅依赖于shiny。每个容器都暴露一个端口(例如EXPOSE 3838),并简单地使用runApp(".", host="0.0.0.0", port=3838) 启动。扩展策略负责根据需要启动和停止容器。身份验证逻辑与应用代码完全解耦。

我的云设置是:

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

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

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

完成后,您可以简单地进入 ALB 规则并添加身份验证作为转发的先决条件:

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

有用的链接

用于在容器中打包 R 代码:

使用 ALB 设置 Cognito 身份验证:

【讨论】:

  • 我在你的设置@antoine-sac 之后做了一个测试闪亮的应用程序。它运作良好,谢谢。一个问题:您是为所有应用使用一个集群,然后每个应用都有一个服务+任务,还是实际上每个应用都有一个单独的集群?
  • 我已经为所有应用程序使用了一个集群,但我认为这并不重要。无论如何,ELB 应该能够指向 ECS 服务。
  • 作为读者的补充说明,我注意到容器长时间运行时内存泄漏。无论实际使用情况如何,内存消耗每天都在缓慢增加约 10MB。这在几周后变得明显。我还没有设法解决它,所以我已经为 24/7 运行的应用程序切换到了闪亮代理解决方案。 Shinyproxy 与 cognito 配合得很好(指针:github.com/openanalytics/shinyproxy/issues/…
【解决方案2】:

您可以使用 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")))
    )
)

【讨论】:

    猜你喜欢
    • 2018-01-29
    • 2017-01-02
    • 2016-06-25
    • 1970-01-01
    • 2018-03-14
    • 2018-05-22
    • 2021-04-15
    • 1970-01-01
    • 2016-12-22
    相关资源
    最近更新 更多