使用AWS Cognito的R身份验证

使用AWS Cognito的R身份验证,r,authentication,shiny,amazon-cognito,shiny-server,R,Authentication,Shiny,Amazon Cognito,Shiny Server,我正在将R Studio Server与R Shining结合使用,运行在Ubuntu 16.04上。一切正常。我想保护R闪亮的仪表盘(username+pw),我正在考虑构建一个小网页,与AWS Cognito通信以验证用户 我找不到任何关于这种组合的文档(Shinny+Cognito),但确实找到了一些关于两者的文档(使用NGINX+Auth0)和Cognito的使用(例如与NodeJS组合) Shining和Cognito(例如PHP或Node JS)的组合是否合乎逻辑且安全?最好的方法是

我正在将R Studio Server与R Shining结合使用,运行在Ubuntu 16.04上。一切正常。我想保护R闪亮的仪表盘(username+pw),我正在考虑构建一个小网页,与AWS Cognito通信以验证用户

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

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


我意识到这个问题相当广泛,但因为我确信我不是唯一一个提出这个问题的人,所以我仍然会问,这样每个人都可以从可能的解决方案中获益。

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

上下文:我有一堆闪亮的应用程序,包装在容器中(通常使用
asachet/shinny base
或其中一个作为基础)。我想私下接待他们,并控制谁可以访问他们

下面的设置是闪亮代理的替代方案。事实上,它不需要任何闪亮的服务器。每个应用程序仅依赖于
shinny
。每个容器都公开一个端口(例如,
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/idpsresponse
作为回调URL的“授权代码授予”流创建一个Cognito客户端应用程序

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

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

有用的链接 对于在容器中包装R代码:

  • 特别是我的个人
要使用ALB设置Cognito身份验证:

  • 穿行:(其中包含)

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

为了使这个答案更加完整,以下是简短的细节。基本上,您需要做的是在应用程序的
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))

}
ui.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)
    })
}
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")))
    )
)

您是否愿意使用shinyproxy部署您的闪亮应用程序?如果是,您有很多认证选项。我在安装@antoine sac之后测试了一个闪亮的应用程序。很好用,谢谢。一个问题:你是对所有应用使用一个集群,然后每个应用都有一个服务+任务,还是每个应用都有一个单独的集群?我对所有应用都使用了一个集群,但我认为这并不重要。无论如何,ELB应该能够指向ECS服务。作为读者的补充说明,我注意到当容器长时间运行时内存泄漏。不管实际使用情况如何,内存消耗每天缓慢增加约10MB。这在几周后变得很明显。我还没有设法摆脱它,所以我已经切换到shinyproxy解决方案,用于全天候运行的应用程序。Shinyproxy与cognito配合得很好(指针:)