Click here to see Overview Diagram
Hi All,
I have service A that needs to call service B in different network domain. To make a call to service B, service A gets access token from identity provider then call service B with the access token in Http Authorization header. When there are multiple or concurrent requests to service A, I want to minimize the calls to identity provider to get access token. So I plan to implement caching by using https://www.npmjs.com/package/lru-cache which is similar to the approach using by google-auth-library
https://github.com/googleapis/google-auth-library-nodejs/blob/master/src/auth/jwtaccess.ts.
The service A will call identity provider to get access token and store to the cache. When the next request come in, the service A will use the token from cache and calls service B. If the cache item is expired, then service A will get service token and store in cache.
I have the following questions:
How do we handle race condition when there are concurrent request to service A that can cause multiple requests are sent to get access token and have multiple updates to the cache?
Let say, access token have 1 hour expiry. How do we have mechanism to get a new token before the token is expired?
Any comments would be very appreciated. Thank you in advance.
It sounds like you would benefit from a little singleton object that manages the token for you. You can create an interface for getting the token that does the following:
If no relevant token in the cache, go get a new one and return a promise that will resolve with the token. Store that promise in the cache in place of the token.
If there is a relevant token in the cache, check it's expiration. If it has expired or is about to expire, delete it and go to step 1. If it's still good, return a promise that resolves with the cached token (this way it always returns a promise, whether cached or not).
If the cache is in the process of getting a new token, there will be a fresh token stored in the cache that represents the future arrival of the new token so the cache can just return that promise and it will resolve to the token that is in the process of being fetched.
The caller's code would look like this:
tokenCache.getToken().then(token => {
// use token here
});
All the logic behind steps 1, 2 and 3 is encapsulated inside the getToken() method.
Here's an outline for a tokenCache class that hopefully gives you the general idea:
const tokenExpiration = 60 * 60 * 1000; // 1 hr in ms
const tokenBeforeTime = 5 * 60 * 1000; // 5 min in ms
class tokenCache {
constructor() {
this.tokenPromise = null;
this.timer = null;
// go get the first token
this._getNewToken().catch(err => {
console.log("error fetching initial token", err);
});
}
getToken() {
if (this.tokenPromise) {
return this.tokenPromise().then(tokenData => {
// if token has expired
if (tokenData.expires < Date.now()) {
return this._getNewToken();
} else {
return tokenData.token;
}
});
} else {
return this._getNewToken();
}
}
// non-public method for getting a new token
_getNewToken() {
// for example purposes, this uses the got() library to make an http request
// you fill in however you want to contact the identity provider to get a new token
this.tokenPromise = got(tokenURL).then(token => {
// make resolve value be an object that contains the token and the expiration
// set timer to get a new token automatically right before expiration
this._scheduleTokenRefresh(tokenExpiration - tokenBeforeTime);
return {
token: token,
expires: Date.now() + tokenExpiration;
}
}).catch(err => {
// up error, clear the cached promise, log the error, keep the promise rejected
console.log(err);
this.tokenPromise = null;
throw err;
});
return this.tokenPromise;
}
// schedule a call to refresh the token before it expires
_scheduleTokenRefresh(t) {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this._getNewToken().catch(err => {
console.log("Error updating token before expiration", err);
});
this.timer = null;
}, t);
}
}
How do we handle race condition when there are concurrent request to service A that can cause multiple requests are sent to get access token and have multiple updates to the cache?
You store a promise and always return that promise. Whether you're in the middle of getting a new token or there's already a token in that promise, it doesn't matter. You return the promise and the caller uses .then() or await on the promise to get the token. It "just works" either way.
Let say, access token have 1 hour expiry. How do we have mechanism to get a new token before the token is expired?
You can check the token for expiration when it's requested and if it's expired, you replace the existing promise with one that represents a new request for the token.
Related
In example project provided by Microsoft here which uses Authorization code flow the acquireTokenByCode method does not return refresh tokens.
From #azure/msal-node here refresh token is not mentioned.
Result returned from the authority's token endpoint.
uniqueId - oid or sub claim from ID token
tenantId - tid claim from ID token
scopes - Scopes that are validated for the respective token
account - An account object representation of the currently signed-in user
idToken - Id token received as part of the response
idTokenClaims - MSAL-relevant ID token claims
accessToken - Access token received as part of the response
fromCache - Boolean denoting whether token came from cache
expiresOn - Javascript Date object representing relative expiration of access token
extExpiresOn - Javascript Date object representing extended relative expiration of access token in case of server outage
state - Value passed in by user in request
familyId - Family ID identifier, usually only used for refresh tokens
please ensure your MSAL authorization code request includes the offline_access scope.
You could use MSAL.js to get token in this case, there is acquireTokenSilent method, it can perform silent renewal of tokens, which means you are no need to get the refresh token by yourself.
Popup
var request = {
scopes: ["Mail.Read"]
};
msalInstance.acquireTokenSilent(request).then(tokenResponse => {
// Do something with the tokenResponse
}).catch(async (error) => {
if (error instanceof InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return myMSALObj.acquireTokenPopup(request);
}
}).catch(error => {
handleError(error);
});
Redirect
var request = {
scopes: ["Mail.Read"]
};
msalInstance.acquireTokenSilent(request).then(tokenResponse => {
// Do something with the tokenResponse
}).catch(error => {
if (error instanceof InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return myMSALObj.acquireTokenRedirect(request)
}
});
It's designed to not return the refresh token if you are using #azure/msal-node.
As they stated in the discussion, the refresh token is handled background, inside the library itself for better security, which I also disagree with.
However, if you insist to have the token, you can manually call the API to the AzureAD endpoint.
I have a Node.js service deployed on App Engine which uses the Dialogflow fulfillment library. The scenario is this: I have an async function which retrieves the credentials using Secret manager and, with that info, calls an API that brings a url instance and a token back. This is a server-to-server authentication (OAuth), so it is the same for all users that access it. I set those values in global variables, like this:
let globalUser = "";
let globalPass = "";
...
async function credentials() {
const credentials = await secretsInstance.getCredentials();
const parsedCredentials = JSON.parse(credentials);
const user = parsedCredentials.user;
const pass = parsedCredentials.pass;
//setting the values to the global variables
globalUser = user;
globalPass = pass;
//call the authentication API - in the callback I set other global variables
await authApiInstance.authenticate(user, pass, callback);
}
After the callback function is called, I set the instance url and token to the global variables.
The token gets expired each 20 minutes, so I need to keep it updated. For that I call a setInterval function in which I call the authApiInstance.authenticate(...)
The problem here is that, when receiving a POST request coming from Dialogflow, I need to call another API that needs that url, which in this stage is empty for the first time, so it throws ECONNREFUSED. Then if I call the server other times, the variable is set.
The logs in GCP are like this:
2020-08-14 23:29:49.078 BRT
"Calling the loadQuestions API
2020-08-14 23:29:49.078 BRT
"The url is: /services/…
2020-08-14 23:29:49.091 BRT
"CATCH: Error: connect ECONNREFUSED 127.0.0.1:80"
2020-08-14 23:29:49.268 BRT
dialogflowGatewayProdxjmztxaet4d8Function execution took 764 ms, finished with status code:
200
2020-08-14 23:29:49.278 BRT
{ message_id: '39045207393', status: 200 }
2020-08-14 23:29:49.289 BRT
"Credentials ok"
2020-08-14 23:29:49.976 BRT
"Url set"
As it can be seen, the credentials and url were set after the API got called, so it didn't have a url to proceed successfully with the call.
I could call the function inside the POST, each time there is a request to guarantee that it will always exist, but the performance would be lost, especially dealing with Chatbots that must be quick.
I also tried the warmup approach, in which theoretically it would be called when deploying and changing the instance (but it could not be called, as by docs):
app.get('/_ah/warmup', (req, res) => {
credentials();
});
How could I approach this? I'm pretty new to Node.js and the server world.
Thanks
credentials(); by itself. no need to do it in express. The issue i would be race condition on the the shared credential.
crude example assuming the event loop has only these script in queue :
let say, you have 2 concurrent users A and B. A request and found the credential expire which in turn request new credential. B request before the credential return from A request, which in turn request another credential. Based on node eventloop, A then get credential_A , B will get credential B. If your third party only allow single credential then A will get an error from api call.
So the approach would be to forward the credential related task to one module, which manages the credential. background task or on request ( get token it expires on request) will face the same race problem. since node doesn't have context of thread, it is simple.
let credential = {}
let isUpdating = false;
const _updateCrediental = (newCrediential){
//map here
}
const _getCredential = async()=> {
try{
if(!updating){
updating = true;
const newCrediential = await apiCall();
updateCrediential(newCrediential);
updating = false;
return credential;
}else{
return false;
}
}catch(err){
throw err;
}
}
export.getCredential = ()=>{
if(credentialIsValid()){
return credential;
}
return __getCredential();
}
/// check the return if it promise type then waaait for it if its false then wait for certain time and check again.
An improvement to this would be using event to instead of using timeout.
I myself would prefer work with database as well as you might want to log credential generation as well. Most database promise certain kind of transaction or locking. (feel safer)
I use the Smartcar API on my Tesla (https://teslaapi.dev/) and successfully made a request before but I think the access token expired and I don't know how to refresh it.
I followed this guide: https://smartcar.com/docs/integration-guides/express/request/
It talks about the access token but it doesn't tell me how to get the refresh token.
// ./index.js
app.get('/vehicle', function(req, res) {
// TODO: Request Step 2: Get vehicle information
return smartcar.getVehicleIds(access.accessToken)
.then(function(data) {
// the list of vehicle ids
return data.vehicles;
})
.then(function(vehicleIds) {
// instantiate the first vehicle in the vehicle id list
const vehicle = new smartcar.Vehicle(vehicleIds[0], access.accessToken);
return vehicle.info();
})
.then(function(info) {
res.render('vehicle', {
info: info,
});
});
});
This doesn't work anymore:
{
"error": "authentication_error",
"message": "Invalid or expired token provided."
}
I think it's because I need to replace the accessToken with a refresh token. How can I do this?
So your hunch about needing to use the refresh token is correct.
If you look at the "Request access token" section of API Reference, it notes that the access tokens are only valid for 2 hours, after that point you'll need to use the refresh token to get a new access token to use.
If you're using the Node SDK, you can use the exchangeRefreshToken method to exchange your refresh token for a new set of tokens.
Here's an example with all of that integrated:
// ./index.js
app.get('/vehicle', function(req, res) {
if (smartcar.isExpired(acccess.expiration)) {
const
}
// TODO: Request Step 2: Get vehicle information
return smartcar.getVehicleIds(access.accessToken)
.then(function(data) {
// the list of vehicle ids
return data.vehicles;
})
.then(function(vehicleIds) {
// instantiate the first vehicle in the vehicle id list
const vehicle = new smartcar.Vehicle(vehicleIds[0], access.accessToken);
return vehicle.info();
})
.then(function(info) {
res.render('vehicle', {
info: info,
});
});
});
For a proper long term implementation of this you'll need to involve some sort of database that stores the access object based on vehicle id.
I'm new to JWT which stands for Json Web Token. I've confused with couple of its terms: Access Token and Refresh Token.
purpose: I wanna implement a user authorization which logs the user out after two hours of being idle (don't request the site or exit from the browser).
To reach that goal I'm trying to follow the below items:
After the user registers/logs-in in the site, I create Access Token and Refresh Token.
Save the refresh token in the DB or cookie.
After 15 minutes the users token the access token expired.
In case of a user being idle for 2 hours, I remove the refresh token from the cookie or DB, else I renew the access token using refresh token.
Is there any optimized way to reach that purpose?
First of all u need to understand the principle of JWT's and how they are passed between server and client and matched server-side against a secret - here's the doc
The payload can be any arbitrary user data - i.E.: just a usrname or id
Basically you need a service that generates a token on successful authentication (when the user logs in with the proper credentials, i.E.: usr & pwd) and create an additional header with the token to be used in further requests to the server.
// INFO: Function to create headers, add token, to be used in HTTP requests
createAuthenticationHeaders() {
this.loadToken(); // INFO: Get token so it can be attached to headers
// INFO: Headers configuration options
this.options = new RequestOptions({
headers: new Headers({
'Content-Type': 'application/json', // INFO: Format set to JSON
'authorization': this.authToken // INFO: Attach token
})
});
}
// INFO: Function to get token from client local storage
loadToken() {
this.authToken = localStorage.getItem('token');; // Get token and asssign to
variable to be used elsewhere
}
and some functionality to store the user-status i.E.:
// INFO: Function to store user's data in client local storage
storeUserData(token, user) {
localStorage.setItem('token', token); // INFO: Set token in local storage
localStorage.setItem('user', JSON.stringify(user)); // INFO: Set user in local
storage as string
this.authToken = token; // INFO: Assign token to be used elsewhere
this.user = user; // INFO: Set user to be used elsewhere
}
and a logout function to destroy the token in the local storage, i.E.:
// INFO: Function for logging out
logout() {
this.authToken = null; // INFO: Set token to null
this.user = null; // INFO: Set user to null
localStorage.clear(); // INFO: Clear local storage
}
In case you use npm's jsonwebtoken, you can set the ttl of the token when generating it:
const token = jwt.sign({ id: idDB }, "secret", { expiresIn: '24h' });
or whatever ttl you desire, the string "secret" refers to the secret that's matched against the server.
btw: If I understand you correctly, your points number 3 and 4 contradict each other..
After 15 minutes the users token the access token expired.
In case of a user being idle for 2 hours, I remove the refresh token from the cookie or DB, else I renew the access token using refresh token.
in case 4 it will be destroyed anyways in 15 mins if you implemented the logic of number 3 correctly
I am creating a relational blog where I make use of ember_simple_auth:session to store the session like
{"authenticated":{"authenticator":"authenticator:devise","token":"rh2f9iy7EjJXESAM5koQ","email":"user#example.com","userId":1}}
However, on the developer tools on Chrome (and possibly on other browsers), it is quite easy to edit the email and userId in order to impersonate another user upon page reload.
EDIT #1
From the conversation with Joachim and Nikolaj, I now realized that the best way to tackle this problem is to probe the localStorage authenticity every time I need it (which is only on page reload) instead of attempting to prevent edits.
In order to validate authenticity, I create a promise that must be solved before the AccountSession can be used. The promise serverValidation() requests to create a token model with the current localStorage info, and when the server gets it, it validates the info and responds 200 with a simple user serialization with type as token if the information is legit. You can check more info on the Source Code.
Session Account
import Ember from 'ember';
const { inject: { service }, RSVP } = Ember;
export default Ember.Service.extend ({
session: service('session'),
store: service(),
serverValidation: false,
// Create a Promise to handle a server request that validates the current LocalStorage
// If valid, then set SessionAccount User.
loadCurrentUser() {
if (!Ember.isEmpty(this.get('session.data.authenticated.userId'))) {
this.serverValidation().then(() => {
return new RSVP.Promise((resolve, reject) => {
const userId = this.get('session.data.authenticated.userId');
// Get User to Session-Account Block
if(this.get('serverValidation') === true) {
return this.get('store').find('user', userId).then((user) => {
this.set('user', user);
resolve();
}).catch((reason) => {
console.log(reason.errors);
var possible404 = reason.errors.filterBy('status','404');
var possible500 = reason.errors.filterBy('status','500');
if(possible404.length !== 0) {
alert('404 | Sign In Not Found Error');
this.get('session').invalidate();
}
else if(possible500.length !== 0) {
alert('500 | Sign In Server Error');
this.get('session').invalidate();
}
reject();
});
}
else{
alert('Session for Server Validation failed! Logging out!');
this.get('session').invalidate();
resolve();
}
});
});
} else {
// Session is empty...
}
},
serverValidation() {
return new RSVP.Promise((resolve) => {
var tokenAuthentication = this.get('store').createRecord('token', {
id: this.get('session.data.authenticated.userId'),
email: this.get('session.data.authenticated.email'),
authenticity_token: this.get('session.data.authenticated.token'),
});
tokenAuthentication.save().then(() => {
this.set('serverValidation',true);
console.log('Server Validation complete with 200');
resolve();
}).catch((reason) => {
this.set('serverValidation',false);
resolve();
});
});
}
});
Token Controller
# Users Controller: JSON response through Active Model Serializers
class Api::V1::TokensController < ApiController
respond_to :json
def create
if token_by_id == token_by_token
if token_by_email == token_by_id
render json: token_by_id, serializer: TokenSerializer, status: 200
else
render json: {}, status: 404
end
else
render json: {}, status: 404
end
end
private
def token_by_id
User.find(user_params[:id])
end
def token_by_email
User.find_by(email: user_params[:email])
end
def token_by_token
User.find_by(authentication_token: user_params[:authenticity_token])
end
def user_params
ActiveModelSerializers::Deserialization.jsonapi_parse!(params.to_unsafe_h)
end
end
There is no way to prevent a user from editing the content of his local storage, session storage, or cookies.
But this should not worry you. The user is identified through the value of the token. The token is generated and sent to him by the authenticator when he logs in. To impersonate another user by editing the session data he would have to know that the other user is logged in, and know the token of that user.
Token is already signed on the server side, a standard JWT mechanism.
Having said that, there can be a couple of ways to check tempering in local storage:
Generate a token the way you already do.
Generate a random secret key to be kept on the server.
Generate a corresponding HMAC using this secret key.
Send the token + HMAC to the user.
When the user sends you this token, first check if HMAC is correct, if not then reject the token right away.
If HMAC is correct, validate the token the way you already do.
Another way:
Along with the token, a HMAC checksum too can be stored separately, and when sent back to the server by the client, check if checksum matches.