├── .Rbuildignore ├── docs ├── pkgdown.yml ├── link.svg ├── docsearch.js ├── pkgdown.js ├── 404.html ├── authors.html ├── pkgdown.css ├── reference │ ├── runShinyExample.html │ ├── index.html │ ├── logoutUI.html │ ├── loginUI.html │ ├── logout.html │ └── login.html ├── LICENSE-text.html ├── news │ └── index.html ├── docsearch.css └── index.html ├── .gitignore ├── NAMESPACE ├── inst └── shiny-examples │ └── shinyauthr_example │ ├── returnClick.js │ └── app.R ├── man ├── runShinyExample.Rd ├── logoutUI.Rd ├── loginUI.Rd ├── logout.Rd └── login.Rd ├── R ├── runExample.R ├── logout.R └── login.R ├── DESCRIPTION ├── _pkgdown.yml ├── NEWS.md ├── LICENSE └── README.md /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^_pkgdown\.yml$ 4 | ^docs$ 5 | ^pkgdown$ 6 | -------------------------------------------------------------------------------- /docs/pkgdown.yml: -------------------------------------------------------------------------------- 1 | pandoc: 2.3.1 2 | pkgdown: 1.4.1 3 | pkgdown_sha: ~ 4 | articles: [] 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | *.Rproj 6 | inst/shiny-examples/shinyauthr_example/rsconnect/ -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(login) 4 | export(loginUI) 5 | export(logout) 6 | export(logoutUI) 7 | export(runShinyExample) 8 | importFrom(rlang,":=") 9 | -------------------------------------------------------------------------------- /inst/shiny-examples/shinyauthr_example/returnClick.js: -------------------------------------------------------------------------------- 1 | $(document).keyup(function(event) { 2 | if ($("#login-password").is(":focus") && (event.keyCode == 13)) { 3 | $("#login-button").click(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /man/runShinyExample.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/runExample.R 3 | \name{runShinyExample} 4 | \alias{runShinyExample} 5 | \title{Run shinyauthr example} 6 | \usage{ 7 | runShinyExample() 8 | } 9 | \description{ 10 | Launch example shiny dashboard using shinyauthr authentication modules 11 | } 12 | \author{ 13 | Paul Campbell, \email{pacampbell91@gmail.com} 14 | } 15 | -------------------------------------------------------------------------------- /R/runExample.R: -------------------------------------------------------------------------------- 1 | #' Run shinyauthr example 2 | #' 3 | #' Launch example shiny dashboard using shinyauthr authentication modules 4 | #' 5 | #' @author Paul Campbell, \email{pacampbell91@gmail.com} 6 | #' 7 | #' @export 8 | runShinyExample <- function() { 9 | appDir <- system.file("shiny-examples", "shinyauthr_example", package = "shinyauthr") 10 | if (appDir == "") { 11 | stop("Could not find example directory. Try re-installing `shinyauthr`.", call. = FALSE) 12 | } 13 | 14 | shiny::runApp(appDir, display.mode = "normal") 15 | } -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: shinyauthr 2 | Type: Package 3 | Title: Shiny Authentication Modules 4 | Version: 0.1.0 5 | Authors@R: 6 | person("Paul", "Campbell", email = "pacampbell91@gmail.com", role = c("aut", "cre")) 7 | Description: Easily add user authentication to shiny apps and build dynamic UIs based on user information. 8 | Maintainer: Paul Campbell 9 | License: MIT + file LICENSE 10 | Encoding: UTF-8 11 | LazyData: true 12 | Imports: 13 | shiny, 14 | shinyjs, 15 | dplyr, 16 | rlang, 17 | sodium 18 | RoxygenNote: 6.1.1 19 | URL: https://github.com/paulc91/shinyauthr 20 | BugReports: https://github.com/paulc91/shinyauthr/issues 21 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | destination: docs 2 | url: 3 | 4 | authors: 5 | Paul Campbell: 6 | href: https://github.com/paulc91 7 | 8 | template: 9 | params: 10 | bootswatch: simplex 11 | 12 | navbar: 13 | left: 14 | - icon: fa-home fa-lg 15 | href: index.html 16 | - text: Reference 17 | href: reference/index.html 18 | - text: Changelog 19 | href: news/index.html 20 | right: 21 | - icon: fa-github fa-lg 22 | href: https://github.com/paulc91/shinyauthr 23 | 24 | reference: 25 | - title: "Login" 26 | desc: > 27 | Login modules 28 | contents: 29 | - login 30 | - loginUI 31 | - title: "Logout" 32 | desc: > 33 | Logout modules 34 | contents: 35 | - logout 36 | - logoutUI 37 | -------------------------------------------------------------------------------- /man/logoutUI.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/logout.R 3 | \name{logoutUI} 4 | \alias{logoutUI} 5 | \title{logout UI module} 6 | \usage{ 7 | logoutUI(id, label = "Log out", icon = NULL, class = "btn-danger", 8 | style = "color: white;") 9 | } 10 | \arguments{ 11 | \item{id}{Shiny id} 12 | 13 | \item{label}{label for the logout button} 14 | 15 | \item{icon}{An optional \code{\link[shiny]{icon}} to appear on the button.} 16 | 17 | \item{class}{bootstrap class for the logout button} 18 | 19 | \item{style}{css styling for the logout button} 20 | } 21 | \value{ 22 | Shiny UI action button 23 | } 24 | \description{ 25 | Shiny UI Module for use with \link{logout} 26 | } 27 | \details{ 28 | Call via \code{logoutUI("your_id")} 29 | } 30 | -------------------------------------------------------------------------------- /docs/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /man/loginUI.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/login.R 3 | \name{loginUI} 4 | \alias{loginUI} 5 | \title{login UI module} 6 | \usage{ 7 | loginUI(id, title = "Please log in", user_title = "User Name", 8 | pass_title = "Password", login_title = "Log in", 9 | error_message = "Invalid username or password!") 10 | } 11 | \arguments{ 12 | \item{id}{Shiny id} 13 | 14 | \item{title}{header title for the login panel} 15 | 16 | \item{user_title}{label for the user name text input} 17 | 18 | \item{pass_title}{label for the password text input} 19 | 20 | \item{login_title}{label for the login button} 21 | 22 | \item{error_message}{message to display after failed login} 23 | } 24 | \value{ 25 | Shiny UI 26 | } 27 | \description{ 28 | Shiny UI Module for use with \link{login} 29 | } 30 | \details{ 31 | Call via \code{loginUI("your_id")} 32 | } 33 | \author{ 34 | Paul Campbell, \email{pacampbell91@gmail.com} 35 | } 36 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # shinyauthr 0.1.0 2 | 3 | * Prepare for CRAN submission 4 | 5 | # shinyauthr 0.0.99 6 | 7 | ### April 2019 - switched to the sodium package for password hashing and decryption 8 | 9 | If you plan to use hashed passwords with shinyauthr you now must use the [sodium package](https://github.com/jeroen/sodium) to do hash your passwords and the `sodium_hashed = TRUE` argument of the `shinyauthr::login` module call for decrpytion to work appropriately. 10 | 11 | This means that previously used `hashed` and `algo` arguments that interfaced with the `digest` package are now deprecated. If you had previously hashed your passwords with the digest package to use with shinyauthr, please re-hash them with `sodium` and use the `sodium_hashed` argument instead. 12 | 13 | Sorry for this breaking change, but sodium hashing provides added protection against brute-force attacks on stored passwords. More information on this [here](https://download.libsodium.org/doc/password_hashing/). 14 | -------------------------------------------------------------------------------- /man/logout.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/logout.R 3 | \name{logout} 4 | \alias{logout} 5 | \title{logout server module} 6 | \usage{ 7 | logout(input, output, session, active) 8 | } 9 | \arguments{ 10 | \item{input}{shiny input} 11 | 12 | \item{output}{shiny output} 13 | 14 | \item{session}{shiny session} 15 | 16 | \item{active}{[reactive] supply the returned \code{user_auth} boolean reactive from \link{login} 17 | here to hide/show the logout button} 18 | } 19 | \value{ 20 | The reactive output of this module should be supplied as the \code{log_out} argument to the 21 | \link{login} module to trigger the logout process 22 | } 23 | \description{ 24 | Shiny authentication module for use with \link{logoutUI} 25 | } 26 | \details{ 27 | Call via \code{shiny::callModule(logout, "your_id", ...)} 28 | } 29 | \examples{ 30 | \dontrun{ 31 | logout_init <- shiny::callModule(logout, "logout", 32 | active = reactive(user_credentials()$user_auth)) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Campbell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /R/logout.R: -------------------------------------------------------------------------------- 1 | #' logout UI module 2 | #' 3 | #' Shiny UI Module for use with \link{logout} 4 | #' 5 | #' Call via \code{logoutUI("your_id")} 6 | #' 7 | #' @param id Shiny id 8 | #' @param label label for the logout button 9 | #' @param icon An optional \code{\link[shiny]{icon}} to appear on the button. 10 | #' @param class bootstrap class for the logout button 11 | #' @param style css styling for the logout button 12 | #' 13 | #' @return Shiny UI action button 14 | #' 15 | #' @export 16 | logoutUI <- function(id, label = "Log out", icon = NULL, class = "btn-danger", style = "color: white;") { 17 | ns <- shiny::NS(id) 18 | 19 | shinyjs::hidden( 20 | shiny::actionButton(ns("button"), label, icon = icon, class = class, style = style) 21 | ) 22 | } 23 | 24 | #' logout server module 25 | #' 26 | #' Shiny authentication module for use with \link{logoutUI} 27 | #' 28 | #' Call via \code{shiny::callModule(logout, "your_id", ...)} 29 | #' 30 | #' @param input shiny input 31 | #' @param output shiny output 32 | #' @param session shiny session 33 | #' @param active [reactive] supply the returned \code{user_auth} boolean reactive from \link{login} 34 | #' here to hide/show the logout button 35 | #' 36 | #' @return The reactive output of this module should be supplied as the \code{log_out} argument to the 37 | #' \link{login} module to trigger the logout process 38 | #' 39 | #' @examples 40 | #' \dontrun{ 41 | #' logout_init <- shiny::callModule(logout, "logout", 42 | #' active = reactive(user_credentials()$user_auth)) 43 | #' } 44 | #' 45 | #' @export 46 | logout <- function(input, output, session, active) { 47 | 48 | shiny::observeEvent(active(), ignoreInit = TRUE, { 49 | shinyjs::toggle(id = "button", anim = TRUE, time = 1, animType = "fade") 50 | }) 51 | 52 | # return reactive logout button tracker 53 | shiny::reactive({input$button}) 54 | } 55 | -------------------------------------------------------------------------------- /man/login.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/login.R 3 | \name{login} 4 | \alias{login} 5 | \title{login server module} 6 | \usage{ 7 | login(input, output, session, data, user_col, pwd_col, 8 | sodium_hashed = FALSE, hashed, algo, log_out = NULL) 9 | } 10 | \arguments{ 11 | \item{input}{shiny input} 12 | 13 | \item{output}{shiny output} 14 | 15 | \item{session}{shiny session} 16 | 17 | \item{data}{data frame or tibble containing usernames, passwords and other user data} 18 | 19 | \item{user_col}{bare (unquoted) column name containing usernames} 20 | 21 | \item{pwd_col}{bare (unquoted) column name containing passwords} 22 | 23 | \item{sodium_hashed}{have the passwords been hash encrypted using the sodium package? defaults to FALSE} 24 | 25 | \item{hashed}{Deprecated. shinyauthr now uses the sodium package for password hashing and decryption. If you have previously hashed your passwords with the digest package to use with shinyauthr please re-hash them with sodium for decryption to work.} 26 | 27 | \item{algo}{Deprecated} 28 | 29 | \item{log_out}{[reactive] supply the returned reactive from \link{logout} here to trigger a user logout} 30 | } 31 | \value{ 32 | The module will return a reactive 2 element list to your main application. 33 | First element \code{user_auth} is a boolean inditcating whether there has been 34 | a successful login or not. Second element \code{info} will be the data frame provided 35 | to the function, filtered to the row matching the succesfully logged in username. 36 | When \code{user_auth} is FALSE \code{info} is NULL. 37 | } 38 | \description{ 39 | Shiny authentication module for use with \link{loginUI} 40 | } 41 | \details{ 42 | Call via \code{shiny::callModule(login, "your_id", ...)} 43 | } 44 | \examples{ 45 | \dontrun{ 46 | user_credentials <- shiny::callModule(login, "login", 47 | data = user_base, 48 | user_col = user, 49 | pwd_col = password, 50 | log_out = reactive(logout_init())) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /docs/docsearch.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | // register a handler to move the focus to the search bar 4 | // upon pressing shift + "/" (i.e. "?") 5 | $(document).on('keydown', function(e) { 6 | if (e.shiftKey && e.keyCode == 191) { 7 | e.preventDefault(); 8 | $("#search-input").focus(); 9 | } 10 | }); 11 | 12 | $(document).ready(function() { 13 | // do keyword highlighting 14 | /* modified from https://jsfiddle.net/julmot/bL6bb5oo/ */ 15 | var mark = function() { 16 | 17 | var referrer = document.URL ; 18 | var paramKey = "q" ; 19 | 20 | if (referrer.indexOf("?") !== -1) { 21 | var qs = referrer.substr(referrer.indexOf('?') + 1); 22 | var qs_noanchor = qs.split('#')[0]; 23 | var qsa = qs_noanchor.split('&'); 24 | var keyword = ""; 25 | 26 | for (var i = 0; i < qsa.length; i++) { 27 | var currentParam = qsa[i].split('='); 28 | 29 | if (currentParam.length !== 2) { 30 | continue; 31 | } 32 | 33 | if (currentParam[0] == paramKey) { 34 | keyword = decodeURIComponent(currentParam[1].replace(/\+/g, "%20")); 35 | } 36 | } 37 | 38 | if (keyword !== "") { 39 | $(".contents").unmark({ 40 | done: function() { 41 | $(".contents").mark(keyword); 42 | } 43 | }); 44 | } 45 | } 46 | }; 47 | 48 | mark(); 49 | }); 50 | }); 51 | 52 | /* Search term highlighting ------------------------------*/ 53 | 54 | function matchedWords(hit) { 55 | var words = []; 56 | 57 | var hierarchy = hit._highlightResult.hierarchy; 58 | // loop to fetch from lvl0, lvl1, etc. 59 | for (var idx in hierarchy) { 60 | words = words.concat(hierarchy[idx].matchedWords); 61 | } 62 | 63 | var content = hit._highlightResult.content; 64 | if (content) { 65 | words = words.concat(content.matchedWords); 66 | } 67 | 68 | // return unique words 69 | var words_uniq = [...new Set(words)]; 70 | return words_uniq; 71 | } 72 | 73 | function updateHitURL(hit) { 74 | 75 | var words = matchedWords(hit); 76 | var url = ""; 77 | 78 | if (hit.anchor) { 79 | url = hit.url_without_anchor + '?q=' + escape(words.join(" ")) + '#' + hit.anchor; 80 | } else { 81 | url = hit.url + '?q=' + escape(words.join(" ")); 82 | } 83 | 84 | return url; 85 | } 86 | -------------------------------------------------------------------------------- /docs/pkgdown.js: -------------------------------------------------------------------------------- 1 | /* http://gregfranko.com/blog/jquery-best-practices/ */ 2 | (function($) { 3 | $(function() { 4 | 5 | $('.navbar-fixed-top').headroom(); 6 | 7 | $('body').css('padding-top', $('.navbar').height() + 10); 8 | $(window).resize(function(){ 9 | $('body').css('padding-top', $('.navbar').height() + 10); 10 | }); 11 | 12 | $('body').scrollspy({ 13 | target: '#sidebar', 14 | offset: 60 15 | }); 16 | 17 | $('[data-toggle="tooltip"]').tooltip(); 18 | 19 | var cur_path = paths(location.pathname); 20 | var links = $("#navbar ul li a"); 21 | var max_length = -1; 22 | var pos = -1; 23 | for (var i = 0; i < links.length; i++) { 24 | if (links[i].getAttribute("href") === "#") 25 | continue; 26 | // Ignore external links 27 | if (links[i].host !== location.host) 28 | continue; 29 | 30 | var nav_path = paths(links[i].pathname); 31 | 32 | var length = prefix_length(nav_path, cur_path); 33 | if (length > max_length) { 34 | max_length = length; 35 | pos = i; 36 | } 37 | } 38 | 39 | // Add class to parent
  • , and enclosing
  • if in dropdown 40 | if (pos >= 0) { 41 | var menu_anchor = $(links[pos]); 42 | menu_anchor.parent().addClass("active"); 43 | menu_anchor.closest("li.dropdown").addClass("active"); 44 | } 45 | }); 46 | 47 | function paths(pathname) { 48 | var pieces = pathname.split("/"); 49 | pieces.shift(); // always starts with / 50 | 51 | var end = pieces[pieces.length - 1]; 52 | if (end === "index.html" || end === "") 53 | pieces.pop(); 54 | return(pieces); 55 | } 56 | 57 | // Returns -1 if not found 58 | function prefix_length(needle, haystack) { 59 | if (needle.length > haystack.length) 60 | return(-1); 61 | 62 | // Special case for length-0 haystack, since for loop won't run 63 | if (haystack.length === 0) { 64 | return(needle.length === 0 ? 0 : -1); 65 | } 66 | 67 | for (var i = 0; i < haystack.length; i++) { 68 | if (needle[i] != haystack[i]) 69 | return(i); 70 | } 71 | 72 | return(haystack.length); 73 | } 74 | 75 | /* Clipboard --------------------------*/ 76 | 77 | function changeTooltipMessage(element, msg) { 78 | var tooltipOriginalTitle=element.getAttribute('data-original-title'); 79 | element.setAttribute('data-original-title', msg); 80 | $(element).tooltip('show'); 81 | element.setAttribute('data-original-title', tooltipOriginalTitle); 82 | } 83 | 84 | if(ClipboardJS.isSupported()) { 85 | $(document).ready(function() { 86 | var copyButton = ""; 87 | 88 | $(".examples, div.sourceCode").addClass("hasCopyButton"); 89 | 90 | // Insert copy buttons: 91 | $(copyButton).prependTo(".hasCopyButton"); 92 | 93 | // Initialize tooltips: 94 | $('.btn-copy-ex').tooltip({container: 'body'}); 95 | 96 | // Initialize clipboard: 97 | var clipboardBtnCopies = new ClipboardJS('[data-clipboard-copy]', { 98 | text: function(trigger) { 99 | return trigger.parentNode.textContent; 100 | } 101 | }); 102 | 103 | clipboardBtnCopies.on('success', function(e) { 104 | changeTooltipMessage(e.trigger, 'Copied!'); 105 | e.clearSelection(); 106 | }); 107 | 108 | clipboardBtnCopies.on('error', function() { 109 | changeTooltipMessage(e.trigger,'Press Ctrl+C or Command+C to copy'); 110 | }); 111 | }); 112 | } 113 | })(window.jQuery || window.$) 114 | -------------------------------------------------------------------------------- /inst/shiny-examples/shinyauthr_example/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(shinydashboard) 3 | library(dplyr) 4 | library(shinyjs) 5 | library(glue) 6 | library(shinyauthr) 7 | 8 | user_base <- data_frame( 9 | user = c("user1", "user2"), 10 | password = c("pass1", "pass2"), 11 | password_hash = sapply(c("pass1", "pass2"), sodium::password_store), 12 | permissions = c("admin", "standard"), 13 | name = c("User One", "User Two") 14 | ) 15 | 16 | ui <- dashboardPage( 17 | 18 | dashboardHeader(title = "shinyauthr", 19 | tags$li(class = "dropdown", style = "padding: 8px;", 20 | shinyauthr::logoutUI("logout")), 21 | tags$li(class = "dropdown", 22 | tags$a(icon("github"), 23 | href = "https://github.com/paulc91/shinyauthr", 24 | title = "See the code on github")) 25 | ), 26 | 27 | dashboardSidebar(collapsed = TRUE, 28 | div(textOutput("welcome"), style = "padding: 20px") 29 | ), 30 | 31 | dashboardBody( 32 | shinyjs::useShinyjs(), 33 | tags$head(tags$style(".table{margin: 0 auto;}"), 34 | tags$script(src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/3.5.16/iframeResizer.contentWindow.min.js", 35 | type="text/javascript"), 36 | includeScript("returnClick.js") 37 | ), 38 | shinyauthr::loginUI("login"), 39 | uiOutput("user_table"), 40 | uiOutput("testUI"), 41 | HTML('
    ') 42 | ) 43 | ) 44 | 45 | server <- function(input, output, session) { 46 | 47 | credentials <- callModule(shinyauthr::login, "login", 48 | data = user_base, 49 | user_col = user, 50 | pwd_col = password_hash, 51 | sodium_hashed = TRUE, 52 | log_out = reactive(logout_init())) 53 | 54 | logout_init <- callModule(shinyauthr::logout, "logout", reactive(credentials()$user_auth)) 55 | 56 | observe({ 57 | if(credentials()$user_auth) { 58 | shinyjs::removeClass(selector = "body", class = "sidebar-collapse") 59 | } else { 60 | shinyjs::addClass(selector = "body", class = "sidebar-collapse") 61 | } 62 | }) 63 | 64 | output$user_table <- renderUI({ 65 | # only show pre-login 66 | if(credentials()$user_auth) return(NULL) 67 | 68 | tagList( 69 | tags$p("test the different outputs from the sample logins below 70 | as well as an invalid login attempt.", class = "text-center"), 71 | 72 | renderTable({user_base[, -3]}) 73 | ) 74 | }) 75 | 76 | user_info <- reactive({credentials()$info}) 77 | 78 | user_data <- reactive({ 79 | req(credentials()$user_auth) 80 | 81 | if (user_info()$permissions == "admin") { 82 | dplyr::starwars[,1:10] 83 | } else if (user_info()$permissions == "standard") { 84 | dplyr::storms[,1:11] 85 | } 86 | 87 | }) 88 | 89 | output$welcome <- renderText({ 90 | req(credentials()$user_auth) 91 | 92 | glue("Welcome {user_info()$name}") 93 | }) 94 | 95 | output$testUI <- renderUI({ 96 | req(credentials()$user_auth) 97 | 98 | fluidRow( 99 | column( 100 | width = 12, 101 | tags$h2(glue("Your permission level is: {user_info()$permissions}. 102 | Your data is: {ifelse(user_info()$permissions == 'admin', 'Starwars', 'Storms')}.")), 103 | box(width = NULL, status = "primary", 104 | title = ifelse(user_info()$permissions == 'admin', "Starwars Data", "Storms Data"), 105 | DT::renderDT(user_data(), options = list(scrollX = TRUE)) 106 | ) 107 | ) 108 | ) 109 | }) 110 | 111 | } 112 | 113 | shiny::shinyApp(ui, server) 114 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Page not found (404) • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | 101 | 102 | 103 | 104 |
    105 | 106 |
    107 |
    108 | 111 | 112 | Content not found. Please use links in the navbar. 113 | 114 |
    115 | 116 |
    117 | 118 | 119 | 120 |
    121 | 124 | 125 |
    126 |

    Site built with pkgdown 1.4.1.

    127 |
    128 | 129 |
    130 |
    131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /docs/authors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Authors • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | 101 | 102 | 103 | 104 |
    105 | 106 |
    107 |
    108 | 111 | 112 |
      113 |
    • 114 |

      Paul Campbell. Author, maintainer. 115 |

      116 |
    • 117 |
    118 | 119 |
    120 | 121 |
    122 | 123 | 124 | 125 |
    126 | 129 | 130 |
    131 |

    Site built with pkgdown 1.4.1.

    132 |
    133 | 134 |
    135 |
    136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/pkgdown.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer */ 2 | 3 | /** 4 | * Basic idea: https://philipwalton.github.io/solved-by-flexbox/demos/sticky-footer/ 5 | * Details: https://github.com/philipwalton/solved-by-flexbox/blob/master/assets/css/components/site.css 6 | * 7 | * .Site -> body > .container 8 | * .Site-content -> body > .container .row 9 | * .footer -> footer 10 | * 11 | * Key idea seems to be to ensure that .container and __all its parents__ 12 | * have height set to 100% 13 | * 14 | */ 15 | 16 | html, body { 17 | height: 100%; 18 | } 19 | 20 | body > .container { 21 | display: flex; 22 | height: 100%; 23 | flex-direction: column; 24 | } 25 | 26 | body > .container .row { 27 | flex: 1 0 auto; 28 | } 29 | 30 | footer { 31 | margin-top: 45px; 32 | padding: 35px 0 36px; 33 | border-top: 1px solid #e5e5e5; 34 | color: #666; 35 | display: flex; 36 | flex-shrink: 0; 37 | } 38 | footer p { 39 | margin-bottom: 0; 40 | } 41 | footer div { 42 | flex: 1; 43 | } 44 | footer .pkgdown { 45 | text-align: right; 46 | } 47 | footer p { 48 | margin-bottom: 0; 49 | } 50 | 51 | img.icon { 52 | float: right; 53 | } 54 | 55 | img { 56 | max-width: 100%; 57 | } 58 | 59 | /* Fix bug in bootstrap (only seen in firefox) */ 60 | summary { 61 | display: list-item; 62 | } 63 | 64 | /* Typographic tweaking ---------------------------------*/ 65 | 66 | .contents .page-header { 67 | margin-top: calc(-60px + 1em); 68 | } 69 | 70 | /* Section anchors ---------------------------------*/ 71 | 72 | a.anchor { 73 | margin-left: -30px; 74 | display:inline-block; 75 | width: 30px; 76 | height: 30px; 77 | visibility: hidden; 78 | 79 | background-image: url(./link.svg); 80 | background-repeat: no-repeat; 81 | background-size: 20px 20px; 82 | background-position: center center; 83 | } 84 | 85 | .hasAnchor:hover a.anchor { 86 | visibility: visible; 87 | } 88 | 89 | @media (max-width: 767px) { 90 | .hasAnchor:hover a.anchor { 91 | visibility: hidden; 92 | } 93 | } 94 | 95 | 96 | /* Fixes for fixed navbar --------------------------*/ 97 | 98 | .contents h1, .contents h2, .contents h3, .contents h4 { 99 | padding-top: 60px; 100 | margin-top: -40px; 101 | } 102 | 103 | /* Sidebar --------------------------*/ 104 | 105 | #sidebar { 106 | margin-top: 30px; 107 | position: -webkit-sticky; 108 | position: sticky; 109 | top: 70px; 110 | } 111 | #sidebar h2 { 112 | font-size: 1.5em; 113 | margin-top: 1em; 114 | } 115 | 116 | #sidebar h2:first-child { 117 | margin-top: 0; 118 | } 119 | 120 | #sidebar .list-unstyled li { 121 | margin-bottom: 0.5em; 122 | } 123 | 124 | .orcid { 125 | height: 16px; 126 | /* margins are required by official ORCID trademark and display guidelines */ 127 | margin-left:4px; 128 | margin-right:4px; 129 | vertical-align: middle; 130 | } 131 | 132 | /* Reference index & topics ----------------------------------------------- */ 133 | 134 | .ref-index th {font-weight: normal;} 135 | 136 | .ref-index td {vertical-align: top;} 137 | .ref-index .icon {width: 40px;} 138 | .ref-index .alias {width: 40%;} 139 | .ref-index-icons .alias {width: calc(40% - 40px);} 140 | .ref-index .title {width: 60%;} 141 | 142 | .ref-arguments th {text-align: right; padding-right: 10px;} 143 | .ref-arguments th, .ref-arguments td {vertical-align: top;} 144 | .ref-arguments .name {width: 20%;} 145 | .ref-arguments .desc {width: 80%;} 146 | 147 | /* Nice scrolling for wide elements --------------------------------------- */ 148 | 149 | table { 150 | display: block; 151 | overflow: auto; 152 | } 153 | 154 | /* Syntax highlighting ---------------------------------------------------- */ 155 | 156 | pre { 157 | word-wrap: normal; 158 | word-break: normal; 159 | border: 1px solid #eee; 160 | } 161 | 162 | pre, code { 163 | background-color: #f8f8f8; 164 | color: #333; 165 | } 166 | 167 | pre code { 168 | overflow: auto; 169 | word-wrap: normal; 170 | white-space: pre; 171 | } 172 | 173 | pre .img { 174 | margin: 5px 0; 175 | } 176 | 177 | pre .img img { 178 | background-color: #fff; 179 | display: block; 180 | height: auto; 181 | } 182 | 183 | code a, pre a { 184 | color: #375f84; 185 | } 186 | 187 | a.sourceLine:hover { 188 | text-decoration: none; 189 | } 190 | 191 | .fl {color: #1514b5;} 192 | .fu {color: #000000;} /* function */ 193 | .ch,.st {color: #036a07;} /* string */ 194 | .kw {color: #264D66;} /* keyword */ 195 | .co {color: #888888;} /* comment */ 196 | 197 | .message { color: black; font-weight: bolder;} 198 | .error { color: orange; font-weight: bolder;} 199 | .warning { color: #6A0366; font-weight: bolder;} 200 | 201 | /* Clipboard --------------------------*/ 202 | 203 | .hasCopyButton { 204 | position: relative; 205 | } 206 | 207 | .btn-copy-ex { 208 | position: absolute; 209 | right: 0; 210 | top: 0; 211 | visibility: hidden; 212 | } 213 | 214 | .hasCopyButton:hover button.btn-copy-ex { 215 | visibility: visible; 216 | } 217 | 218 | /* headroom.js ------------------------ */ 219 | 220 | .headroom { 221 | will-change: transform; 222 | transition: transform 200ms linear; 223 | } 224 | .headroom--pinned { 225 | transform: translateY(0%); 226 | } 227 | .headroom--unpinned { 228 | transform: translateY(-100%); 229 | } 230 | 231 | /* mark.js ----------------------------*/ 232 | 233 | mark { 234 | background-color: rgba(255, 255, 51, 0.5); 235 | border-bottom: 2px solid rgba(255, 153, 51, 0.3); 236 | padding: 1px; 237 | } 238 | 239 | /* vertical spacing after htmlwidgets */ 240 | .html-widget { 241 | margin-bottom: 10px; 242 | } 243 | 244 | /* fontawesome ------------------------ */ 245 | 246 | .fab { 247 | font-family: "Font Awesome 5 Brands" !important; 248 | } 249 | 250 | /* don't display links in code chunks when printing */ 251 | /* source: https://stackoverflow.com/a/10781533 */ 252 | @media print { 253 | code a:link:after, code a:visited:after { 254 | content: ""; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /docs/reference/runShinyExample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Run shinyauthr example — runShinyExample • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 | 103 | 104 | 105 | 106 |
    107 | 108 |
    109 |
    110 | 115 | 116 |
    117 |

    Launch example shiny dashboard using shinyauthr authentication modules

    118 |
    119 | 120 |
    runShinyExample()
    121 | 122 | 123 | 124 |
    125 | 133 |
    134 | 135 | 136 |
    137 | 140 | 141 |
    142 |

    Site built with pkgdown 1.4.1.

    143 |
    144 | 145 |
    146 |
    147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /docs/LICENSE-text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | License • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | 101 | 102 | 103 | 104 |
    105 | 106 |
    107 |
    108 | 111 | 112 |
    MIT License
    113 | 
    114 | Copyright (c) 2018 Paul Campbell
    115 | 
    116 | Permission is hereby granted, free of charge, to any person obtaining a copy
    117 | of this software and associated documentation files (the "Software"), to deal
    118 | in the Software without restriction, including without limitation the rights
    119 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    120 | copies of the Software, and to permit persons to whom the Software is
    121 | furnished to do so, subject to the following conditions:
    122 | 
    123 | The above copyright notice and this permission notice shall be included in all
    124 | copies or substantial portions of the Software.
    125 | 
    126 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    127 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    128 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    129 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    130 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    131 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    132 | SOFTWARE.
    133 | 
    134 | 135 |
    136 | 137 |
    138 | 139 | 140 | 141 |
    142 | 145 | 146 |
    147 |

    Site built with pkgdown 1.4.1.

    148 |
    149 | 150 |
    151 |
    152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /R/login.R: -------------------------------------------------------------------------------- 1 | #' login UI module 2 | #' 3 | #' Shiny UI Module for use with \link{login} 4 | #' 5 | #' Call via \code{loginUI("your_id")} 6 | #' 7 | #' @param id Shiny id 8 | #' @param title header title for the login panel 9 | #' @param user_title label for the user name text input 10 | #' @param pass_title label for the password text input 11 | #' @param login_title label for the login button 12 | #' @param error_message message to display after failed login 13 | #' 14 | #' @return Shiny UI 15 | #' 16 | #' @author Paul Campbell, \email{pacampbell91@gmail.com} 17 | #' 18 | #' @export 19 | loginUI <- function(id, title = "Please log in", user_title = "User Name", pass_title = "Password", 20 | login_title = "Log in", error_message = "Invalid username or password!") { 21 | ns <- shiny::NS(id) 22 | 23 | shiny::div(id = ns("panel"), style = "width: 500px; max-width: 100%; margin: 0 auto; padding: 20px;", 24 | shiny::wellPanel( 25 | shiny::tags$h2(title, class = "text-center", style = "padding-top: 0;"), 26 | 27 | shiny::textInput(ns("user_name"), shiny::tagList(shiny::icon("user"), user_title)), 28 | 29 | shiny::passwordInput(ns("password"), shiny::tagList(shiny::icon("unlock-alt"), pass_title)), 30 | 31 | shiny::div( 32 | style = "text-align: center;", 33 | shiny::actionButton(ns("button"), login_title, class = "btn-primary", style = "color: white;") 34 | ), 35 | 36 | shinyjs::hidden( 37 | shiny::div(id = ns("error"), 38 | shiny::tags$p(error_message, 39 | style = "color: red; font-weight: bold; padding-top: 5px;", class = "text-center")) 40 | ) 41 | ) 42 | ) 43 | } 44 | 45 | #' login server module 46 | #' 47 | #' Shiny authentication module for use with \link{loginUI} 48 | #' 49 | #' Call via \code{shiny::callModule(login, "your_id", ...)} 50 | #' 51 | #' @param input shiny input 52 | #' @param output shiny output 53 | #' @param session shiny session 54 | #' @param data data frame or tibble containing usernames, passwords and other user data 55 | #' @param user_col bare (unquoted) column name containing usernames 56 | #' @param pwd_col bare (unquoted) column name containing passwords 57 | #' @param sodium_hashed have the passwords been hash encrypted using the sodium package? defaults to FALSE 58 | #' @param hashed Deprecated. shinyauthr now uses the sodium package for password hashing and decryption. If you have previously hashed your passwords with the digest package to use with shinyauthr please re-hash them with sodium for decryption to work. 59 | #' @param algo Deprecated 60 | #' @param log_out [reactive] supply the returned reactive from \link{logout} here to trigger a user logout 61 | #' 62 | #' @return The module will return a reactive 2 element list to your main application. 63 | #' First element \code{user_auth} is a boolean inditcating whether there has been 64 | #' a successful login or not. Second element \code{info} will be the data frame provided 65 | #' to the function, filtered to the row matching the succesfully logged in username. 66 | #' When \code{user_auth} is FALSE \code{info} is NULL. 67 | #' 68 | #' @importFrom rlang := 69 | #' 70 | #' @examples 71 | #' \dontrun{ 72 | #' user_credentials <- shiny::callModule(login, "login", 73 | #' data = user_base, 74 | #' user_col = user, 75 | #' pwd_col = password, 76 | #' log_out = reactive(logout_init())) 77 | #' } 78 | #' 79 | #' @export 80 | login <- function(input, output, session, data, user_col, pwd_col, sodium_hashed = FALSE, hashed, algo, log_out = NULL) { 81 | 82 | if (!missing(hashed)) { 83 | stop("in shinyauthr::login module call. Argument hashed is deprecated. shinyauthr now uses the sodium package for password hashing and decryption. If you had previously hashed your passwords with the digest package to use with shinyauthr, please re-hash them with sodium and use the sodium_hashed argument instead for decryption to work. Sorry for this breaking change but sodium hashing provides added protection against brute-force attacks on stored passwords.", 84 | call. = FALSE) 85 | } 86 | 87 | credentials <- shiny::reactiveValues(user_auth = FALSE, info = NULL) 88 | 89 | shiny::observeEvent(log_out(), { 90 | credentials$user_auth <- FALSE 91 | credentials$info <- NULL 92 | shiny::updateTextInput(session, "password", value = "") 93 | }) 94 | 95 | shiny::observeEvent(credentials$user_auth, ignoreInit = TRUE, { 96 | shinyjs::toggle(id = "panel") 97 | }) 98 | 99 | users <- dplyr::enquo(user_col) 100 | pwds <- dplyr::enquo(pwd_col) 101 | 102 | # ensure all text columns are character class 103 | data <- dplyr::mutate_if(data, is.factor, as.character) 104 | # if password column hasn't been hashed with sodium, do it for them 105 | # if (!sodium_hashed) data <- dplyr::mutate(data, !!pwds := sapply(!!pwds, sodium::password_store)) 106 | 107 | shiny::observeEvent(input$button, { 108 | 109 | # check for match of input username to username column in data 110 | row_username <- which(dplyr::pull(data, !!users) == input$user_name) 111 | 112 | if (length(row_username)) { 113 | row_password <- dplyr::filter(data,dplyr::row_number() == row_username) 114 | row_password <- dplyr::pull(row_password, !!pwds) 115 | if (sodium_hashed) { 116 | password_match <- sodium::password_verify(row_password, input$password) 117 | } else { 118 | password_match <- identical(row_password, input$password) 119 | } 120 | } else { 121 | password_match <- FALSE 122 | } 123 | 124 | # if user name row and password name row are same, credentials are valid 125 | if (length(row_username) == 1 && password_match) { 126 | credentials$user_auth <- TRUE 127 | credentials$info <- dplyr::filter(data, !!users == input$user_name) 128 | } else { # if not valid temporarily show error message to user 129 | shinyjs::toggle(id = "error", anim = TRUE, time = 1, animType = "fade") 130 | shinyjs::delay(5000, shinyjs::toggle(id = "error", anim = TRUE, time = 1, animType = "fade")) 131 | } 132 | 133 | }) 134 | 135 | # return reactive list containing auth boolean and user information 136 | shiny::reactive({ 137 | shiny::reactiveValuesToList(credentials) 138 | }) 139 | 140 | } 141 | -------------------------------------------------------------------------------- /docs/reference/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Function reference • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | 101 | 102 | 103 | 104 |
    105 | 106 |
    107 |
    108 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 126 | 127 | 128 | 129 | 132 | 133 | 134 | 135 | 138 | 139 | 140 | 141 | 142 | 146 | 147 | 148 | 149 | 152 | 153 | 154 | 155 | 158 | 159 | 160 | 161 |
    123 |

    Login

    124 |

    Login modules

    125 |
    130 |

    login()

    131 |

    login server module

    136 |

    loginUI()

    137 |

    login UI module

    143 |

    Logout

    144 |

    Logout modules

    145 |
    150 |

    logout()

    151 |

    logout server module

    156 |

    logoutUI()

    157 |

    logout UI module

    162 |
    163 | 164 | 171 |
    172 | 173 | 174 |
    175 | 178 | 179 |
    180 |

    Site built with pkgdown 1.4.1.

    181 |
    182 | 183 |
    184 |
    185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shinyauthr 2 | 3 | 4 | [![Lifecycle: maturing](https://img.shields.io/badge/lifecycle-maturing-blue.svg)](https://www.tidyverse.org/lifecycle/#maturing) 5 | 6 | 7 | `shinyauthr` is an R package providing module functions that can be used to add an authentication layer to your shiny apps. 8 | 9 | It borrows some code from treysp's [shiny_password](https://github.com/treysp/shiny_password) template with the goal of making implementation simpler for end users and allowing the login/logout UIs to fit easily into any UI framework, including [shinydashboard](https://rstudio.github.io/shinydashboard/). See [live example app here](https://cultureofinsight.shinyapps.io/shinyauthr/) and code in the [inst directory](inst/shiny-examples/shinyauthr_example). 10 | 11 | ## Installation 12 | 13 | ```r 14 | remotes::install_github("paulc91/shinyauthr") 15 | ``` 16 | ## Usage 17 | 18 | The package provides 2 module functions each with a UI and server element: 19 | 20 | - `login()` 21 | - `loginUI()` 22 | - `logout()` 23 | - `logoutUI()` 24 | 25 | Below is a minimal reproducible example of how to use the authentication modules in a shiny app. Note that you must initiate the use of the shinyjs package with `shinyjs::useShinyjs()` in your UI code for this to work appropriately. 26 | 27 | ```r 28 | library(shiny) 29 | library(shinyauthr) 30 | library(shinyjs) 31 | 32 | # dataframe that holds usernames, passwords and other user data 33 | user_base <- data.frame( 34 | user = c("user1", "user2"), 35 | password = c("pass1", "pass2"), 36 | permissions = c("admin", "standard"), 37 | name = c("User One", "User Two"), 38 | stringsAsFactors = FALSE, 39 | row.names = NULL 40 | ) 41 | 42 | ui <- fluidPage( 43 | # must turn shinyjs on 44 | shinyjs::useShinyjs(), 45 | # add logout button UI 46 | div(class = "pull-right", logoutUI(id = "logout")), 47 | # add login panel UI function 48 | loginUI(id = "login"), 49 | # setup table output to show user info after login 50 | tableOutput("user_table") 51 | ) 52 | 53 | server <- function(input, output, session) { 54 | 55 | # call the logout module with reactive trigger to hide/show 56 | logout_init <- callModule(shinyauthr::logout, 57 | id = "logout", 58 | active = reactive(credentials()$user_auth)) 59 | 60 | # call login module supplying data frame, user and password cols 61 | # and reactive trigger 62 | credentials <- callModule(shinyauthr::login, 63 | id = "login", 64 | data = user_base, 65 | user_col = user, 66 | pwd_col = password, 67 | log_out = reactive(logout_init())) 68 | 69 | # pulls out the user information returned from login module 70 | user_data <- reactive({credentials()$info}) 71 | 72 | output$user_table <- renderTable({ 73 | # use req to only render results when credentials()$user_auth is TRUE 74 | req(credentials()$user_auth) 75 | user_data() 76 | }) 77 | } 78 | 79 | shinyApp(ui = ui, server = server) 80 | 81 | ``` 82 | ## Details 83 | 84 | When the login module is called, it returns a reactive list containing 2 elements: 85 | 86 | - `user_auth` 87 | - `info` 88 | 89 | The initial values of these variables are `FALSE` and `NULL` respectively. However, 90 | given a data frame or tibble containing user names, passwords and other user data (optional), the login module will assign a `user_auth` value of `TRUE` if the user supplies a matching user name and password. The value of `info` then becomes the row of data associated with that user which can be used in the main to control content based on user permission variables etc. 91 | 92 | The logout button will only show when `user_auth` is `TRUE`. Clicking the button will reset `user_auth` back to `FALSE` which will hide the button and show the login panel again. 93 | 94 | You can set the code in your server functions to only run after a successful login through use of the `req()` function inside all reactives, renders and observers. In the example above, using `req(credentials()$user_auth)` inside the `renderTable` function ensures the table showing the returned user information is only rendered when `user_auth` is `TRUE`. 95 | 96 | ## Hashing Passwords with `sodium` 97 | 98 | If you are hosting your user passwords on the internet, it is a good idea to first encrypt them with a hashing algorithm. You can use the [sodium package](https://github.com/jeroen/sodium) to do this. Sodium uses a slow hashing algorithm that is specifically designed to protect stored passwords from brute-force attacks. More on this [here](https://download.libsodium.org/doc/password_hashing/). You then tell the `shinyauthr::login` module that your passwords have been hashed by `sodium` and `shinyauthr` will then decrypt when login is requested. Your plain text passwords must be a character vector, not factors, when hashing for this to work as shiny inputs are passed as character strings. 99 | 100 | For example, a sample user base like the following can be incorporated for use with `shinyauthr`: 101 | 102 | ```r 103 | # create a user base then hash passwords with sodium 104 | # then save to an rds file in app directory 105 | library(sodium) 106 | 107 | user_base <- data.frame( 108 | user = c("user1", "user2"), 109 | password = sapply(c("pass1", "pass2"), sodium::password_store), 110 | permissions = c("admin", "standard"), 111 | name = c("User One", "User Two"), 112 | stringsAsFactors = FALSE, 113 | row.names = NULL 114 | ) 115 | 116 | saveRDS(user_base, "user_base.rds") 117 | ``` 118 | ```r 119 | # in your app code, read in the user base rds file 120 | user_base <- readRDS("user_base.rds") 121 | ``` 122 | ```r 123 | # then when calling the module set sodium_hashed = TRUE 124 | credentials <- callModule(shinyauthr::login, "login", 125 | data = user_base, 126 | user_col = user, 127 | pwd_col = password, 128 | sodium_hashed = TRUE, 129 | log_out = reactive(logout_init())) 130 | ``` 131 | 132 | ## Disclaimer 133 | 134 | I'm not a security professional so cannot guarantee this authentication procedure to be foolproof. It is ultimately the shiny app developer's responsibility not to expose any sensitive content to the client without the necessary login criteria being met. 135 | 136 | I would welcome any feedback on any potential vulnerabilities in the process. I know that apps hosted on a server without an SSL certificate could be open to interception of usernames and passwords submitted by a user. As such I would not recommend the use of shinyauthr without an HTTPS connection. 137 | 138 | For apps intended for use within commercial organisations, I would recommend one of RStudio's commercial shiny hosting options, or [shinyproxy](https://www.shinyproxy.io/), both of which have built in authetication options. 139 | 140 | However, I hope that having an easy-to-implement open-source shiny authentication option like this will prove useful when alternative options are not feasible. 141 | 142 | _Paul Campbell_ 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/news/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Changelog • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | 101 | 102 | 103 | 104 |
    105 | 106 |
    107 |
    108 | 112 | 113 |
    114 |

    115 | shinyauthr 0.1.0

    116 |
      117 |
    • Prepare for CRAN submission
    • 118 |
    119 |
    120 |
    121 |

    122 | shinyauthr 0.0.99

    123 |
    124 |

    125 | April 2019 - switched to the sodium package for password hashing and decryption

    126 |

    If you plan to use hashed passwords with shinyauthr you now must use the sodium package to do hash your passwords and the sodium_hashed = TRUE argument of the shinyauthr::login module call for decrpytion to work appropriately.

    127 |

    This means that previously used hashed and algo arguments that interfaced with the digest package are now deprecated. If you had previously hashed your passwords with the digest package to use with shinyauthr, please re-hash them with sodium and use the sodium_hashed argument instead.

    128 |

    Sorry for this breaking change, but sodium hashing provides added protection against brute-force attacks on stored passwords. More information on this here.

    129 |
    130 |
    131 |
    132 | 133 | 142 | 143 |
    144 | 145 | 146 |
    147 | 150 | 151 |
    152 |

    Site built with pkgdown 1.4.1.

    153 |
    154 | 155 |
    156 |
    157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /docs/reference/logoutUI.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | logout UI module — logoutUI • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 | 103 | 104 | 105 | 106 |
    107 | 108 |
    109 |
    110 | 115 | 116 |
    117 |

    Shiny UI Module for use with logout

    118 |
    119 | 120 |
    logoutUI(id, label = "Log out", icon = NULL, class = "btn-danger",
    121 |   style = "color: white;")
    122 | 123 |

    Arguments

    124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
    id

    Shiny id

    label

    label for the logout button

    icon

    An optional icon to appear on the button.

    class

    bootstrap class for the logout button

    style

    css styling for the logout button

    147 | 148 |

    Value

    149 | 150 |

    Shiny UI action button

    151 |

    Details

    152 | 153 |

    Call via logoutUI("your_id")

    154 | 155 |
    156 | 165 |
    166 | 167 | 168 |
    169 | 172 | 173 |
    174 |

    Site built with pkgdown 1.4.1.

    175 |
    176 | 177 |
    178 |
    179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /docs/reference/loginUI.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | login UI module — loginUI • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 | 103 | 104 | 105 | 106 |
    107 | 108 |
    109 |
    110 | 115 | 116 |
    117 |

    Shiny UI Module for use with login

    118 |
    119 | 120 |
    loginUI(id, title = "Please log in", user_title = "User Name",
    121 |   pass_title = "Password", login_title = "Log in",
    122 |   error_message = "Invalid username or password!")
    123 | 124 |

    Arguments

    125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 |
    id

    Shiny id

    title

    header title for the login panel

    user_title

    label for the user name text input

    pass_title

    label for the password text input

    login_title

    label for the login button

    error_message

    message to display after failed login

    152 | 153 |

    Value

    154 | 155 |

    Shiny UI

    156 |

    Details

    157 | 158 |

    Call via loginUI("your_id")

    159 | 160 |
    161 | 172 |
    173 | 174 | 175 |
    176 | 179 | 180 |
    181 |

    Site built with pkgdown 1.4.1.

    182 |
    183 | 184 |
    185 |
    186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /docs/reference/logout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | logout server module — logout • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 | 103 | 104 | 105 | 106 |
    107 | 108 |
    109 |
    110 | 115 | 116 |
    117 |

    Shiny authentication module for use with logoutUI

    118 |
    119 | 120 |
    logout(input, output, session, active)
    121 | 122 |

    Arguments

    123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 141 | 142 |
    input

    shiny input

    output

    shiny output

    session

    shiny session

    active

    [reactive] supply the returned user_auth boolean reactive from login 140 | here to hide/show the logout button

    143 | 144 |

    Value

    145 | 146 |

    The reactive output of this module should be supplied as the log_out argument to the 147 | login module to trigger the logout process

    148 |

    Details

    149 | 150 |

    Call via shiny::callModule(logout, "your_id", ...)

    151 | 152 |

    Examples

    153 |
    if (FALSE) { 154 | logout_init <- shiny::callModule(logout, "logout", 155 | active = reactive(user_credentials()$user_auth)) 156 | }
    157 |
    158 | 168 |
    169 | 170 | 171 |
    172 | 175 | 176 |
    177 |

    Site built with pkgdown 1.4.1.

    178 |
    179 | 180 |
    181 |
    182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /docs/reference/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | login server module — login • shinyauthr 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 | 103 | 104 | 105 | 106 |
    107 | 108 |
    109 |
    110 | 115 | 116 |
    117 |

    Shiny authentication module for use with loginUI

    118 |
    119 | 120 |
    login(input, output, session, data, user_col, pwd_col,
    121 |   sodium_hashed = FALSE, hashed, algo, log_out = NULL)
    122 | 123 |

    Arguments

    124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 |
    input

    shiny input

    output

    shiny output

    session

    shiny session

    data

    data frame or tibble containing usernames, passwords and other user data

    user_col

    bare (unquoted) column name containing usernames

    pwd_col

    bare (unquoted) column name containing passwords

    sodium_hashed

    have the passwords been hash encrypted using the sodium package? defaults to FALSE

    hashed

    Deprecated. shinyauthr now uses the sodium package for password hashing and decryption. If you have previously hashed your passwords with the digest package to use with shinyauthr please re-hash them with sodium for decryption to work.

    algo

    Deprecated

    log_out

    [reactive] supply the returned reactive from logout here to trigger a user logout

    167 | 168 |

    Value

    169 | 170 |

    The module will return a reactive 2 element list to your main application. 171 | First element user_auth is a boolean inditcating whether there has been 172 | a successful login or not. Second element info will be the data frame provided 173 | to the function, filtered to the row matching the succesfully logged in username. 174 | When user_auth is FALSE info is NULL.

    175 |

    Details

    176 | 177 |

    Call via shiny::callModule(login, "your_id", ...)

    178 | 179 |

    Examples

    180 |
    if (FALSE) { 181 | user_credentials <- shiny::callModule(login, "login", 182 | data = user_base, 183 | user_col = user, 184 | pwd_col = password, 185 | log_out = reactive(logout_init())) 186 | }
    187 |
    188 | 198 |
    199 | 200 | 201 |
    202 | 205 | 206 |
    207 |

    Site built with pkgdown 1.4.1.

    208 |
    209 | 210 |
    211 |
    212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /docs/docsearch.css: -------------------------------------------------------------------------------- 1 | /* Docsearch -------------------------------------------------------------- */ 2 | /* 3 | Source: https://github.com/algolia/docsearch/ 4 | License: MIT 5 | */ 6 | 7 | .algolia-autocomplete { 8 | display: block; 9 | -webkit-box-flex: 1; 10 | -ms-flex: 1; 11 | flex: 1 12 | } 13 | 14 | .algolia-autocomplete .ds-dropdown-menu { 15 | width: 100%; 16 | min-width: none; 17 | max-width: none; 18 | padding: .75rem 0; 19 | background-color: #fff; 20 | background-clip: padding-box; 21 | border: 1px solid rgba(0, 0, 0, .1); 22 | box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .175); 23 | } 24 | 25 | @media (min-width:768px) { 26 | .algolia-autocomplete .ds-dropdown-menu { 27 | width: 175% 28 | } 29 | } 30 | 31 | .algolia-autocomplete .ds-dropdown-menu::before { 32 | display: none 33 | } 34 | 35 | .algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-] { 36 | padding: 0; 37 | background-color: rgb(255,255,255); 38 | border: 0; 39 | max-height: 80vh; 40 | } 41 | 42 | .algolia-autocomplete .ds-dropdown-menu .ds-suggestions { 43 | margin-top: 0 44 | } 45 | 46 | .algolia-autocomplete .algolia-docsearch-suggestion { 47 | padding: 0; 48 | overflow: visible 49 | } 50 | 51 | .algolia-autocomplete .algolia-docsearch-suggestion--category-header { 52 | padding: .125rem 1rem; 53 | margin-top: 0; 54 | font-size: 1.3em; 55 | font-weight: 500; 56 | color: #00008B; 57 | border-bottom: 0 58 | } 59 | 60 | .algolia-autocomplete .algolia-docsearch-suggestion--wrapper { 61 | float: none; 62 | padding-top: 0 63 | } 64 | 65 | .algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column { 66 | float: none; 67 | width: auto; 68 | padding: 0; 69 | text-align: left 70 | } 71 | 72 | .algolia-autocomplete .algolia-docsearch-suggestion--content { 73 | float: none; 74 | width: auto; 75 | padding: 0 76 | } 77 | 78 | .algolia-autocomplete .algolia-docsearch-suggestion--content::before { 79 | display: none 80 | } 81 | 82 | .algolia-autocomplete .ds-suggestion:not(:first-child) .algolia-docsearch-suggestion--category-header { 83 | padding-top: .75rem; 84 | margin-top: .75rem; 85 | border-top: 1px solid rgba(0, 0, 0, .1) 86 | } 87 | 88 | .algolia-autocomplete .ds-suggestion .algolia-docsearch-suggestion--subcategory-column { 89 | display: block; 90 | padding: .1rem 1rem; 91 | margin-bottom: 0.1; 92 | font-size: 1.0em; 93 | font-weight: 400 94 | /* display: none */ 95 | } 96 | 97 | .algolia-autocomplete .algolia-docsearch-suggestion--title { 98 | display: block; 99 | padding: .25rem 1rem; 100 | margin-bottom: 0; 101 | font-size: 0.9em; 102 | font-weight: 400 103 | } 104 | 105 | .algolia-autocomplete .algolia-docsearch-suggestion--text { 106 | padding: 0 1rem .5rem; 107 | margin-top: -.25rem; 108 | font-size: 0.8em; 109 | font-weight: 400; 110 | line-height: 1.25 111 | } 112 | 113 | .algolia-autocomplete .algolia-docsearch-footer { 114 | width: 110px; 115 | height: 20px; 116 | z-index: 3; 117 | margin-top: 10.66667px; 118 | float: right; 119 | font-size: 0; 120 | line-height: 0; 121 | } 122 | 123 | .algolia-autocomplete .algolia-docsearch-footer--logo { 124 | background-image: url("data:image/svg+xml;utf8,"); 125 | background-repeat: no-repeat; 126 | background-position: 50%; 127 | background-size: 100%; 128 | overflow: hidden; 129 | text-indent: -9000px; 130 | width: 100%; 131 | height: 100%; 132 | display: block; 133 | transform: translate(-8px); 134 | } 135 | 136 | .algolia-autocomplete .algolia-docsearch-suggestion--highlight { 137 | color: #FF8C00; 138 | background: rgba(232, 189, 54, 0.1) 139 | } 140 | 141 | 142 | .algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight { 143 | box-shadow: inset 0 -2px 0 0 rgba(105, 105, 105, .5) 144 | } 145 | 146 | .algolia-autocomplete .ds-suggestion.ds-cursor .algolia-docsearch-suggestion--content { 147 | background-color: rgba(192, 192, 192, .15) 148 | } 149 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Shiny Authentication Modules • shinyauthr 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 |
    23 |
    66 | 67 | 68 | 69 | 70 |
    71 |
    72 |
    73 | 75 | 76 | 77 |

    shinyauthr is an R package providing module functions that can be used to add an authentication layer to your shiny apps.

    78 |

    It borrows some code from treysp’s shiny_password template with the goal of making implementation simpler for end users and allowing the login/logout UIs to fit easily into any UI framework, including shinydashboard. See live example app here and code in the inst directory.

    79 |
    80 |

    81 | Installation

    82 |
    remotes::install_github("paulc91/shinyauthr")
    83 |
    84 |
    85 |

    86 | Usage

    87 |

    The package provides 2 module functions each with a UI and server element:

    88 | 94 |

    Below is a minimal reproducible example of how to use the authentication modules in a shiny app. Note that you must initiate the use of the shinyjs package with shinyjs::useShinyjs() in your UI code for this to work appropriately.

    95 |
    library(shiny)
     96 | library(shinyauthr)
     97 | library(shinyjs)
     98 | 
     99 | # dataframe that holds usernames, passwords and other user data
    100 | user_base <- data.frame(
    101 |   user = c("user1", "user2"),
    102 |   password = c("pass1", "pass2"), 
    103 |   permissions = c("admin", "standard"),
    104 |   name = c("User One", "User Two"),
    105 |   stringsAsFactors = FALSE,
    106 |   row.names = NULL
    107 | )
    108 | 
    109 | ui <- fluidPage(
    110 |   # must turn shinyjs on
    111 |   shinyjs::useShinyjs(),
    112 |   # add logout button UI 
    113 |   div(class = "pull-right", logoutUI(id = "logout")),
    114 |   # add login panel UI function
    115 |   loginUI(id = "login"),
    116 |   # setup table output to show user info after login
    117 |   tableOutput("user_table")
    118 | )
    119 | 
    120 | server <- function(input, output, session) {
    121 |   
    122 |   # call the logout module with reactive trigger to hide/show
    123 |   logout_init <- callModule(shinyauthr::logout, 
    124 |                             id = "logout", 
    125 |                             active = reactive(credentials()$user_auth))
    126 |   
    127 |   # call login module supplying data frame, user and password cols
    128 |   # and reactive trigger
    129 |   credentials <- callModule(shinyauthr::login, 
    130 |                             id = "login", 
    131 |                             data = user_base,
    132 |                             user_col = user,
    133 |                             pwd_col = password,
    134 |                             log_out = reactive(logout_init()))
    135 |   
    136 |   # pulls out the user information returned from login module
    137 |   user_data <- reactive({credentials()$info})
    138 |   
    139 |   output$user_table <- renderTable({
    140 |     # use req to only render results when credentials()$user_auth is TRUE
    141 |     req(credentials()$user_auth)
    142 |     user_data()
    143 |   })
    144 | }
    145 | 
    146 | shinyApp(ui = ui, server = server)
    147 |
    148 |
    149 |

    150 | Details

    151 |

    When the login module is called, it returns a reactive list containing 2 elements:

    152 |
      153 |
    • user_auth
    • 154 |
    • info
    • 155 |
    156 |

    The initial values of these variables are FALSE and NULL respectively. However, given a data frame or tibble containing user names, passwords and other user data (optional), the login module will assign a user_auth value of TRUE if the user supplies a matching user name and password. The value of info then becomes the row of data associated with that user which can be used in the main to control content based on user permission variables etc.

    157 |

    The logout button will only show when user_auth is TRUE. Clicking the button will reset user_auth back to FALSE which will hide the button and show the login panel again.

    158 |

    You can set the code in your server functions to only run after a successful login through use of the req() function inside all reactives, renders and observers. In the example above, using req(credentials()$user_auth) inside the renderTable function ensures the table showing the returned user information is only rendered when user_auth is TRUE.

    159 |
    160 |
    161 |

    162 | Hashing Passwords with sodium 163 |

    164 |

    If you are hosting your user passwords on the internet, it is a good idea to first encrypt them with a hashing algorithm. You can use the sodium package to do this. Sodium uses a slow hashing algorithm that is specifically designed to protect stored passwords from brute-force attacks. More on this here. You then tell the shinyauthr::login module that your passwords have been hashed by sodium and shinyauthr will then decrypt when login is requested. Your plain text passwords must be a character vector, not factors, when hashing for this to work as shiny inputs are passed as character strings.

    165 |

    For example, a sample user base like the following can be incorporated for use with shinyauthr:

    166 |
    # create a user base then hash passwords with sodium
    167 | # then save to an rds file in app directory
    168 | library(sodium)
    169 | 
    170 | user_base <- data.frame(
    171 |   user = c("user1", "user2"),
    172 |   password = sapply(c("pass1", "pass2"), sodium::password_store), 
    173 |   permissions = c("admin", "standard"),
    174 |   name = c("User One", "User Two"),
    175 |   stringsAsFactors = FALSE,
    176 |   row.names = NULL
    177 | )
    178 | 
    179 | saveRDS(user_base, "user_base.rds")
    180 | 182 | 189 |
    190 |
    191 |

    192 | Disclaimer

    193 |

    I’m not a security professional so cannot guarantee this authentication procedure to be foolproof. It is ultimately the shiny app developer’s responsibility not to expose any sensitive content to the client without the necessary login criteria being met.

    194 |

    I would welcome any feedback on any potential vulnerabilities in the process. I know that apps hosted on a server without an SSL certificate could be open to interception of usernames and passwords submitted by a user. As such I would not recommend the use of shinyauthr without an HTTPS connection.

    195 |

    For apps intended for use within commercial organisations, I would recommend one of RStudio’s commercial shiny hosting options, or shinyproxy, both of which have built in authetication options.

    196 |

    However, I hope that having an easy-to-implement open-source shiny authentication option like this will prove useful when alternative options are not feasible.

    197 |

    Paul Campbell

    198 |
    199 |
    200 |
    201 | 202 | 235 |
    236 | 237 | 238 | 247 |
    248 | 249 | 250 | 251 | 252 | 253 | 254 | --------------------------------------------------------------------------------