/ *
Implements everything needed for a [ Passport . js ] ( https : //www.passportjs.org) middleware.
It supports either a Redis store or a MemoryStore . The MemoryStore can be used in development
if you can 't or won' t run Redis while you work . Ultimately you should be using the Redis
store even in development as it keeps you logged in while you work which is easier .
The type of store is configured in the ` secrets/config.json ` file ( which is loaded into ` lib/config.js ` ) using the ` session_store ` option . See ` configure_sessions ` for more information on this
configuration option .
The most important part of this module is the ` socket ` function , which adds ` Passport.js `
authentication to ` socket.io ` which is something missing from ` Passport.js ` . Look at that
if you wondered how to do that .
* /
import cookie from 'cookie' ;
import { Strategy } from 'passport-local' ;
import passport from 'passport' ;
import signature from 'cookie-signature' ;
import redis from 'redis' ;
import session from 'express-session' ;
import memorystore from 'memorystore' ;
import connectRedis from 'connect-redis' ;
import * as models from '../lib/models.js' ;
import logging from '../lib/logging.js' ;
import { session _store , auth } from "../lib/config.js" ;
import assert from "assert" ;
const log = logging . create ( "lib/auth.js" ) ;
const redisClient = redis . createClient ( ) ;
const MemoryStore = memorystore ( session ) ;
const RedisStore = connectRedis ( session ) ;
passport . use ( new Strategy (
async ( username , password , cb ) => {
let user = await models . User . auth ( username , password ) ;
return user ? cb ( null , user ) : cb ( null , false ) ;
} )
) ;
passport . serializeUser ( ( user , cb ) => {
cb ( null , user . id ) ;
} ) ;
passport . deserializeUser ( async ( id , cb ) => {
try {
const user = await models . User . first ( { id } ) ;
if ( user ) {
return cb ( null , user ) ;
} else {
return cb ( null , false ) ;
}
} catch ( error ) {
log . error ( error ) ;
return cb ( null , false ) ;
}
} ) ;
/ *
Configures ` Passport.js ` to use either ` RedisStore ` or ` MemoryStore ` . If you want to
add a different store then modify this function . If the ` config.json:session_store ` is
` "redis" ` you 'll get the `RedisStore`. If it' s set to ` "memory" ` then it will use the
` MemoryStore ` .
_ _ _WARNING _ _ _ : You don ' t really call this and instead use the ` sessionStore ` variable in
this module .
+ _ _ _return _ _ _ ( RedisStore | MemoryStore | undefined ) -- The store to use or ` undefined ` on error .
* /
export const configure _sessions = ( ) => {
if ( session _store === "redis" ) {
log . info ( "Using redis to store sessions." ) ;
return new RedisStore ( { client : redisClient } ) ;
} else if ( session _store === "memory" ) {
log . warn ( "Using memory to store sessions. Restarts will force logins." ) ;
return new MemoryStore ( { checkPeriod : 86400000 , } ) ;
} else {
assert ( false , ` Error, unknown session store ${ session _store } in secrets/config.json. Only "redis" and "memory" supported. ` ) ;
return undefined ; // shouldn't reach this but shut up eslint
}
}
/ *
* The configured session store being used . Just use this instead of
* calling ` configure_sessions ` yourself .
* /
export const sessionStore = configure _sessions ( ) ;
/ *
The cookie secret configured in ` secrets/config.json:auth ` .
_ _ _FOOTGUN _ _ _ : Don ' t expose this to the internet !
* /
export const cookie _secret = auth . cookie _secret ;
/ *
The cookie _domain _ configured in ` secrets/config.json:auth ` . If this isn ' t
exactly the same as your domain then you ' ll not be able to log in . You can
run :
` ` ` shell
node bando . js api -- cookies - suck
` ` `
This will output cookie debugging information so you can figure out what ' s going on .
Incidentally , debugging the stupid cookies ends up a huge chunk of the code in ` commands/api.js `
because , you guessed it , ` --cookies-suck ` .
* /
export const cookie _domain = auth . cookie _domain ;
assert ( cookie _domain !== undefined , "secrets/config.json:auth.cookie_domain isn't set." ) ;
/ *
This does the actual ` Passport.js ` login , but you wouldn ' t necessarily use this
directly unless you are making something custom . Instead , you "tag" handlers
in ` api/ ` handlers with ` .login = true ` and then the ` commands/api.js ` will
automatically run this before running your handler . Look in ` api/login.js ` to see
how to use ` post.login = true ` to actually use this .
If you want to see how this is used , look for ` auth.login ` in ` commands/api.js ` .
+ _ _ _return _ _ Function -- Returns a Express . js handler that does the ` passport.authenticate ` .
* /
export const login = ( ) => {
return ( req , res , next ) => {
passport . authenticate ( 'local' , ( err , user , info ) => {
if ( err ) {
log . error ( err , info ) ;
return res . status ( 500 ) . json ( { error : info } ) ;
} else if ( ! user ) {
// log.debug(`Auth attempt user is ${user.id}.`);
return res . status ( 401 ) . json ( { error : "Authentication required" } ) ;
} else {
// log.debug(`User exists, attempting login ${user.id}`);
return req . login ( user , ( err ) => {
if ( err ) {
log . error ( err ) ;
res . status ( 403 ) . json ( { error : "Invalid login" } ) ;
// BUG: is this right or is next("route")
return next ( err ) ;
} else {
return next ( ) ;
}
} ) ;
}
} ) ( req , res , next ) ;
}
}
/ *
An Express . js handler that make authentication required . Just like with ` login ` it
is normally used by adding the ` .authenticated = true ` tag to your handlers . If you
do ` get.authenticated = true ` then that ` get ` will require a login before it runs .
The ` commands/api.js ` command is the one that uses this ` require ` function to add that
feature to your handlers in ` api/ ` .
* /
export const required = ( ) => {
return ( req , res , next ) => {
const user = req . user ;
if ( ! user ) {
return res . status ( 401 ) . json ( { error : "Authentication required" } ) ;
// WARNING: next('route') is NOT needed to abort the chain of calls despite express docs
} else {
return next ( ) ;
}
}
}
/ *
* This is added to any ` socket/ ` handlers tagged with ` .authenticated ` . It does
* the same thing as ` required ` above but instead does all the fairly stupid things
* you need to authenticate a ` socket.io ` connection .
*
* It ' s main purpose is to authenticate , but _also _ to connect the ` req.user ` to the
* socket so you can access the user in your ` socket/ ` handlers .
*
* + ` socket Object ` -- A socket . io socket to authenticate .
* + _ _ _return _ _ _ ` boolean ` -- A ` Promise ` that performs the authentication then adds the ` req.user ` to the socket .
* /
export const socket = async ( socket ) => {
try {
if ( ! socket . handshake . headers . cookie ) {
throw new Error ( "No cookies in socket handshake headers, disconnecting socket." ) ;
}
let cookies = cookie . parse ( socket . handshake . headers . cookie ) ;
if ( ! cookies ) {
throw new Error ( "No cookies were parsed even though the header exists, disconnecting socket." ) ;
}
const connect _sid = cookies [ 'connect.sid' ] ;
if ( ! connect _sid ) {
throw new Error ( "No connect.sid cookie in the list of cookies, disconnecting socket." ) ;
}
if ( connect _sid . slice ( 0 , 2 ) !== 's:' ) {
throw new Error ( ` is_authenticate only supports signed cookies. Your cookies don't start with s: they start with ${ connect _sid . slice ( 0 , 2 ) } ` ) ;
}
let session _id = signature . unsign ( connect _sid . slice ( 2 ) , cookie _secret ) ;
log . debug ( ` Session id is ${ session _id } ` ) ;
if ( ! session _id ) {
throw new Error ( ` Unable to get session_id ${ session _id } with session signature. ` ) ;
}
return new Promise ( ( resolve , reject ) => {
sessionStore . get ( session _id , ( err , session ) => {
socket . express _session = session ;
if ( err ) {
log . error ( err ) ;
reject ( err ) ;
} else if ( session && session . passport && session . passport . user !== undefined ) {
socket . user _id = session . passport . user ;
resolve ( true ) ;
} else {
socket . disconnect ( ) ;
delete socket . user ;
delete socket . express _session ;
resolve ( false ) ;
}
} ) ;
} ) ;
} catch ( error ) {
// not logging the error stack trace for now since i'm using the Error as a jump/clean
log . debug ( error . message ) ;
socket . disconnect ( ) ;
delete socket . user ;
delete socket . express _session ;
return false ;
}
}
/ *
Initializes your Express . js application with the ` Passport.js ` required handlers .
* /
export const init = ( app ) => {
app . use ( passport . initialize ( ) ) ;
app . use ( passport . session ( ) ) ;
}