From 513a77bab0ee47990775db50a916dd0cbf3e02b7 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sun, 4 Sep 2016 10:12:55 +0200 Subject: [PATCH 1/4] Adapt data strcutures and models used during authentication after changes to the JMAP spec The authentication procedure defined in the JMAP spec changed recently. Issue #44. The 200 response requesting more authorization data has been changed as follows: - The `methods` property is now an array of `AuthMethod` objects - The `continuationToken` property was renamed to `loginId` --- CHANGELOG.md | 6 ++ lib/API.js | 1 + lib/client/Client.js | 20 ++--- lib/models/AuthContinuation.js | 52 +++++++++++- lib/models/AuthMethod.js | 20 +++++ test/common/Client.js | 94 ++++++++++----------- test/common/models/AuthContinuation.js | 110 +++++++++++++++++++++++-- test/common/models/AuthMethod.js | 37 +++++++++ 8 files changed, 274 insertions(+), 66 deletions(-) create mode 100644 lib/models/AuthMethod.js create mode 100644 test/common/models/AuthMethod.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d9710..03bcb2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [master] +### Added +- The AuthMethod model + +### Changed +- Data strcutures used during authentication procedure after changes to the JMAP spec +- The AuthContinuation model ## [0.0.20] - 2016-08-22 ### Fixed diff --git a/lib/API.js b/lib/API.js index 88363c4..ad49874 100644 --- a/lib/API.js +++ b/lib/API.js @@ -65,6 +65,7 @@ export default { SetResponse: require('./models/SetResponse'), AuthAccess: require('./models/AuthAccess'), AuthContinuation: require('./models/AuthContinuation'), + AuthMethod: require('./models/AuthMethod'), Constants: require('./utils/Constants'), Attachment: require('./models/Attachment'), AccountCapabilities: require('./models/AccountCapabilities'), diff --git a/lib/client/Client.js b/lib/client/Client.js index c3d0aed..1201449 100644 --- a/lib/client/Client.js +++ b/lib/client/Client.js @@ -154,29 +154,29 @@ export default class Client { * @return {Promise} A {@link Promise} that will eventually be resovled with a {@link AuthAccess} object */ _authenticateResponse(data, continuationCallback) { - if (data.continuationToken && data.accessToken === undefined) { + if (data.loginId && data.accessToken === undefined) { // got an AuthContinuation response var authContinuation = new AuthContinuation(data); return continuationCallback(authContinuation) .then(continueData => { - if (authContinuation.methods.indexOf(continueData.method) < 0) { - throw new Error('The "' + continueData.method + '" authentication protocol is not supported by the server.'); + if (!authContinuation.supports(continueData.type)) { + throw new Error('The "' + continueData.type + '" authentication type is not supported by the server.'); } let param = { - token: authContinuation.continuationToken, - method: continueData.method + loginId: authContinuation.loginId, + type: continueData.type }; - if (continueData.password) { - param.password = continueData.password; + if (continueData.value) { + param.value = continueData.value; } return this.transport .post(this.authenticationUrl, this._defaultNonAuthenticatedHeaders(), param); }) .then(resp => this._authenticateResponse(resp, continuationCallback)); - } else if (data.accessToken && data.continuationToken === undefined) { + } else if (data.accessToken && data.loginId === undefined) { // got auth access response return new AuthAccess(data); } else { @@ -212,7 +212,7 @@ export default class Client { return this.authenticate(username, deviceName, function(authContinuation) { // wrap the continuationCallback to resolve with method:'external' return continuationCallback(authContinuation).then(() => { - return { method: 'external' }; + return { type: 'external' }; }); }); } @@ -233,7 +233,7 @@ export default class Client { authPassword(username, password, deviceName) { return this.authenticate(username, deviceName, function() { return this.promiseProvider.newPromise(function(resolve, reject) { - resolve({ method: 'password', password: password }); + resolve({ type: 'password', value: password }); }); }.bind(this)); } diff --git a/lib/models/AuthContinuation.js b/lib/models/AuthContinuation.js index 00dc9a5..0cc4720 100644 --- a/lib/models/AuthContinuation.js +++ b/lib/models/AuthContinuation.js @@ -1,6 +1,7 @@ 'use strict'; import Utils from '../utils/Utils.js'; +import AuthMethod from './AuthMethod'; export default class AuthContinuation { /** @@ -12,11 +13,56 @@ export default class AuthContinuation { */ constructor(payload) { Utils.assertRequiredParameterIsPresent(payload, 'payload'); - Utils.assertRequiredParameterIsPresent(payload.continuationToken, 'continuationToken'); + Utils.assertRequiredParameterIsPresent(payload.loginId, 'loginId'); Utils.assertRequiredParameterIsArrayWithMinimumLength(payload.methods, 'methods'); - this.continuationToken = payload.continuationToken; - this.methods = payload.methods; + this.loginId = payload.loginId; + this.methods = payload.methods.map((method) => new AuthMethod(method)); this.prompt = payload.prompt || null; } + + /** + * Getter for the AuthMethod instance matching the given authentication type + * + * @param type {String} The authentication type + * @return {AuthMethod} + */ + getMethod(type) { + Utils.assertRequiredParameterHasType(type, 'type', 'string'); + + let result = null; + + this.methods.forEach((authMethod) => { + if (authMethod.type === type) { + result = authMethod; + } + }); + + if (!result) { + throw new Error('No AuthMethod of type "' + type + '" found'); + } + + return result; + } + + /** + * Checks if the given authentication type is supported by one of the registred auth methods + * + * @param type {String} The authentication type to check + * @return {Boolean} True if supported, False otherwise + */ + supports(type) { + Utils.assertRequiredParameterHasType(type, 'type', 'string'); + + let result = false; + + try { + this.getMethod(type); + result = true; + } catch (e) { + } + + return result; + } + } diff --git a/lib/models/AuthMethod.js b/lib/models/AuthMethod.js new file mode 100644 index 0000000..3d62b4d --- /dev/null +++ b/lib/models/AuthMethod.js @@ -0,0 +1,20 @@ +'use strict'; + +import Utils from '../utils/Utils.js'; + +export default class AuthMethod { + /** + * This class represents a JMAP [AuthMethod]{@link http://jmap.io/spec.html#getting-an-access-token}. + * + * @constructor + * + * @param payload {Object} The server response of POST request to the authentication URL. + */ + + constructor(payload) { + Utils.assertRequiredParameterIsPresent(payload, 'payload'); + Utils.assertRequiredParameterHasType(payload.type, 'type', 'string'); + + this.type = payload.type; + } +} diff --git a/test/common/Client.js b/test/common/Client.js index 0d73d32..2a8b5a4 100644 --- a/test/common/Client.js +++ b/test/common/Client.js @@ -1684,15 +1684,15 @@ describe('The Client class', function() { post: function(url, headers, data) { return q({ - continuationToken: 'continuationToken1', - methods: ['password', 'external'] + loginId: 'loginId1', + methods: [{type:'password'}, {type:'external'}] }); } }) .withAuthenticationUrl('https://test') .authenticate('user@domain.com', 'Device name', function(authContinuation) { - expect(authContinuation.continuationToken).to.equal('continuationToken1'); - expect(authContinuation.methods).to.deep.equal(['password', 'external']); + expect(authContinuation.loginId).to.equal('loginId1'); + expect(authContinuation.methods.length).to.deep.equal(2); done(); return q.reject(); @@ -1708,13 +1708,13 @@ describe('The Client class', function() { calls++; return q({ - continuationToken: 'continuationToken1', - methods: ['password', 'external'] + loginId: 'loginId1', + methods: [{type:'password'}, {type:'external'}] }); } else { - expect(data.token).to.equal('continuationToken1'); - expect(data.method).to.equal('password'); - expect(data.password).to.equal('password1'); + expect(data.loginId).to.equal('loginId1'); + expect(data.type).to.equal('password'); + expect(data.value).to.equal('password1'); done(); return q(); @@ -1724,8 +1724,8 @@ describe('The Client class', function() { .withAuthenticationUrl('https://test') .authenticate('user@domain.com', 'Device name', function(authContinuation) { return q({ - method: 'password', - password: 'password1' + type: 'password', + value: 'password1' }); }); }); @@ -1734,14 +1734,14 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers, data) { return q({ - continuationToken: 'continuationToken1', - methods: ['password'] + loginId: 'loginId1', + methods: [{type:'password'}] }); } }) .withAuthenticationUrl('https://test') .authenticate('user@domain.com', 'Device name', function(authContinuation) { - return q({ method: 'external' }); + return q({type: 'external'}); }) .then(null, function(err) { expect(err).to.be.instanceOf(Error); @@ -1765,8 +1765,8 @@ describe('The Client class', function() { post: function(url, headers, data) { if (data.username) { return q({ - continuationToken: 'continuationToken1', - methods: ['password', 'external'] + loginId: 'loginId1', + methods: [{type:'password'}, {type:'external'}] }); } else { return q(authAccessResponse); @@ -1775,7 +1775,7 @@ describe('The Client class', function() { }) .withAuthenticationUrl('https://test') .authenticate('user@domain.com', 'Device name', function(authContinuation) { - return q({ method: 'external' }); + return q({type: 'external'}); }) .then(function(authAccess) { expect(authAccess).to.deep.equal(authAccessResponse); @@ -1799,20 +1799,20 @@ describe('The Client class', function() { post: function(url, headers, data) { if (data.username) { return q({ - continuationToken: 'continuationToken1', - methods: ['password'] + loginId: 'loginId1', + methods: [{type:'password'}] }); - } else if (data.password === 'pwd1') { + } else if (data.value === 'pwd1') { return q({ - continuationToken: 'continuationToken2', + loginId: 'loginId2', prompt: '2nd Auth Factor', - methods: ['password'] + methods: [{type:'totp'}] }); - } else if (data.password === 'pwd2') { + } else if (data.value === 'pwd2') { return q({ - continuationToken: 'continuationToken3', + loginId: 'loginId3', prompt: '3rd Auth Factor', - methods: ['password'] + methods: [{type:'yubikeyotp'}] }); } else { return q(authAccessResponse); @@ -1821,12 +1821,12 @@ describe('The Client class', function() { }) .withAuthenticationUrl('https://test') .authenticate('user@domain.com', 'Device name', function(authContinuation) { - if (authContinuation.continuationToken === 'continuationToken1') { - return q({ method: 'password', password: 'pwd1' }); - } else if (authContinuation.continuationToken === 'continuationToken2') { - return q({ method: 'password', password: 'pwd2' }); + if (authContinuation.loginId === 'loginId1') { + return q({type: 'password', value: 'pwd1'}); + } else if (authContinuation.loginId === 'loginId2') { + return q({type: 'totp', value: 'pwd2'}); } else { - return q({ method: 'password', password: 'pwd3' }); + return q({type: 'yubikeyotp', value: 'pwd3'}); } }) .then(function(authAccess) { @@ -1840,13 +1840,13 @@ describe('The Client class', function() { post: function(url, headers, data) { return q({ accessToken: 'accessToken1', - continuationToken: 'continuationToken1' + loginId: 'loginId1' }); } }) .withAuthenticationUrl('https://test') .authenticate('user@domain.com', 'Device name', function(authContinuation) { - return q({ method: 'external' }); + return q({type: 'external'}); }) .then(null, function(err) { expect(err).to.be.instanceOf(Error); @@ -1859,8 +1859,8 @@ describe('The Client class', function() { post: function(url, headers, data) { if (data.username) { return q({ - continuationToken: 'continuationToken1', - methods: ['external'] + loginId: 'loginId1', + methods: [{type:'external'}] }); } else { return q({ @@ -1886,8 +1886,8 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers, data) { return q({ - continuationToken: 'continuationToken1', - methods: ['password'] + loginId: 'loginId1', + methods: [{type:'password'}] }); } }) @@ -1903,15 +1903,15 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers, data) { return q({ - continuationToken: 'continuationToken1', - methods: ['password', 'external'] + loginId: 'loginId1', + methods: [{type:'password'}, {type:'external'}] }); } }) .withAuthenticationUrl('https://test') .authExternal('user@domain.com', 'Device name', function(authContinuation) { - expect(authContinuation.continuationToken).to.equal('continuationToken1'); - expect(authContinuation.methods).to.deep.equal(['password', 'external']); + expect(authContinuation.loginId).to.equal('loginId1'); + expect(authContinuation.methods.length).to.equal(2); done(); return q.reject(); @@ -1934,8 +1934,8 @@ describe('The Client class', function() { post: function(url, headers, data) { if (data.username) { return q({ - continuationToken: 'continuationToken1', - methods: ['password', 'external'] + loginId: 'loginId1', + methods: [{type:'password'}, {type:'external'}] }); } else { return q(authAccessResponse); @@ -1944,7 +1944,7 @@ describe('The Client class', function() { }) .withAuthenticationUrl('https://test') .authExternal('user@domain.com', 'Device name', function(authContinuation) { - return q(authContinuation.continuationToken); + return q(authContinuation.loginId); }) .then(function(authAccess) { expect(authAccess).to.deep.equal(authAccessResponse); @@ -1961,8 +1961,8 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers, data) { return q({ - continuationToken: 'continuationToken1', - methods: ['external'] + loginId: 'loginId1', + methods: [{type:'external'}] }); } }, promiseProvider) @@ -1990,8 +1990,8 @@ describe('The Client class', function() { post: function(url, headers, data) { if (data.username) { return q({ - continuationToken: 'continuationToken1', - methods: ['password', 'external'] + loginId: 'loginId1', + methods: [{type:'password'}, {type:'external'}] }); } else { return q(authAccessResponse); diff --git a/test/common/models/AuthContinuation.js b/test/common/models/AuthContinuation.js index 566da18..46d5970 100644 --- a/test/common/models/AuthContinuation.js +++ b/test/common/models/AuthContinuation.js @@ -11,7 +11,7 @@ describe('The AuthContinuation class', function() { }).to.throw(Error); }); - it('should throw if payload.continuationToken parameter is not defined', function() { + it('should throw if payload.loginId parameter is not defined', function() { expect(function() { var payload = {methods: []}; @@ -21,7 +21,7 @@ describe('The AuthContinuation class', function() { it('should throw if payload.methods parameter is not defined', function() { expect(function() { - var payload = {continuationToken: 'continuationToken1'}; + var payload = {loginId: 'loginId1'}; new jmap.AuthContinuation(payload); }).to.throw(Error); @@ -29,19 +29,117 @@ describe('The AuthContinuation class', function() { it('should throw if payload.methods parameter is not an array', function() { expect(function() { - var payload = {continuationToken: 'continuationToken1', methods: 'password'}; + var payload = {loginId: 'loginId1', methods: 'password'}; new jmap.AuthContinuation(payload); }).to.throw(Error); }); - it('should expose continuationToken and methods properties', function() { - var payload = {continuationToken: 'continuationToken1', methods: ['password', 'external']}; + it('should throw if payload.methods parameter is not an array of objects', function() { + expect(function() { + var payload = {loginId: 'loginId1', methods: ['oauth','password']}; + + new jmap.AuthContinuation(payload); + }).to.throw(Error); + }); + + it('should expose loginId and methods properties', function() { + var payload = { + loginId: 'loginId1', + methods: [ + {type: 'password'}, + {type: 'external'} + ] + }; var authContinuation = new jmap.AuthContinuation(payload); - expect(authContinuation.continuationToken).to.equal(payload.continuationToken); + expect(authContinuation.loginId).to.equal(payload.loginId); expect(authContinuation.methods).to.deep.equal(payload.methods); }); }); + + describe('The getMethod method', function() { + it('should throw if invalid type parameter is provided', function() { + expect(function() { + var payload = { + loginId: 'loginId1', + methods: [ + {type: 'password'} + ] + }; + var authContinuation = new jmap.AuthContinuation(payload); + + authContinuation.getMethod(null); + }).to.throw(Error); + }); + + it('should throw if the given type is not supported', function() { + expect(function() { + var payload = { + loginId: 'loginId1', + methods: [ + {type: 'password'} + ] + }; + var authContinuation = new jmap.AuthContinuation(payload); + + authContinuation.getMethod('oauth'); + }).to.throw(Error); + }); + + it('should return an AuthMethod instance', function() { + var payload = { + loginId: 'loginId1', + methods: [ + {type: 'external'}, + {type: 'password'} + ] + }; + var authContinuation = new jmap.AuthContinuation(payload); + + expect(authContinuation.getMethod('password')).to.be.an.instanceof(jmap.AuthMethod); + }); + }); + + describe('The supports method', function() { + it('should throw if invalid type parameter is provided', function() { + expect(function() { + var payload = { + loginId: 'loginId1', + methods: [ + {type: 'password'} + ] + }; + var authContinuation = new jmap.AuthContinuation(payload); + + authContinuation.supports(null); + }).to.throw(Error); + }); + + it('should return true if type is supported', function() { + var payload = { + loginId: 'loginId1', + methods: [ + {type: 'totp'}, + {type: 'password'} + ] + }; + var authContinuation = new jmap.AuthContinuation(payload); + + expect(authContinuation.supports('password')).to.be.true; + }); + + it('should return false if type is not supported', function() { + var payload = { + loginId: 'loginId1', + methods: [ + {type: 'password'} + ] + }; + var authContinuation = new jmap.AuthContinuation(payload); + + expect(authContinuation.supports('oauth')).to.be.false; + }); + }); }); diff --git a/test/common/models/AuthMethod.js b/test/common/models/AuthMethod.js new file mode 100644 index 0000000..e81a524 --- /dev/null +++ b/test/common/models/AuthMethod.js @@ -0,0 +1,37 @@ +'use strict'; + +var expect = require('chai').expect, + jmap = require('../../../dist/jmap-client'); + +describe('The AuthMethod class', function() { + describe('constructor', function() { + it('should throw if payload parameter is not defined', function() { + expect(function() { + new jmap.AuthMethod(); + }).to.throw(Error); + }); + + it('should throw if payload.type parameter is not defined', function() { + expect(function() { + var payload = {}; + + new jmap.AuthMethod(payload); + }).to.throw(Error); + }); + + it('should throw if payload.type parameter is not a string', function() { + expect(function() { + var payload = {type: []}; + + new jmap.AuthMethod(payload); + }).to.throw(Error); + }); + + it('should expose the type property', function() { + var payload = {type: 'password'}; + var authMethod = new jmap.AuthMethod(payload); + + expect(authMethod.type).to.equal(payload.type); + }); + }); +}); From 0de7857c5d568cef923f478a992f337fb3f3910d Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sun, 4 Sep 2016 16:34:51 +0200 Subject: [PATCH 2/4] Use the X-JMAP authentication scheme to construct the Authorization headers This makes the Authorization headers sent by the JMAP client compliant with RFC 7235 and the most recent version of the JMAP protocol specification. Plus added reference to issue #44 in Changelog entries. --- CHANGELOG.md | 3 ++- lib/client/Client.js | 2 +- test/common/Client.js | 14 +++++++------- test/common/models/Attachment.js | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bcb2f..6b1f331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - The AuthMethod model ### Changed -- Data strcutures used during authentication procedure after changes to the JMAP spec +- Use the X-JMAP authentication scheme to construct the Authorization header. #44 +- Data structures used during authentication procedure after changes to the JMAP spec. #44 - The AuthContinuation model ## [0.0.20] - 2016-08-22 diff --git a/lib/client/Client.js b/lib/client/Client.js index 1201449..feb6e48 100644 --- a/lib/client/Client.js +++ b/lib/client/Client.js @@ -650,7 +650,7 @@ export default class Client { return { Accept: 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8', - Authorization: this.authToken + Authorization: 'X-JMAP ' + this.authToken }; } diff --git a/test/common/Client.js b/test/common/Client.js index 2a8b5a4..0b28f5a 100644 --- a/test/common/Client.js +++ b/test/common/Client.js @@ -121,7 +121,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'token', + Authorization: 'X-JMAP token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -218,7 +218,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'token', + Authorization: 'X-JMAP token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -315,7 +315,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'token', + Authorization: 'X-JMAP token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -747,7 +747,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'token', + Authorization: 'X-JMAP token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -978,7 +978,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'token', + Authorization: 'X-JMAP token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -1127,7 +1127,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'token', + Authorization: 'X-JMAP token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -1278,7 +1278,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'token', + Authorization: 'X-JMAP token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); diff --git a/test/common/models/Attachment.js b/test/common/models/Attachment.js index aa9ff00..fb81cd1 100644 --- a/test/common/models/Attachment.js +++ b/test/common/models/Attachment.js @@ -218,7 +218,7 @@ describe('The Attachment class', function() { it('should send an authenticated POST request to the downloadUrl, and reject on failure', function(done) { new jmap.Attachment(newClient(function(url, headers, data, raw) { expect(url).to.equal('downloadUrl/id1'); - expect(headers.Authorization).to.equal('token'); + expect(headers.Authorization).to.equal('X-JMAP token'); expect(data).to.equal(null); expect(raw).to.equal(true); From 614d90c875ada035af6add26f0e0cdc508b49bfa Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 14 Sep 2016 22:07:49 +0200 Subject: [PATCH 3/4] Send Authroization header with X-JMAP scheme only when authenticated via the JMAP auth protocol The `X-JMAP` auth scheme is implicitly added when using `Client.withAuthAccess()` while the scheme can be specified as additional argument with `Client.withAuthenticationToken()`. If no scheme was specified, the Authorization header only contains the provided token value. This is an addition to the changes requsted in #44. --- lib/client/Client.js | 7 ++-- test/common/Client.js | 59 ++++++++++++++++++++++++++++---- test/common/models/Attachment.js | 2 +- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/lib/client/Client.js b/lib/client/Client.js index feb6e48..6591d97 100644 --- a/lib/client/Client.js +++ b/lib/client/Client.js @@ -56,11 +56,13 @@ export default class Client { * The token will be exposed as the `authToken` property afterwards. * * @param token {String} The authentication token to use in JMAP requests. + * @param [scheme] {String} The authentication scheme according to RFC 7235 * * @return {Client} This Client instance. */ - withAuthenticationToken(token) { + withAuthenticationToken(token, scheme) { this.authToken = token; + this.authScheme = scheme; return this; } @@ -107,6 +109,7 @@ export default class Client { withAuthAccess(access) { Utils.assertRequiredParameterHasType(access, 'access', AuthAccess); + this.authScheme = 'X-JMAP'; this.authToken = access.accessToken; ['username', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'versions', 'extensions'].forEach((property) => { this[property] = access[property]; @@ -650,7 +653,7 @@ export default class Client { return { Accept: 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8', - Authorization: 'X-JMAP ' + this.authToken + Authorization: (this.authScheme ? this.authScheme + ' ' : '') + this.authToken }; } diff --git a/test/common/Client.js b/test/common/Client.js index 0b28f5a..c3a7684 100644 --- a/test/common/Client.js +++ b/test/common/Client.js @@ -94,6 +94,7 @@ describe('The Client class', function() { var client = defaultClient().withAuthAccess(authAccess); expect(client.authToken).to.equal(authAccess.accessToken); + expect(client.authScheme).to.equal('X-JMAP'); ['username', 'versions', 'extensions', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl'].forEach(function(property) { expect(client[property]).to.equal(authAccess[property]); }); @@ -121,7 +122,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'X-JMAP token', + Authorization: 'token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -135,6 +136,50 @@ describe('The Client class', function() { .then(null, done); }); + it('should send correct HTTP headers with Authorization scheme', function(done) { + new jmap.Client({ + post: function(url, headers) { + expect(headers).to.deep.equal({ + Authorization: 'Bearer token', + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json; charset=UTF-8' + }); + + return q.reject(); + } + }) + .withAPIUrl('https://test') + .withAuthenticationToken('token', 'Bearer') + .getAccounts() + .then(null, done); + }); + + it('should send correct HTTP Authorization header after JMAP authentication', function(done) { + var authAccess = new jmap.AuthAccess({ + username: 'user', + accessToken: 'accessToken1', + apiUrl: 'https://test', + eventSourceUrl: '/es', + uploadUrl: '/upload', + downloadUrl: '/download' + }); + + new jmap.Client({ + post: function(url, headers) { + expect(headers).to.deep.equal({ + Authorization: 'X-JMAP accessToken1', + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json; charset=UTF-8' + }); + + return q.reject(); + } + }) + .withAuthAccess(authAccess) + .getAccounts() + .then(null, done); + }); + it('should send a valid JMAP "getAccounts" request body when there is no options', function(done) { new jmap.Client({ post: function(url, headers, body) { @@ -218,7 +263,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'X-JMAP token', + Authorization: 'token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -315,7 +360,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'X-JMAP token', + Authorization: 'token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -747,7 +792,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'X-JMAP token', + Authorization: 'token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -978,7 +1023,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'X-JMAP token', + Authorization: 'token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -1127,7 +1172,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'X-JMAP token', + Authorization: 'token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); @@ -1278,7 +1323,7 @@ describe('The Client class', function() { new jmap.Client({ post: function(url, headers) { expect(headers).to.deep.equal({ - Authorization: 'X-JMAP token', + Authorization: 'token', 'Content-Type': 'application/json; charset=UTF-8', Accept: 'application/json; charset=UTF-8' }); diff --git a/test/common/models/Attachment.js b/test/common/models/Attachment.js index fb81cd1..aa9ff00 100644 --- a/test/common/models/Attachment.js +++ b/test/common/models/Attachment.js @@ -218,7 +218,7 @@ describe('The Attachment class', function() { it('should send an authenticated POST request to the downloadUrl, and reject on failure', function(done) { new jmap.Attachment(newClient(function(url, headers, data, raw) { expect(url).to.equal('downloadUrl/id1'); - expect(headers.Authorization).to.equal('X-JMAP token'); + expect(headers.Authorization).to.equal('token'); expect(data).to.equal(null); expect(raw).to.equal(true); From 4d526315348e6f804986883360f316c59784d849 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 14 Sep 2016 22:35:55 +0200 Subject: [PATCH 4/4] Store all payload properties in AuthMethod model Use Object.assign() to copy all properties from the server response to the model instance. babel-plugin-object-assign will make sure this is properly translates to ES5. --- Gruntfile.js | 5 ++++- lib/models/AuthMethod.js | 2 +- package.json | 1 + test/common/models/AuthMethod.js | 11 +++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 0c8e9e1..57f6ed2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -135,7 +135,10 @@ module.exports = function(grunt) { browserify: { dist: { options: { - transform: ['browserify-versionify', 'babelify'], + transform: [ + 'browserify-versionify', + ['babelify', {plugins: ['object-assign']}] + ], browserifyOptions: { standalone: 'jmap' }, diff --git a/lib/models/AuthMethod.js b/lib/models/AuthMethod.js index 3d62b4d..052fe7c 100644 --- a/lib/models/AuthMethod.js +++ b/lib/models/AuthMethod.js @@ -15,6 +15,6 @@ export default class AuthMethod { Utils.assertRequiredParameterIsPresent(payload, 'payload'); Utils.assertRequiredParameterHasType(payload.type, 'type', 'string'); - this.type = payload.type; + Object.assign(this, payload); } } diff --git a/package.json b/package.json index ce469fc..d39f21c 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "main": "lib/jmap-client.js", "devDependencies": { "babelify": "6.3.0", + "babel-plugin-object-assign": "1.2.1", "browserify-versionify": "1.0.6", "chai": "3.0.0", "chai-datetime": "1.4.0", diff --git a/test/common/models/AuthMethod.js b/test/common/models/AuthMethod.js index e81a524..8fed007 100644 --- a/test/common/models/AuthMethod.js +++ b/test/common/models/AuthMethod.js @@ -33,5 +33,16 @@ describe('The AuthMethod class', function() { expect(authMethod.type).to.equal(payload.type); }); + + it('should expose all properties', function() { + var payload = { + type: 'u2f', + appId: 'app-id', + signChallenge: 'xyz' + }; + var authMethod = new jmap.AuthMethod(payload); + + expect(authMethod).to.deep.equal(payload); + }); }); });