├── modules ├── lib │ └── .keep ├── frontend │ ├── main.api │ ├── index.app │ └── auth.sjs └── backend │ └── user-db.sjs └── config.mho /modules/lib/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config.mho: -------------------------------------------------------------------------------- 1 | @ = require([ 2 | 'mho:std' 3 | ]); 4 | 5 | //---------------------------------------------------------------------- 6 | 7 | exports.serve = function(args) { 8 | 9 | var routes = [ 10 | // __mho, etc: 11 | @route.SystemRoutes(), 12 | 13 | // shared frontend/backend code: 14 | @route.CodeDirectory("lib/", 15 | require.url("./modules/lib") .. @url.toPath), 16 | 17 | // frontend: 18 | @route.ExecutableDirectory(/^/, 19 | require.url("./modules/frontend/") .. @url.toPath) 20 | ]; 21 | 22 | @server.run([ 23 | { 24 | address: @Port(6060), 25 | routes: routes 26 | } 27 | ]); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /modules/frontend/main.api: -------------------------------------------------------------------------------- 1 | @ = require([ 2 | 'mho:std', 3 | {id:'../backend/user-db', name: 'db'} 4 | ]); 5 | 6 | //---------------------------------------------------------------------- 7 | 8 | exports.login = function(credentials) { 9 | var {username, password} = credentials .. JSON.parse; 10 | @db.verifyUser(username, password); 11 | 12 | return Session(username); 13 | }; 14 | 15 | exports.register = function(credentials) { 16 | var {username, password} = credentials .. JSON.parse; 17 | @db.registerUser(username, password); 18 | 19 | return Session(username); 20 | } 21 | 22 | //---------------------------------------------------------------------- 23 | 24 | // session api for an authenticated user: 25 | function Session(username) { 26 | return { 27 | user: username, 28 | time: @generate(-> new Date()) 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /modules/frontend/index.app: -------------------------------------------------------------------------------- 1 | @ = require([ 2 | 'mho:std', 3 | 'mho:app', 4 | {id: './auth', name: 'auth'} 5 | ]); 6 | 7 | //---------------------------------------------------------------------- 8 | 9 | // add some static html: 10 | @mainContent .. @appendContent( 11 | @PageHeader('Session test app') 12 | ); 13 | 14 | // main program logic: 15 | function main() { 16 | @auth.withSession { 17 | |session| 18 | 19 | @mainContent .. @appendContent( 20 | [ 21 | @H4(`You are logged in as ${session.user}`), 22 | @Button('Log out') .. @OnClick({|| @auth.removeStoredCredentials(); 23 | return; // bail out of main() 24 | }), 25 | @H4(`The time at the server is ${session.time}`) 26 | ] 27 | ) { 28 | || 29 | // display this content 'forever' (or until retracted, by virtue 30 | // of the session going away): 31 | hold(); 32 | } 33 | } 34 | } 35 | 36 | // kick things off in a loop, so that the user logging out leads to 37 | // another login dialog: 38 | while (1) { 39 | main(); 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /modules/backend/user-db.sjs: -------------------------------------------------------------------------------- 1 | @ = require([ 2 | 'mho:std', 3 | {id: 'sjs:sjcl', name: 'crypto'} 4 | ]); 5 | 6 | 7 | //---------------------------------------------------------------------- 8 | // Password helper: 9 | 10 | // Create a PBKDF-2 derived password 11 | function derivePasswordForStoring(password_string) { 12 | var salt = @crypto.random.randomWords(4); 13 | var pass = @crypto.misc.pbkdf2(password_string, salt, 1000); 14 | return { 15 | algorithm: 'PBKDF2-HMAC-SHA256', 16 | count: 1000, 17 | salt: @crypto.codec.base64.fromBits(salt), 18 | pass: @crypto.codec.base64.fromBits(pass) 19 | }; 20 | } 21 | 22 | 23 | // Verify a password string against a PBKDF-2 derived password 24 | function verifyPassword(password_string, derived) { 25 | if (derived.algorithm !== 'PBKDF2-HMAC-SHA256') 26 | throw new Error("unknown password derivation algorithm #{derived.algorithm}"); 27 | 28 | var pass = @crypto.codec.base64.fromBits( 29 | @crypto.misc.pbkdf2(password_string, 30 | @crypto.codec.base64.toBits(derived.salt), 31 | derived.count)); 32 | 33 | return pass === derived.pass; 34 | }; 35 | 36 | 37 | //---------------------------------------------------------------------- 38 | 39 | // Username -> credentials 'db', to be implemented by a real db. In 40 | // current github conductance, we could e.g. use LocalDB or LevelDB 41 | // (see e.g. https://github.com/onilabs/conductance/blob/master/modules/flux/kv.sjs#L304) 42 | var Users = {}; 43 | 44 | 45 | /** 46 | @function verifyUser 47 | @param {String} [username] 48 | @param {String} [password] 49 | @summary Verify that the given user exists and the password matches; throw error if not 50 | */ 51 | exports.verifyUser = function(username, password) { 52 | var record = Users[username]; 53 | if (!record) throw new Error("Unknown user"); 54 | 55 | if (!verifyPassword(password, record)) 56 | throw new Error("Invalid password"); 57 | 58 | return true; 59 | }; 60 | 61 | /** 62 | @function registerUser 63 | @param {String} [username] 64 | @param {String} [password] 65 | @summary Register a new user under the given username; throw error if user already exists 66 | */ 67 | exports.registerUser = function(username, password) { 68 | if (Users[username]) throw new Error("User already exists"); 69 | 70 | Users[username] = derivePasswordForStoring(password); 71 | 72 | return true; 73 | }; 74 | 75 | -------------------------------------------------------------------------------- /modules/frontend/auth.sjs: -------------------------------------------------------------------------------- 1 | @ = require([ 2 | 'mho:std', 3 | 'mho:app', 4 | {id: 'sjs:sjcl', name: 'crypto' } 5 | ]); 6 | 7 | //---------------------------------------------------------------------- 8 | // helpers 9 | 10 | // We never use the user's cleartext password, but a hash derived from 11 | // the password. Note that this is just a complementary security 12 | // measure to prevent any casual inspection of a password that the 13 | // user might use elsewhere. A proper salted password will be derived 14 | // from this on the server. 15 | function obscurePassword(cleartext) { 16 | return "session-test-app #{cleartext}" .. 17 | @crypto.hash.sha256.hash .. 18 | @crypto.codec.base64.fromBits; 19 | } 20 | 21 | function removeStoredCredentials() { 22 | delete localStorage['session_test_app_credentials']; 23 | } 24 | exports.removeStoredCredentials = removeStoredCredentials; 25 | 26 | 27 | // The @Input signature has changed since Conductance v 0.5.1; make 28 | // our code compatible with all Conductance versions: 29 | var Input; 30 | if (@TextInput) { 31 | // conductance v 0.5.1 32 | Input = (settings) -> @Input(settings.type, settings.value); 33 | } 34 | else { 35 | Input = @Input; 36 | } 37 | 38 | 39 | //---------------------------------------------------------------------- 40 | /** 41 | @function withSession 42 | @param {Function} [block] Function to execute with session 43 | @summary Execute `block` with an authenticated server api session, 44 | prompting for username/password if neccessary. 45 | */ 46 | function withSession(block) { 47 | @withAPI('./main.api') { 48 | |api| 49 | 50 | while (1) { 51 | var session; 52 | // attempt to login in with stored credentials: 53 | if (localStorage['session_test_app_credentials']) { 54 | try { 55 | session = api.login(localStorage['session_test_app_credentials']); 56 | } 57 | catch (e) { 58 | // credentials are invalid 59 | removeStoredCredentials(); 60 | // go round loop again 61 | continue; 62 | } 63 | } 64 | else { 65 | // display a login/register dialog 66 | session = doLoginDialog(api); 67 | } 68 | 69 | // we've got a session; run our block: 70 | block(session); 71 | return; 72 | } 73 | } 74 | } 75 | exports.withSession = withSession; 76 | 77 | //---------------------------------------------------------------------- 78 | 79 | // helper to display a login/register dialog that returns a session: 80 | function doLoginDialog(api) { 81 | 82 | var Command = @Emitter(); 83 | var Username = @ObservableVar(); 84 | var Password = @ObservableVar(); 85 | var ErrorMessage = @ObservableVar(); 86 | 87 | @doModal( 88 | { 89 | title: 'Log in...', 90 | 91 | body: 92 | [ 93 | 'Username:', Input({type: 'text', value: Username}) .. @Autofocus, 94 | 'Password:', Input({type: 'password', value: Password}), 95 | @Span(ErrorMessage) .. @Style('color:red') 96 | ], 97 | 98 | footer: 99 | [ 100 | @Button('Register') .. @OnClick(-> Command.emit('register')), 101 | @Button('Log in') .. @OnClick(-> Command.emit('login')) 102 | ], 103 | 104 | close_button: false, 105 | keyboard: false, 106 | backdrop: 'static' 107 | } 108 | ) { 109 | || 110 | 111 | while (1) { 112 | 113 | var command = Command .. @wait(); 114 | 115 | var credentials = { 116 | username: Username .. @current, 117 | password: Password .. @current .. obscurePassword 118 | } .. JSON.stringify; 119 | 120 | try { 121 | var session; 122 | if (command === 'register') { 123 | session = api.register(credentials); 124 | } 125 | else if (command === 'login') { 126 | session = api.login(credentials); 127 | } 128 | // no error thrown -> we've got a session 129 | // store credentials for future use: 130 | localStorage['session_test_app_credentials'] = credentials; 131 | return session; 132 | } 133 | catch (e) { 134 | ErrorMessage.set(e.message); 135 | // go round loop again 136 | } 137 | } 138 | } 139 | } 140 | 141 | 142 | --------------------------------------------------------------------------------