Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authenticated property on Kuzzle object #390

Merged
merged 19 commits into from
May 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 23 additions & 37 deletions src/Kuzzle.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,28 +125,6 @@ class Kuzzle extends KuzzleEventEmitter {
this._autoReplay = value;
}

get jwt () {
return this._jwt;
}

set jwt (token) {
if (token === undefined || token === null) {
this._jwt = undefined;
}
else if (typeof token === 'string') {
this._jwt = token;
}
else if (typeof token === 'object'
&& token.result
&& token.result.jwt
&& typeof token.result.jwt === 'string'
) {
this._jwt = token.result.jwt;
} else {
throw new Error(`Invalid token argument: ${token}`);
}
}

get host () {
return this.protocol.host;
}
Expand Down Expand Up @@ -212,6 +190,22 @@ class Kuzzle extends KuzzleEventEmitter {
return this.protocol.sslConnection;
}

get authenticated () {
return this.auth.authenticationToken && !this.auth.authenticationToken.expired;
}

get jwt () {
if (!this.auth.authenticationToken) {
return null;
}

return this.auth.authenticationToken.encodedJwt;
}

set jwt (encodedJwt) {
this.auth.authenticationToken = encodedJwt;
}

get connected () {
return this.protocol.connected;
}
Expand Down Expand Up @@ -251,7 +245,7 @@ class Kuzzle extends KuzzleEventEmitter {
this.protocol.addListener('queryError', (err, query) => this.emit('queryError', err, query));

this.protocol.addListener('tokenExpired', () => {
this.jwt = undefined;
this.auth.authenticationToken = null;
this.emit('tokenExpired');
});

Expand Down Expand Up @@ -287,16 +281,17 @@ class Kuzzle extends KuzzleEventEmitter {
this.playQueue();
}

if (this.jwt) {
return this.auth.checkToken(this.jwt)
if (this.auth.authenticationToken) {
return this.auth.checkToken()
.then(res => {

// shouldn't obtain an error but let's invalidate the token anyway
if (!res.valid) {
this.jwt = undefined;
this.auth.authenticationToken = null;
}
})
.catch(() => {
this.jwt = undefined;
this.auth.authenticationToken = null;
})
.then(() => this.emit('reconnected'));
}
Expand Down Expand Up @@ -384,16 +379,7 @@ class Kuzzle extends KuzzleEventEmitter {
request.volatile.sdkInstanceId = this.protocol.id;
request.volatile.sdkVersion = this.sdkVersion;

/*
* Do not add the token for the checkToken route, to avoid getting a token error when
* a developer simply wish to verify his token
*/
if (this.jwt !== undefined
&& !(request.controller === 'auth'
&& (request.action === 'checkToken' || request.action === 'login'))
) {
request.jwt = this.jwt;
}
this.auth.authenticateRequest(request);

let queuable = true;
if (options && options.queuable === false) {
Expand Down
63 changes: 51 additions & 12 deletions src/controllers/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const
Jwt = require('../core/Jwt'),
BaseController = require('./base'),
User = require('./security/user');

Expand All @@ -16,6 +17,39 @@ class AuthController extends BaseController {
*/
constructor (kuzzle) {
super(kuzzle, 'auth');

this._authenticationToken = null;
}

get authenticationToken () {
return this._authenticationToken;
}

set authenticationToken (encodedJwt) {
if (encodedJwt === undefined || encodedJwt === null) {
this._authenticationToken = null;
} else if (typeof encodedJwt === 'string') {
this._authenticationToken = new Jwt(encodedJwt);
} else {
throw new Error(`Invalid token argument: ${encodedJwt}`);
}
}

/**
* Do not add the token for the checkToken route, to avoid getting a token error when
* a developer simply wish to verify his token
*
* @param {object} request
*/
authenticateRequest (request) {
if (!this.authenticationToken
|| (request.controller === 'auth'
&& (request.action === 'checkToken' || request.action === 'login'))
) {
return;
}

request.jwt = this.authenticationToken.encodedJwt;
}

/**
Expand All @@ -25,10 +59,14 @@ class AuthController extends BaseController {
* @return {Promise|*|PromiseLike<T>|Promise<T>}
*/
checkToken (token) {
if (token === undefined && this.authenticationToken) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(to be discussed) Personnally, I would throw if no token is defined or, perhaps better, I would let Kuzzle respond that it needs a token to be verified (like it was done before your changes)

And if users want to verify the stored token, they only need to use it like this: kuzzle.auth.checkToken(kuzzle.jwt)
Don't forget that the SDK automatically does an auth:checkToken on the stored jwt when a network connection is (re-)established.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was the original behaviour but most of the time users want to check if the token stored by the SDK is still valid so IMHO they will understand that if you call this method without a specific token, it will check the SDK internal token.
Also it's more simpler to get ride of the kuzzle.jwt argument.
But maybe I can throw an error if checkToken is called without a token and if there is no internal token

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token is already automatically checked by the SDK, but... why not In that case, you have to document it properly in the checkToken SDK documentation

token = this.authenticationToken.encodedJwt;
}

return this.query({
action: 'checkToken',
body: {token}
}, {queuable: false})
body: { token }
}, { queuable: false })
.then(response => response.result);
}

Expand Down Expand Up @@ -144,23 +182,24 @@ class AuthController extends BaseController {
throw new Error('Kuzzle.auth.login: strategy is required');
}

const
request = {
strategy,
expiresIn,
body: credentials,
action: 'login'
};
const request = {
strategy,
expiresIn,
body: credentials,
action: 'login'
};

return this.query(request, {queuable: false})
.then(response => {
try {
this.kuzzle.jwt = response.result.jwt;
this._authenticationToken = new Jwt(response.result.jwt);

this.kuzzle.emit('loginAttempt', {success: true});
}
catch (err) {
return Promise.reject(err);
}

return response.result.jwt;
})
.catch(err => {
Expand All @@ -179,7 +218,7 @@ class AuthController extends BaseController {
action: 'logout'
}, {queuable: false})
.then(() => {
this.kuzzle.jwt = undefined;
this._authenticationToken = null;
});
}

Expand Down Expand Up @@ -246,7 +285,7 @@ class AuthController extends BaseController {

return this.query(query, options)
.then(response => {
this.kuzzle.jwt = response.result.jwt;
this._authenticationToken = new Jwt(response.result.jwt);

return response.result;
});
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/realtime/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ class RealTimeController extends BaseController {
}

this.subscriptions = {};
this.kuzzle.jwt = undefined;
this.kuzzle.auth.authenticationToken = null;

const now = Date.now();

if ((now - this.lastExpirationTimestamp) > expirationThrottleDelay) {
this.lastExpirationTimestamp = now;
this.kuzzle.emit('tokenExpired');
Expand Down
56 changes: 56 additions & 0 deletions src/core/Jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

const decodeBase64 = base64 => {
if (Buffer) {
return Buffer.from(base64, 'base64').toString();
}

return atob(base64);
};

class Jwt {
constructor (encodedJwt) {
this._encodedJwt = encodedJwt;

this._userId = null;
this._expiresAt = null;

this._decode();
}

get encodedJwt () {
return this._encodedJwt;
}

get userId () {
return this._userId;
}

get expiresAt () {
return this._expiresAt;
}

get expired () {
return Date.now() > this.expiresAt;
}

_decode () {
const [, payloadRaw, ] = this._encodedJwt.split('.');

if (!payloadRaw) {
throw new Error('Invalid JWT format');
}

let payload;
try {
payload = JSON.parse(decodeBase64(payloadRaw));
} catch (error) {
throw new Error('Invalid JSON payload for JWT');
}

this._userId = payload._id;
this._expiresAt = payload.exp;
}
}

module.exports = Jwt;
Loading