Skip to content
Snippets Groups Projects
Commit 17ab37e5 authored by Viktor Vorobev's avatar Viktor Vorobev
Browse files

Merged in NRRPLT-8942-cache-the-oidc-token (pull request #8)

[NRRPLT-8942] Verify token at /introspection endpoint and keep it in cache intill the end of it's validity

* [NRRPLT-8942] Verify token at /introspection endpoint and keep it in cache intill the end of it's validity

+ Tests
+ Cache cleanup

* Merged development into NRRPLT-8942-cache-the-oidc-token

Approved-by: Ugo Albanese
parent 43ae0c65
No related branches found
No related tags found
No related merge requests found
......@@ -30,6 +30,7 @@ require('tls').DEFAULT_ECDH_CURVE = 'auto';
const CREATE_TOKEN_URL = '/protocol/openid-connect/token';
const USERINFO_ENDPOINT = '/protocol/openid-connect/userinfo';
const INTROSPECT_TOKEN_URL = '/protocol/openid-connect/token/introspect';
let authConfig;
let lastRenewalTime = 0;
let lastRetrievedToken;
......@@ -92,8 +93,46 @@ const getToken = () => {
return deferred.promise;
};
const introspectToken = (token) => {
if (authConfig.deactivate) return q(false);
const options = {
method: 'post',
form: {
token,
client_id: authConfig.clientId,
client_secret: authConfig.clientSecret
},
url: authConfig.url + INTROSPECT_TOKEN_URL,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
const deferred = q.defer();
request(options, (err, res, body) => {
if (err) {
deferred.reject(new Error(err));
} else if (res.statusCode < 200 || res.statusCode >= 300) {
deferred.reject(
new Error('Status code: ' + res.statusCode + '\n' + body)
);
} else {
try {
const response = JSON.parse(body);
deferred.resolve(response);
} catch (e) {
deferred.reject(new Error(body));
}
}
});
return deferred.promise;
};
export default {
getToken,
introspectToken,
configure,
getAuthConfig,
getUserinfoEndpoint
......
......@@ -23,42 +23,66 @@
* ---LICENSE-END**/
'use strict';
import q from 'q';
import oidcAuthenticator from '../../proxy/oidcAuthenticator';
import BaseAuthenticator from '../BaseAuthenticator';
import { Identity } from './Identity';
const q = require('q'),
path = require('path'),
identity = new Identity();
// Clean the cache from the old tokens once an hour
const CACHE_CLEANUP_INTEVAL_MS = 1000 * 60 * 60;
export class Authenticator extends BaseAuthenticator {
static get TOKEN_CACHE_DURATION_MS() {
return 60 * 1000;
}
private authCache = new Map();
private authCache: Map<string, { time_ms: number; response: any }> = new Map();
constructor(private config) {
super();
setTimeout(this.cleanCache, CACHE_CLEANUP_INTEVAL_MS);
}
checkToken(token) {
checkToken(token: string) {
if (this.config.storage === 'Collab') {
// No need to check token, it will be done by the underlying Collab storage requests
return q.when(true);
}
// do we have the token in cache?
// Do we have the token in cache?
if (this.authCache.has(token)) {
const cache = this.authCache.get(token);
if (Date.now() - cache.time <= Authenticator.TOKEN_CACHE_DURATION_MS) {
// cache still time valid
return q.when(cache.userinfo);
} else this.authCache.delete(token);
if (cache) {
if (Date.now() < cache.time_ms) {
// Cache still valid
return q.when(cache.response);
} else {
console.debug('Token in cache is expired');
this.authCache.delete(token);
}
}
}
// no valid cache, we verify the token by trying to retrieve the user info
return identity.getUserInfo('me', token).then(userinfo => {
this.authCache.set(token, { time: Date.now(), userinfo });
return userinfo;
// No valid cache, verify the token by trying to retrieve the user info
return oidcAuthenticator.introspectToken(token).then(response => {
const deferred = q.defer();
// Check if the token is active
if (response.active) {
// Set token cache life time based on token expiration date
this.authCache.set(token, { time_ms: response.exp * 1000, response });
deferred.resolve(response);
} else {
deferred.reject(new Error('Token is not active.'));
}
return deferred.promise;
});
}
/**
* Removes expired entries from the authCache based on the current time.
*/
cleanCache() {
const currentTimeMS = Date.now();
this.authCache.forEach((cache, key) => {
if (currentTimeMS >= cache.time_ms) {
this.authCache.delete(key);
}
});
}
......
......@@ -62,12 +62,17 @@ describe('CollabAuthenticator', () => {
Authenticator: CollabAuthenticator
} = require('../../storage/Collab/Authenticator'),
nock = require('nock');
const INTROSPECT_TOKEN_URL = '/protocol/openid-connect/token/introspect';
it('should get user info when trying to authenticate with non collab storage', () => {
const response = { id: 'some_id' };
it('should return the introspection endpoint response when token is active', () => {
const response = { active: true, other: 'someData' };
let collabAuth = new CollabAuthenticator({ storage: 'FS' });
nock('https://localhost')
.get('/protocol/openid-connect/userinfo')
nock('http://localhost')
.post(
INTROSPECT_TOKEN_URL,
'token=emptyToken&client_id=CLIENT_ID&client_secret=CLIENT_SECRET'
)
.matchHeader('content-type', 'application/x-www-form-urlencoded')
.reply(200, response);
return collabAuth
......@@ -75,6 +80,52 @@ describe('CollabAuthenticator', () => {
.should.eventually.deep.equal(response);
});
it('should retrun proper error, when token is not active', () => {
const response = { active: false, other: 'someData' };
let collabAuth = new CollabAuthenticator({ storage: 'FS' });
nock('http://localhost')
.post(
INTROSPECT_TOKEN_URL,
'token=emptyToken&client_id=CLIENT_ID&client_secret=CLIENT_SECRET'
)
.matchHeader('content-type', 'application/x-www-form-urlencoded')
.reply(200, response);
return collabAuth
.checkToken('emptyToken')
.should.be.rejectedWith('Token is not active.');
});
it('should retrun proper error, when introspection response is not successful', () => {
const response = { random: 'json', other: 'someData' };
let collabAuth = new CollabAuthenticator({ storage: 'FS' });
nock('http://localhost')
.post(
INTROSPECT_TOKEN_URL,
'token=emptyToken&client_id=CLIENT_ID&client_secret=CLIENT_SECRET'
)
.matchHeader('content-type', 'application/x-www-form-urlencoded')
.reply(403, response);
return collabAuth
.checkToken('emptyToken')
.should.be.rejectedWith(/Status code: 403/);
});
it('should retrun proper error, when introspection response body is corrupted', () => {
const response = 'Corrupted response body';
let collabAuth = new CollabAuthenticator({ storage: 'FS' });
nock('http://localhost')
.post(
INTROSPECT_TOKEN_URL,
'token=emptyToken&client_id=CLIENT_ID&client_secret=CLIENT_SECRET'
)
.matchHeader('content-type', 'application/x-www-form-urlencoded')
.reply(403, response);
return collabAuth.checkToken('emptyToken').should.be.rejectedWith(response);
});
it('should resolve to true when trying to authenticate with collab storage', () => {
let collabAuth = new CollabAuthenticator({ storage: 'Collab' });
return collabAuth.checkToken('emptyToken').should.eventually.equal(true);
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment