Last active
July 27, 2019 03:25
-
-
Save tannerhallman/b9de18c13e82777e52f433f5321e89a1 to your computer and use it in GitHub Desktop.
Rails devise_token_auth recreated in javascript so our rails monolith can have authenticates JS microservices π. I pulled ruby code straight out of the devise_token_auth methods. Hope this saves you the hours it took me. :)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const bcrypt = require("bcrypt"); //package.json -- "bcrypt": "^3.0.6", ruby's gem version -> devise_token_auth (0.1.42) | |
// async function main() { | |
// // Create a new user called `Alice` | |
// //const newUser = await prisma.createUser({ name: 'Alice' }) | |
// //console.log(`Created new user: ${newUser.name} (ID: ${newUser.id})`) | |
// // Read all users from the database and print them to the console | |
// //const allUsers = await prisma.users() | |
// //console.log(allUsers) | |
// } | |
// main().catch(e => console.error(e)) | |
function isTokenValid(user, accessToken, clientId) { | |
// | |
return new Promise(async resolve => { | |
let validity = false; | |
let resolvedContext = {}; | |
let userTokens = JSON.parse(user.tokens); // the array of user.tokens | |
let tokenObject = null; // the object derived from the user tokens where client IDs match | |
// parse the client tokens | |
const userTokenKeys = Object.keys(userTokens); | |
userTokenKeys.forEach((clientKey, index) => { | |
if (clientKey === clientId) { | |
// if the client on user.tokens matches clientId header | |
tokenObject = userTokens[clientKey]; | |
} | |
}); | |
// is token current | |
const tokenCurrent = await isTokenCurrent(tokenObject, accessToken); | |
// can token be reused | |
const tokenReusable = await canTokenBeReused(tokenObject, accessToken); | |
if (tokenCurrent && tokenReusable) { | |
resolve({ | |
validity, | |
resolvedContext | |
}); | |
} | |
}); | |
// def valid_token?(token, client_id='default') | |
// client_id ||= 'default' | |
// return false unless self.tokens[client_id] | |
// return true if token_is_current?(token, client_id) | |
// return true if token_can_be_reused?(token, client_id) | |
// # return false if none of the above conditions are met | |
// return false | |
// end | |
} | |
async function isTokenCurrent(tokenObject, accessToken) { | |
let isCurrent = false; | |
// user.tokens => | |
// {"1p34up1238u41234"=> | |
// {"token"=>"$2a$10$BBvF9HONEsWqiZn/KPCf/OF3M/JhhhhgpfwTzrVYqWqdAcG3iE0q", "expiry"=>1603005270}, | |
// "J5f3FJf2U-LN1Sor0kTVUg"=> | |
// {"token"=>"$2a$10$3n/Ot8PYhhhhhqOSCLFQLuNjtxUt.FqCr7tMmBkrTjL2tsOHHeqcS", "expiry"=>1603005277}} | |
// def token_is_current?(token, client_id) | |
// # ghetto HashWithIndifferentAccess | |
// expiry = self.tokens[client_id]['expiry'] || self.tokens[client_id][:expiry] | |
// token_hash = self.tokens[client_id]['token'] || self.tokens[client_id][:token] | |
// return true if ( | |
// # ensure that expiry and token are set | |
// expiry && token && | |
let expiry = tokenObject.expiry; | |
let tokenFromTokenObject = tokenObject.token; | |
if (expiry && tokenFromTokenObject) { | |
// # ensure that the token has not yet expired | |
// DateTime.strptime(expiry.to_s, '%s') > Time.now && | |
const isExpiryLaterThanNow = new Date(expiry * 1000) > new Date(); | |
if (isExpiryLaterThanNow) { | |
const doTokensMatch = await tokensMatch( | |
tokenFromTokenObject, | |
accessToken | |
); | |
if (doTokensMatch) { | |
// # ensure that the token is valid | |
// DeviseTokenAuth::Concerns::User.tokens_match?(token_hash, token) | |
// ) | |
// end | |
isCurrent = true; | |
} | |
} | |
} | |
return isCurrent; | |
} | |
function canTokenBeReused(tokenObject, accessToken) { | |
// def token_can_be_reused?(token, client_id) | |
// # ghetto HashWithIndifferentAccess | |
let isResusable = false; | |
return new Promise(async resolve => { | |
let updatedAt = tokenObject.updated_at; | |
let lastToken = tokenObject.last_token; | |
// updated_at = self.tokens[client_id]['updated_at'] || self.tokens[client_id][:updated_at] | |
// last_token = self.tokens[client_id]['last_token'] || self.tokens[client_id][:last_token] | |
if (updatedAt && lastToken) { | |
// # ensure that the last token and its creation time exist | |
// # ensure that previous token falls within the batch buffer throttle time of the last request | |
// Time.parse(updated_at) > Time.now - DeviseTokenAuth.batch_request_buffer_throttle && | |
// # ensure that the token is valid | |
// ::BCrypt::Password.new(last_token) == token | |
const batch_request_buffer_throttle = 5000; | |
const isLastTokenInBufferThrottle = | |
new Date(updatedAt * 1000) > | |
new Date(Date.now() - batch_request_buffer_throttle); | |
if (isLastTokenInBufferThrottle) { | |
const doTokensMatch = await tokensMatch(lastToken, accessToken); | |
if (doTokensMatch) { | |
isResusable = true; | |
} | |
} | |
} | |
resolve(isResusable); | |
}); | |
return isResusable; | |
} | |
function tokensMatch(tokenHash, accessToken) { | |
// https://www.rubydoc.info/github/codahale/bcrypt-ruby/BCrypt/Password#create-class_method | |
return new Promise(resolve => { | |
bcrypt.compare(accessToken, tokenHash).then(function(res) { | |
resolve(res); | |
}); | |
}); | |
// def self.tokens_match?(token_hash, token) | |
// @token_equality_cache ||= {} | |
// key = "#{token_hash}/#{token}" | |
// result = @token_equality_cache[key] ||= (::BCrypt::Password.new(token_hash) == token) | |
// if @token_equality_cache.size > 10000 | |
// @token_equality_cache = {} | |
// end | |
// result | |
// end | |
} | |
// This is how you would use this.. | |
// req represents the incoming http request and we're accessing the headers. | |
let uid = req.headers.uid; | |
let accessToken = req.headers["access-token"]; | |
let clientId = req.headers.client; | |
let clientVersion = req.headers["client-version"]; | |
user = // await prisma.user({ email: uid }); // get the user object from the database somehow. | |
if (user) { | |
const tokenValid = await isTokenValid(user, accessToken, clientId); | |
// continue with other business logic! | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment