Authentication vs authorization.
Password storage.
Demo following technologies:
JSON
Asynchronous programming using async
and await
.
Mongo db
Testing.
Authentication: Who we are. Example: user-id and password.
Authorization: What we can do. Example: Unix file system permissions.
2-Factor Authentication: Authentication based on 2 independent factors: for example, what we know and what we have. Examples: user-id password and fingerprint; user-id password and cellphone; user-id password and iris scan.
Authentication and authorization are orthogonal concepts, but authorization usually requires some kind of earlier authentication.
Access control: control access to resources based on authentication and authorization.
A responsible programmer should never store a plaintext password.
Typically, passwords are hashed (not encrypted) using a one-way hashing algorithm like SHA256 or bcrypt. Only hash is stored. When user logs in, entered password is hashed and compared with stored hash; if they match, then login is allowed, else denied.
Simple hash is amenable to a dictionary attack where hashes are precomputed for many common passwords; if precomputed hash matches stored hash (got by accessing the password file), then password is dictionary word.
Map variable length content to fixed-length hash string.
Good hashing algorithms ensure that changing the content slightly (even by a single bit) will result in a different hash.
The uses of hashing include hash tables, summarizing content, cryptographic signatures.
Cryptographically secure hashing algorithms SHA-256, and bcrypt. The latter is purposely slow. Note that MD5 is no longer regarded as cryptographically secure.
$ echo hello | sha256sum 5891b5b522d5df086d0ff... (64 hexets) $ echo hellp | sha256sum bf8c83416f31143ee2fa5... (64 hexets) $
One way of cracking passwords is to have pre-built maps mapping
common "dictionary words" to their hash values and simply find a
match in the stored hashes for the hash of the password being
cracked.
Dictionary attacks can be made harder by adding a random salt to each password before hashing. The salt is stored along with the hash to allow matching an entered password. So a 2 character salt with 64 possibilities for each salt character would increase the number of combinations for a dictionary attack by 4096.
In 2000's, brute-forcing passwords becoming possible because of extremely fast hardware (sometimes using GPUs).
Normal hashing algorithms like SHA-256 were designed to be fast.
Current best practice for password hashing is to use purposely
slowed up hash algorithms like bcrypt
to make it harder for
crackers.
import bcrypt from 'bcrypt'; async function go() { const PW = 'asdf'; const h1 = await bcrypt.hash(PW, 10); const h2 = await bcrypt.hash(PW, 10); console.log(`hash1: ${h1}`); console.log(`hash2: ${h2}`); console.log('cmp1', await bcrypt.compare(PW, h1)); console.log('cmp2', await bcrypt.compare(PW + 'a', h1)); } go().catch(err => console.error(err));
$ node code/auth/bin/bcrypt-hash.mjs hash1: $2b$10$wXRP2XE8q3czM8mHe9Fi2.h... (truncated) hash2: $2b$10$0zqbCRkJomsoXaL3oSo4De2... (truncated) cmp1 true cmp2 false
One of many nosql databases. No rigid relations need to be predefined.
Allows storing and JS data serialized in "documents" having
_id
as primary key (when storing a document without an _id
property, it is created
automatically with a suitable ID value).
Allows indexing.
Provides basic Create-Read-Update-Delete (CRUD) repertoire.
All operations asynchronous. Set up to return a Promise
when
called without a handler.
insertOne()
and insertMany()
.
find()
returns a Cursor
. Can grab all using toArray()
.
updateOne()
and updateMany()
. Also, findOneAndUpdate()
and findOneAndReplace()
. Can combine insert and update
functionality using upsert
option.
deleteOne()
and deleteMany()
.
As mentioned, most DB operations are asynchronous.
Usually a good idea to cache DB connection as opening a DB connection is an expensive operation.
Create a Data Access Object (DAO) which wraps a database. Cache database connection within object when object is first created.
DAO should provide dumb persistence operations and not contain domain logic.
Getting a connection to a DB is an asynchronous operation.
Can we build object using an async
constructor
??
Use a static factory method instead.
$ npm init -y #creates package.json ... $ npm install mongodb #old versions required --save npm notice created a lockfile as package-lock.json... ... $ ls -a .gitignore node_modules package.json package-lock.json ... $
Allows interacting with mongo db. Following log assumes that
collection userInfos
in db users
is loaded with simpsons data.
$ mongosh Current Mongosh Log ID: ... ... ... test> help ... > use users //assume populated with Simpsons switched to db users users> db.userInfos.find({}) { "_id" : "bart", "id" : "bart", ... } { "_id" : "marge", "id" : "marge", ... } { "_id" : "lisa", "id" : "lisa", ... } { "_id" : "homer", "id" : "homer", ... } users> db.userInfos.find({"firstName": "Bart"}) { "_id" : "bart", "id" : "bart", ... } users> db.userInfos.find({}).length() 4
users> db.userInfos.deleteOne({"firstName": "Bart"}) { "acknowledged" : true, "deletedCount" : 1 } > db.userInfos.find({}).length() 3 users> db.userInfos.deleteMany({}) { "acknowledged" : true, "deletedCount" : 3 } users> db.userInfos.find({}).length() 0
Store user-info objects.
User info objects can contain any fields.
Demonstrate use of insert*()
, find*()
, delete*()
.
Data Access Object which implements db operations.
User interface.
Authentication services: shows validation using validator module.
Memory based mongo double. Instantiate actual user DAO, but using an in-memory mongo db instance. Advantages include speed and that clearing of in-memory db before each test.
Auth dao tests.
Auth service tests.