Created
January 26, 2024 03:44
-
-
Save jbryer/d563b3147c7e5ea8c8d7910601676e39 to your computer and use it in GitHub Desktop.
Shiny authentication
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This script is modified by Jason Bryer ([email protected]) from Huidong Tian's | |
# original script. The blog post describing the method is here: | |
# http://withr.me/authentication-of-shiny-server-application-using-a-simple-method/ | |
# The original R script is located here: https://gist.github.com/withr/9001831 | |
# | |
# This script adds two new features: 1. Render a logout button, and 2. provide | |
# the ability for visitors to create a new account. | |
# | |
# Within your server.R file, be sure to use: source('Login.R', local=TRUE) | |
# | |
# To use this file, you can add uiOutput('uiLogin'), uiOutput('uiNewAccount'), | |
# and uiOutput('uiLogout') anywhere in your shiny application. If you wish to | |
# have part of you application available only to authenticated users, you can | |
# checked to see if they are logged in with the USER$Logged field (this is a | |
# logical). Additional, USER$username and USER$group will give the username | |
# and password of the logged in user, respectively. | |
default.group <- 'student' # The value for Group when creating new accounts | |
cookie_base <- 'msdscookie' | |
cookie_username <- paste0(cookie_base, 'username') | |
cookie_email <- paste0(cookie_base, 'email') | |
cookie_group <- paste0(cookie_base, 'group') # TODO: probably shouldn't save this as a cookie. Get from database | |
login_db_conn <- RSQLite::dbConnect(RSQLite::SQLite(), db_file) | |
if(!'users' %in% dbListTables(login_db_conn)) { | |
users <- data.frame(username = character(), | |
password = character(), | |
group = character(), | |
email = character(), | |
stringsAsFactors = FALSE) | |
dbWriteTable(login_db_conn, 'users', users) | |
} | |
USER <- reactiveValues(Logged = FALSE, | |
unique = format(Sys.time(), '%Y%m%d%H%M%S'), | |
username = NA, | |
email = NA, | |
group = NA) | |
observeEvent(get_cookie(cookie_username), { | |
username <- get_cookie(cookie_username) | |
if(!is.null(username)) { | |
USER$username <- username | |
USER$email <- get_cookie(cookie_email) | |
USER$group <- get_cookie(cookie_group) | |
if(is.null(USER$group)) { | |
USER$group <- 'student' | |
} | |
USER$Logged <- TRUE | |
} | |
}, once = TRUE) | |
# Password input textbox | |
passwdInput <- function(inputId, label, value) { | |
tagList( | |
tags$label(label), | |
tags$input(id=inputId, type="password", value=value, class='form-control') | |
) | |
} | |
# Returns a panel for logging in. | |
output$uiLogin <- renderUI({ | |
wellPanel( | |
uiOutput('pass'), | |
div(textInput(paste0("username", USER$unique), | |
"Username: ", value='')), | |
div(passwdInput(paste0("password", USER$unique), | |
"Password: ", value='')), | |
br(), | |
actionButton("Login", "Login") | |
) | |
}) | |
# Provides a UI for creating an account | |
output$uiNewAccount <- renderUI({ | |
wellPanel( | |
uiOutput('newuser'), | |
div(textInput(paste0("newusername", USER$unique), | |
"Username: ", value='')), | |
div(passwdInput(paste0("newpassword1", USER$unique), | |
"Password: ", value='')), | |
div(passwdInput(paste0("newpassword2", USER$unique), | |
"Confirm Password: ", value='')), | |
div(textInput(paste0('newemail', USER$unique), | |
"Email Address: ", value='')), | |
br(), | |
actionButton("CreateUser", "Create Account") | |
) | |
}) | |
##### Password Reset | |
reset_code <- reactiveVal('') | |
reset_code_verify <- reactiveVal('') | |
reset_message <- reactiveVal('') | |
reset_username <- reactiveVal('') | |
output$forgotPassword <- renderUI({ | |
code <- isolate(input$reset_password_code) | |
reset_password <- FALSE | |
if(nchar(reset_code_verify()) == 6) { | |
if(code == reset_code()) { | |
reset_password <- TRUE | |
} | |
} | |
if(reset_code() == '') { | |
wellPanel( | |
div(reset_message(), style = 'color:red'), | |
div( | |
textInput('forgot_password_email', 'Email address: ', value = '')), | |
br(), | |
actionButton('send_reset_password_code', 'Send reset code') | |
) | |
} else if(reset_password) { | |
wellPanel( | |
div(reset_message(), style = 'color:red'), | |
div( | |
passwordInput('reset_password1', label = 'Enter new password:', value = ''), | |
passwordInput('reset_password2', label = 'Confirm new password:', value = '') | |
), | |
br(), | |
actionButton('reset_new_password', 'Reset Password') | |
) | |
} else { | |
wellPanel( | |
div(reset_message(), style = 'color:red'), | |
div( | |
textInput('reset_password_code', 'Enter the code from the email:', value = '') | |
), | |
br(), | |
actionButton('send_reset_password_code', 'Resend Code'), | |
actionButton('submit_reset_password_code', 'Submit') | |
) | |
} | |
}) | |
observeEvent(input$submit_reset_password_code, { | |
if(input$submit_reset_password_code == 1) { | |
code <- isolate(input$reset_password_code) | |
reset_code_verify(code) | |
if(nchar(code) != 6 & reset_code() == code) { | |
reset_message('Code is not correct') | |
} | |
} | |
}) | |
observeEvent(input$reset_new_password, { | |
if(input$reset_password1 == input$reset_password2) { | |
query <- paste0( | |
"UPDATE users SET password = '", | |
input$reset_password1, | |
"' WHERE username = '", reset_username(), "'" | |
) | |
login_db_conn <- RSQLite::dbConnect(RSQLite::SQLite(), db_file) | |
dbSendQuery(login_db_conn, query) | |
RSQLite::dbDisconnect(login_db_conn) | |
reset_message('Password updated successfully. Please go to the login tab.') | |
reset_code('') | |
} else { | |
reset_message('Passwords do not match.') | |
} | |
}) | |
observeEvent(input$send_reset_password_code, { | |
login_db_conn <- RSQLite::dbConnect(RSQLite::SQLite(), db_file) | |
PASSWORD <- dbReadTable(login_db_conn, 'users') | |
RSQLite::dbDisconnect(login_db_conn) | |
email_address <- isolate(input$forgot_password_email) |> tolower() | |
if(!email_address %in% PASSWORD$email) { | |
reset_message(paste0(email_address, ' not found.')) | |
} else { | |
code <- sample(099999, size = 1) |> as.character() |> str_pad(width = 6, pad = '0') | |
tryCatch({ | |
username <- PASSWORD[PASSWORD$email == email_address,]$username[1] | |
reset_username(username) | |
email <- envelope() %>% | |
from(reset_password_from_email) |> | |
to(email_address) |> | |
subject(reset_password_subject) |> | |
text(paste0('Your password reset code is: ', | |
code, | |
' \nIf you did not request to reset your password you can ignore this email.')) | |
smtp <- server( | |
email_host, | |
email_port, | |
email_username, | |
email_password | |
) | |
smtp(email, verbose = FALSE) | |
reset_code(code) | |
}, error = function(e) { | |
reset_message(paste0('Error sending email: ', as.character(e))) | |
}) | |
} | |
}) | |
# UI for a logout button | |
output$uiLogout <- renderUI({ | |
actionButton('logoutButton', 'Logout', | |
icon = icon("user"), | |
style = "position: absolute; right: 20px; top: 10px") | |
}) | |
# Log the user out | |
observeEvent(input$logoutButton, { | |
if(!is.null(input$logoutButton) & input$logoutButton == 1) { | |
USER$Logged <- FALSE | |
USER$username <- USER$group <- NA | |
USER$unique <- format(Sys.time(), '%Y%m%d%H%M%S') | |
USER$email <- NA | |
remove_cookie(cookie_username) | |
remove_cookie(cookie_email) | |
remove_cookie(cookie_group) | |
} | |
}) | |
# Add a new user | |
output$newuser <- renderText({ | |
text <- '' | |
login_db_conn <- RSQLite::dbConnect(RSQLite::SQLite(), db_file) | |
if(USER$Logged == FALSE) { | |
if(!is.null(input$CreateUser)) { | |
if(input$CreateUser > 0) { | |
newusername <- input[[paste0('newusername', USER$unique)]] | |
newpassword1 <- input[[paste0('newpassword1', USER$unique)]] | |
newpassword2 <- input[[paste0('newpassword2', USER$unique)]] | |
newemail <- input[[paste0('newemail', USER$unique)]] |> tolower() | |
PASSWORD <- dbReadTable(login_db_conn, 'users') | |
# Validate input fields | |
if(is.null(newusername) | | |
is.null(newpassword1) | | |
is.null(newpassword2) | | |
is.null(newemail)) { | |
text <- 'Please enter all fields' | |
} else if(nchar(newusername) < 5 | | |
nchar(newpassword1) < 5) { | |
text <- 'Please enter username and password with at least 5 characters' | |
} else if(newpassword1 != newpassword2) { | |
text <- 'Passwords do not match' | |
} else if(is.null(newemail) | | |
nchar(newemail) < 5 | | |
grep(".+@.+", newemail) < 1) { | |
text <- 'Invalid email address' | |
} else if(tolower(newusername) %in% PASSWORD$username) { | |
text <- 'Username already exists' | |
} else { # Add the user | |
newuser <- data.frame( | |
username = newusername, | |
password = newpassword1, | |
group = default.group, | |
email = newemail | |
) | |
for(i in names(PASSWORD)[(!names(PASSWORD) %in% names(newuser))]) { | |
newuser[,i] <- NA # Make sure the data.frames line up | |
} | |
dbWriteTable(login_db_conn, 'users', newuser, append = TRUE) | |
USER$Logged <- TRUE | |
USER$username <- newusername | |
USER$group <- default.group | |
USER$email <- newemail | |
set_cookie(cookie_username, newusername) | |
set_cookie(cookie_email, newemail) | |
set_cookie(cookie_group, default.group) | |
} | |
} | |
} | |
} | |
RSQLite::dbDisconnect(login_db_conn) | |
text | |
}) | |
# Log the user in | |
output$pass <- renderText({ | |
if(USER$Logged == FALSE) { | |
if(!is.null(input$Login)) { | |
if(input$Login > 0) { | |
login_db_conn <- RSQLite::dbConnect(RSQLite::SQLite(), db_file) | |
PASSWORD <- dbReadTable(login_db_conn, 'users') | |
RSQLite::dbDisconnect(login_db_conn) | |
username <- isolate(input[[paste0('username', USER$unique)]]) | |
password <- isolate(input[[paste0('password', USER$unique)]]) | |
Id.username <- which(tolower(PASSWORD$username) == tolower(username)) | |
if(!is.null(Id.username) & length(Id.username) == 1 & | |
password == PASSWORD[Id.username,]$password) | |
{ | |
USER$Logged <- TRUE | |
USER$username <- username | |
USER$group <- PASSWORD[Id.username,]$group | |
USER$email <- PASSWORD[Id.username,]$email |> tolower() | |
set_cookie(cookie_username, username) | |
set_cookie(cookie_email, PASSWORD[Id.username,]$email) | |
set_cookie(cookie_group, PASSWORD[Id.username,]$group) | |
} else { | |
"Username or password failed!" | |
} | |
} | |
} | |
} | |
}) | |
RSQLite::dbDisconnect(login_db_conn) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Downloaded from: http://www.myersdaily.org/joseph/javascript/md5.js; | |
function md5cycle(x, k) { | |
var a = x[0], b = x[1], c = x[2], d = x[3]; | |
a = ff(a, b, c, d, k[0], 7, -680876936); | |
d = ff(d, a, b, c, k[1], 12, -389564586); | |
c = ff(c, d, a, b, k[2], 17, 606105819); | |
b = ff(b, c, d, a, k[3], 22, -1044525330); | |
a = ff(a, b, c, d, k[4], 7, -176418897); | |
d = ff(d, a, b, c, k[5], 12, 1200080426); | |
c = ff(c, d, a, b, k[6], 17, -1473231341); | |
b = ff(b, c, d, a, k[7], 22, -45705983); | |
a = ff(a, b, c, d, k[8], 7, 1770035416); | |
d = ff(d, a, b, c, k[9], 12, -1958414417); | |
c = ff(c, d, a, b, k[10], 17, -42063); | |
b = ff(b, c, d, a, k[11], 22, -1990404162); | |
a = ff(a, b, c, d, k[12], 7, 1804603682); | |
d = ff(d, a, b, c, k[13], 12, -40341101); | |
c = ff(c, d, a, b, k[14], 17, -1502002290); | |
b = ff(b, c, d, a, k[15], 22, 1236535329); | |
a = gg(a, b, c, d, k[1], 5, -165796510); | |
d = gg(d, a, b, c, k[6], 9, -1069501632); | |
c = gg(c, d, a, b, k[11], 14, 643717713); | |
b = gg(b, c, d, a, k[0], 20, -373897302); | |
a = gg(a, b, c, d, k[5], 5, -701558691); | |
d = gg(d, a, b, c, k[10], 9, 38016083); | |
c = gg(c, d, a, b, k[15], 14, -660478335); | |
b = gg(b, c, d, a, k[4], 20, -405537848); | |
a = gg(a, b, c, d, k[9], 5, 568446438); | |
d = gg(d, a, b, c, k[14], 9, -1019803690); | |
c = gg(c, d, a, b, k[3], 14, -187363961); | |
b = gg(b, c, d, a, k[8], 20, 1163531501); | |
a = gg(a, b, c, d, k[13], 5, -1444681467); | |
d = gg(d, a, b, c, k[2], 9, -51403784); | |
c = gg(c, d, a, b, k[7], 14, 1735328473); | |
b = gg(b, c, d, a, k[12], 20, -1926607734); | |
a = hh(a, b, c, d, k[5], 4, -378558); | |
d = hh(d, a, b, c, k[8], 11, -2022574463); | |
c = hh(c, d, a, b, k[11], 16, 1839030562); | |
b = hh(b, c, d, a, k[14], 23, -35309556); | |
a = hh(a, b, c, d, k[1], 4, -1530992060); | |
d = hh(d, a, b, c, k[4], 11, 1272893353); | |
c = hh(c, d, a, b, k[7], 16, -155497632); | |
b = hh(b, c, d, a, k[10], 23, -1094730640); | |
a = hh(a, b, c, d, k[13], 4, 681279174); | |
d = hh(d, a, b, c, k[0], 11, -358537222); | |
c = hh(c, d, a, b, k[3], 16, -722521979); | |
b = hh(b, c, d, a, k[6], 23, 76029189); | |
a = hh(a, b, c, d, k[9], 4, -640364487); | |
d = hh(d, a, b, c, k[12], 11, -421815835); | |
c = hh(c, d, a, b, k[15], 16, 530742520); | |
b = hh(b, c, d, a, k[2], 23, -995338651); | |
a = ii(a, b, c, d, k[0], 6, -198630844); | |
d = ii(d, a, b, c, k[7], 10, 1126891415); | |
c = ii(c, d, a, b, k[14], 15, -1416354905); | |
b = ii(b, c, d, a, k[5], 21, -57434055); | |
a = ii(a, b, c, d, k[12], 6, 1700485571); | |
d = ii(d, a, b, c, k[3], 10, -1894986606); | |
c = ii(c, d, a, b, k[10], 15, -1051523); | |
b = ii(b, c, d, a, k[1], 21, -2054922799); | |
a = ii(a, b, c, d, k[8], 6, 1873313359); | |
d = ii(d, a, b, c, k[15], 10, -30611744); | |
c = ii(c, d, a, b, k[6], 15, -1560198380); | |
b = ii(b, c, d, a, k[13], 21, 1309151649); | |
a = ii(a, b, c, d, k[4], 6, -145523070); | |
d = ii(d, a, b, c, k[11], 10, -1120210379); | |
c = ii(c, d, a, b, k[2], 15, 718787259); | |
b = ii(b, c, d, a, k[9], 21, -343485551); | |
x[0] = add32(a, x[0]); | |
x[1] = add32(b, x[1]); | |
x[2] = add32(c, x[2]); | |
x[3] = add32(d, x[3]); | |
} | |
function cmn(q, a, b, x, s, t) { | |
a = add32(add32(a, q), add32(x, t)); | |
return add32((a << s) | (a >>> (32 - s)), b); | |
} | |
function ff(a, b, c, d, x, s, t) { | |
return cmn((b & c) | ((~b) & d), a, b, x, s, t); | |
} | |
function gg(a, b, c, d, x, s, t) { | |
return cmn((b & d) | (c & (~d)), a, b, x, s, t); | |
} | |
function hh(a, b, c, d, x, s, t) { | |
return cmn(b ^ c ^ d, a, b, x, s, t); | |
} | |
function ii(a, b, c, d, x, s, t) { | |
return cmn(c ^ (b | (~d)), a, b, x, s, t); | |
} | |
function md51(s) { | |
txt = ''; | |
var n = s.length, | |
state = [1732584193, -271733879, -1732584194, 271733878], i; | |
for (i=64; i<=s.length; i+=64) { | |
md5cycle(state, md5blk(s.substring(i-64, i))); | |
} | |
s = s.substring(i-64); | |
var tail = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]; | |
for (i=0; i<s.length; i++) | |
tail[i>>2] |= s.charCodeAt(i) << ((i%4) << 3); | |
tail[i>>2] |= 0x80 << ((i%4) << 3); | |
if (i > 55) { | |
md5cycle(state, tail); | |
for (i=0; i<16; i++) tail[i] = 0; | |
} | |
tail[14] = n*8; | |
md5cycle(state, tail); | |
return state; | |
} | |
function md5blk(s) { /* I figured global was faster. */ | |
var md5blks = [], i; /* Andy King said do it this way. */ | |
for (i=0; i<64; i+=4) { | |
md5blks[i>>2] = s.charCodeAt(i) | |
+ (s.charCodeAt(i+1) << 8) | |
+ (s.charCodeAt(i+2) << 16) | |
+ (s.charCodeAt(i+3) << 24); | |
} | |
return md5blks; | |
} | |
var hex_chr = '0123456789abcdef'.split(''); | |
function rhex(n) | |
{ | |
var s='', j=0; | |
for(; j<4; j++) | |
s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] | |
+ hex_chr[(n >> (j * 8)) & 0x0F]; | |
return s; | |
} | |
function hex(x) { | |
for (var i=0; i<x.length; i++) | |
x[i] = rhex(x[i]); | |
return x.join(''); | |
} | |
function md5(s) { | |
return hex(md51(s)); | |
} | |
function add32(a, b) { | |
return (a + b) & 0xFFFFFFFF; | |
} | |
if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') { | |
function add32(x, y) { | |
var lsw = (x & 0xFFFF) + (y & 0xFFFF), | |
msw = (x >> 16) + (y >> 16) + (lsw >> 16); | |
return (msw << 16) | (lsw & 0xFFFF); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
jQuery(function($) { | |
// Password Input | |
var passwordInputBinding = new Shiny.InputBinding(); | |
$.extend(passwordInputBinding, { | |
find: function(scope) { | |
return $(scope).find('input[type="password"]'); | |
}, | |
getId: function(el) { | |
return Shiny.InputBinding.prototype.getId.call(this, el) || el.name; | |
}, | |
getValue: function(el) { | |
return md5(el.value); | |
}, | |
setValue: function(el, value) { | |
el.value = value; | |
}, | |
subscribe: function(el, callback) { | |
$(el).on('keyup.passwordInputBinding input.passwordInputBinding', function(event) { | |
callback(true); | |
}); | |
$(el).on('change.passwordInputBinding', function(event) { | |
callback(false); | |
}); | |
}, | |
unsubscribe: function(el) { | |
$(el).off('.passwordInputBinding'); | |
}, | |
getRatePolicy: function() { | |
return { | |
policy: 'debounce', | |
delay: 250 | |
}; | |
} | |
}); | |
Shiny.inputBindings.register(passwordInputBinding, 'shiny.passwordInput'); | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
add_cookie_handlers(page_fluid( | |
theme = bs_theme(version = 5), | |
# tags$style(type = 'text/css', '.modal-dialog { width: 90%; }'), | |
tagList( | |
tags$head( # Needed to encrypt the password when sent to the server | |
tags$link(rel = "stylesheet", type = "text/css", href = "style.css"), | |
tags$script(type = "text/javascript", src = "md5.js"), | |
tags$script(type = "text/javascript", src = "passwdInputBinding.js") | |
# tags$style(type = 'text/css', '.modal-dialog .modal-lg { width: 90%; !important; }') | |
), | |
tags$style(type="text/css", "pre { white-space: pre; word-wrap: normal; overflow-x: auto; font-size: 10pt; }"), | |
withMathJax(), | |
uiOutput('tabs') | |
) | |
)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment