使用AWS Cognito的R身份验证
我正在将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的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)的组合是否合乎逻辑且安全?最好的方法是
我意识到这个问题相当广泛,但因为我确信我不是唯一一个提出这个问题的人,所以我仍然会问,这样每个人都可以从可能的解决方案中获益。以下是我实施的设置的描述。这是使用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
等。这都是在ALB规则中设置的。这是我们可以轻松添加身份验证的地方app2
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代码:
- 及
- 特别是我的个人
- 穿行:(其中包含)
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配合得很好(指针:)