Skip to content

Instantly share code, notes, and snippets.

@tannerhallman
Last active July 27, 2019 03:25
Show Gist options
  • Save tannerhallman/b9de18c13e82777e52f433f5321e89a1 to your computer and use it in GitHub Desktop.
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. :)
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