Skip to content

Commit

Permalink
Merge pull request #57 from roundcube-next/dev-auth-update
Browse files Browse the repository at this point in the history
Update auth-related functions and models
  • Loading branch information
Abdulkader BENCHI committed Sep 15, 2016
2 parents 8a506da + 4d52631 commit e4c5877
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 69 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,13 @@ 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
- 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
### Fixed
Expand Down
5 changes: 4 additions & 1 deletion Gruntfile.js
Expand Up @@ -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'
},
Expand Down
1 change: 1 addition & 0 deletions lib/API.js
Expand Up @@ -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'),
Expand Down
27 changes: 15 additions & 12 deletions lib/client/Client.js
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -154,29 +157,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 {
Expand Down Expand Up @@ -212,7 +215,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' };
});
});
}
Expand All @@ -233,7 +236,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));
}
Expand Down Expand Up @@ -650,7 +653,7 @@ export default class Client {
return {
Accept: 'application/json; charset=UTF-8',
'Content-Type': 'application/json; charset=UTF-8',
Authorization: this.authToken
Authorization: (this.authScheme ? this.authScheme + ' ' : '') + this.authToken
};
}

Expand Down
52 changes: 49 additions & 3 deletions lib/models/AuthContinuation.js
@@ -1,6 +1,7 @@
'use strict';

import Utils from '../utils/Utils.js';
import AuthMethod from './AuthMethod';

export default class AuthContinuation {
/**
Expand All @@ -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;
}

}
20 changes: 20 additions & 0 deletions 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');

Object.assign(this, payload);
}
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down

0 comments on commit e4c5877

Please sign in to comment.