Skip to content

Instantly share code, notes, and snippets.

@jbryer
Created January 26, 2024 03:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbryer/d563b3147c7e5ea8c8d7910601676e39 to your computer and use it in GitHub Desktop.
Save jbryer/d563b3147c7e5ea8c8d7910601676e39 to your computer and use it in GitHub Desktop.
Shiny authentication
# This script is modified by Jason Bryer (jason@bryer.org) 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)
// 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);
}
}
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');
})
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