Facebook doesn't know your Facebook password!
A website should never directly store a user's password
Instead: store a hashed and salted version of the password
plain data:
name | password
--------+--------------------
Alice | 123456
Bob | 123456
Charlie | abc123
Dave | correcthorsebattery
data with hashed passwords:
name | password hash
--------+---------------------------------
Alice | e10adc3949ba59abbe56e057f20f883e
Bob | e10adc3949ba59abbe56e057f20f883e
Charlie | e99a18c428cb38d5f260853678922e03
Dave | 3c077829151f03a4101bf36510d551b1
data with hashed and salted passwords:
name | salt | hash
--------+----------+---------------------------------
Alice | BzrYZGvv | c17dff0de6bbdfd0c8e7c2f35f2f74b0
Bob | w6hxMeFz | 107b7047ae12bd19ca64f34b49fa1c98
Charlie | uOqA9bpX | c087747abdda0dc67ae9f31871692453
Dave | nf7ExQnd | cd6bc62d87ad35d6ea4cbe83e89536f7
For Alice, the salt ("BzrYZGvv") and the hash of "123456-BzrYZGvv" are stored
sorted from most secure to not secure:
MD5 is not secure but is used in these examples because of its simplicity
A hash is a derived value that can be stored instead of a password
example MD5 hashes (not secure) in hex notation :
123456
→ e10adc3949ba59abbe56e057f20f883e
abc123
→ e99a18c428cb38d5f260853678922e03
user registration:
user sends password (e.g. 123456
), server saves the password hash (e.g. e10adc3949ba59abbe56e057f20f883e
)
user login:
user sends password, server computes its hash and compares it to the saved hash
Salted hashes are hashes of passwords with some additional random data
If passwords are hashed unsalted it would be easy to recognize hashes of common or simple passwords
based on MD5 (not secure):
Account creation:
alice
, password 123456
123456-BzrYZGvv
c17dff0de6bbdfd0c8e7c2f35f2f74b0
alice
BzrYZGvv
c17dff0de6bbdfd0c8e7c2f35f2f74b0
based on MD5 (not secure):
Login attempt (unsuccessful):
alice
, password 111111
alice
from the database: BzrYZGvv
111111-BzrYZGvv
c42f4b80513e7aee0ff1c5b7ebe339e0
c17dff0de6bbdfd0c8e7c2f35f2f74b0
)node packages for hashing algorithms:
argon2
(fast native implementation)argon2-wasm-pro
(compiled to WebAssembly)scrypt
(fast native implementation)scrypt-js
(pure JavaScript)Implementation with argon2-wasm-pro
and mingodb
(pure JavaScript)
Packages for real use cases: argon2
and mongodb
const express = require('express');
const argon2 = require('argon2-wasm-pro');
const crypto = require('crypto');
const mingodb = require('@karuga/mingodb');
const db = mingodb('data.json'); // simple db
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
res.end(
'welcome to the auth service\n\n' +
'resources:\n/register\n/login'
);
});
app.post('/register', async (req, res) => {
const username = req.body.username;
const password = req.body.password;
const user = db.users.findOne({ username: username });
if (user !== undefined) {
return res.json({
status: 'error',
message: 'user exists',
});
}
const hash = await argon2.hash({
pass: password,
salt: crypto.randomBytes(16),
});
const data = { username: username, hash: hash.encoded };
db.users.insertOne(data);
res.json({ status: 'success' });
});
using the /register
endpoint from the browser console:
let res = await fetch('/register', {
method: 'post',
body: JSON.stringify({
username: 'alice',
password: 'ecila',
}),
headers: { 'Content-Type': 'application/json' },
});
let data = await res.json();
console.log(data);
app.post('/login', async (req, res) => {
const verified = await verifyCredentials(
req.body.username,
req.body.password
);
if (!verified) {
return res.json({ status: 'error' });
}
res.json({ status: 'success' });
});
const verifyCredentials = async (username, password) => {
const user = db.users.findOne({ username: username });
if (user === undefined) {
return false; // user does not exist
}
try {
await argon2.verify({
pass: password,
encoded: user.hash,
});
return true;
} catch {
return false; // wrong password
}
};
using the /login
endpoint from the browser console:
let res = await fetch('/login', {
method: 'post',
body: JSON.stringify({
username: 'alice',
password: 'ecila',
}),
headers: { 'Content-Type': 'application/json' },
});
let data = await res.json();
console.log(data);
use cases:
Alice has a secret private key and publishes an associated public key
Alice can "sign" some information with her private key - recipients of that information can verify that it is from Alice by using Alice's public key
Anyone can encrypt information using the public key and send it to Alice - only Alice can decrypt it with her private key, no one else can read it
common cryptographic algorithm: RSA
Keys are often stored in .pem
format
alice_private.pem
:
-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAMUKUlOcPJ4E0T1/9qirGZ+1udvYF9Bqvrc2zuoplenl8S0bdXGG
vm1SlCUX6UMKC/YkB77BjFaRIvhceCrTriUCAwEAAQJBAIn2KySNrUe6+cKE2XDJ
tDxGImdSy4HLa9OelYwRJ/1HHclYgACwzigWT6U+Xaej95lzKrWV+Gwlw/q10dWA
ZrUCIQD4xrKnJtGgU/HI7piVGNvYl7jP3zQJMMagq+iYKSi0/wIhAMrDCkwEcSld
E2HVyWUln+XTNwEVg484QZiPvE2aRyjbAiB5YjH/XvB/gxYBTXHDpfp3ByiUvLqe
FV+FO/vkaojDLwIgBpVusCk0w3MSPgsDDxW5q2zATHi2XOAmwR1pr9tilCECICRO
Lj9zPl9v6NhpmAXFPffzH7SJ5eIoF6bxu0j8l3GL
-----END RSA PRIVATE KEY-----
alice_public.pem
:
-----BEGIN RSA PUBLIC KEY-----
MEgCQQDFClJTnDyeBNE9f/aoqxmftbnb2BfQar63Ns7qKZXp5fEtG3Vxhr5tUpQl
F+lDCgv2JAe+wYxWkSL4XHgq064lAgMBAAE=
-----END RSA PUBLIC KEY-----
generating a pair of RSA keys in the browser:
https://www.csfieldguide.org.nz/en/interactives/rsa-key-generator/
const crypto = require('crypto');
let { privateKey, publicKey } = crypto.generateKeyPairSync(
'rsa',
{
modulusLength: 512,
}
);
let privateKeyPemString = privateKey.export({
type: 'pkcs1',
format: 'pem',
});
fs.writeFileSync('alice_private.pem', privateKeyPemString);
let privateKeyPemString = fs.readFileSync(
'alice_private.pem',
{ encoding: 'ascii' }
);
let privateKey = crypto.createPrivateKey(
privateKeyPemString
);
let message =
'the holder of this token is logged in as user1';
let signature = crypto.sign(
'SHA256',
Buffer.from(message, 'utf-8'),
privateKey
);
verifying a genuine message:
let message =
'the holder of this token is logged in as user1';
let verifiedA = crypto.verify(
'SHA256',
Buffer.from(message, 'utf-8'),
publicKey,
signature
);
// true
verification of fake message fails:
let fakeMessage =
'the holder of this token is logged in as admin';
let verifiedB = crypto.verify(
'SHA256',
Buffer.from(fakeMessage, 'utf-8'),
publicKey,
signature
);
// false
encrypting data to send to Alice:
let message = 'Hello, Alice!';
let encryptedMessage = crypto.publicEncrypt(
publicKey,
Buffer.from(message, 'utf-8')
);
decrypting of data by Alice:
let decryptedMessage = crypto
.privateDecrypt(privateKey, encryptedMessage)
.toString('utf-8');
Common procedure:
If a user logged in successfully they receive a secret token that identifies them for some time (e.g. for 30 minutes or for 1 day)
contents of tokens:
approach A: token contains a unique session id; associated session data is saved on a server
approach B: token contains all session data, signed by an authorization service
JSON web tokens (JWT) are a means for a user to identify themselves to a web site
example contents of a JWT (3 parts: algorithm, data, signature):
{ "alg": "RS256" }
{
"iss": "google.com",
"sub": "alice@gmail.com",
"aud": "medium.com",
"exp": 1577836800
}
eyJzdWIiOiJhbGljZSIsImlzcyI6ImF1dGguZ...
translation:
This is a JSON web token signed with RS256
(RSA Signature with SHA-256)
We (google.com) confirm that the holder of this token is
logged in as "alice@gmail.com" with our service.
This confirmation is intended for use on medium.com.
This confirmation is valid until 2020-01-01 00:00.
signature: ...
scenario:
auth.foo-systems.com
forum.foo-systems.com
extension of the code from the previous section:
const fs = require('fs');
const jsonwebtoken = require('jsonwebtoken');
const publicKey = fs.readFileSync('./public.pem');
const privateKey = fs.readFileSync('./private.pem');
updated start page handler:
app.get('/', (req, res) => {
res.end(
'welcome to the auth service\n\n' +
'resources:\n/register\n/login\n/public-key'
);
});
make public key available:
app.get('/public-key', (req, res) => {
res.end(publicKey);
});
app.post('/login', async (req, res) => {
const verified = await verifyCredentials(
req.body.username,
req.body.password
);
if (!verified) {
return res.json({ status: 'error' });
}
const token = createToken(req.body.username);
res.json({ status: 'success', token: token });
});
const createToken = username => {
return jsonwebtoken.sign(
{
sub: username,
iss: 'auth.foo-systems.com',
aud: 'forum.foo-systems.com',
// expires in 1 h
exp: Math.floor(Date.now() / 1000) + 60 * 60,
},
privateKey,
{ algorithm: 'RS256' }
);
};
using the /login
endpoint from the browser console:
let res = await fetch('/login', {
method: 'post',
body: JSON.stringify({
username: 'alice',
password: 'ecila',
}),
headers: { 'Content-Type': 'application/json' },
});
let data = await res.json();
console.log(data);
online version:
A JWT can be sent in the HTTP authorization header as a so-called bearer token to verify the user's identity:
Authorization: Bearer eyJhbGciOiJSUzI...
Middleware that restricts route access to clients that are logged in with the auth service:
const restrictToLoggedin = expressJwt({
secret: publicKey,
});
app.get('/public', (req, res) => {
res.json(publicData);
});
app.get('/private', restrictToLoggedin, (req, res) => {
if (req.user) {
res.json(privateData);
} else {
res.json({ status: 'auth error' });
}
});
complete code:
const fs = require('fs');
const cors = require('cors');
const express = require('express');
const expressJwt = require('express-jwt');
const publicKey = fs.readFileSync('./public.pem', {
encoding: 'utf-8',
});
const secretData = ['foo', 'bar', 'baz'];
const app = express();
app.use(express.json());
app.use(cors());
const restrictToLoggedin = expressJwt({
secret: publicKey,
});
app.get('/', (request, response) => {
response.json({ page: 'public start page' });
});
app.get('/items', restrictToLoggedin, (req, res) => {
res.json(secretData);
});
authorization service: provides a token that enables the holder to request some actions on behalf of the token's subject
example authorization token for GitHub:
authorization token for GitHub
the holder of this token may:
- create new repositories for user "marko-knoebl"
- delete repositories belonging to user "marko-knoebl"
standard claims in openID connect: