You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
Utility to help with interactive auth
This commit is contained in:
226
lib/interactive-auth.js
Normal file
226
lib/interactive-auth.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/** @module interactive-auth */
|
||||||
|
var q = require("q");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstracts the logic used to drive the interactive auth process.
|
||||||
|
*
|
||||||
|
* <p>Components implementing an interactive auth flow should instantiate one of
|
||||||
|
* these, passing in the necessary callbacks to the constructor. They should
|
||||||
|
* then call attemptAuth, which will return a promise which will resolve or
|
||||||
|
* reject when the interactive-auth process completes.
|
||||||
|
*
|
||||||
|
* <p>Meanwhile, calls will be made to the startAuthStage and doRequest
|
||||||
|
* callbacks, and information gathered from the user can be submitted with
|
||||||
|
* submitAuthDict.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @alias module:interactive-auth
|
||||||
|
*
|
||||||
|
* @param {object} opts options object
|
||||||
|
*
|
||||||
|
* @param {object?} opts.authData error response from the last request. If
|
||||||
|
* null, a request will be made with no auth before starting.
|
||||||
|
*
|
||||||
|
* @param {function(object?): module:client.Promise} opts.doRequest
|
||||||
|
* called with the new auth dict to submit the request. Should return a
|
||||||
|
* promise which resolves to the successful response or rejects with a
|
||||||
|
* MatrixError.
|
||||||
|
*
|
||||||
|
* @param {function(string, object?)} opts.startAuthStage
|
||||||
|
* called to ask the UI to start a particular auth stage. The arguments
|
||||||
|
* are: the login type (eg m.login.password); and (if the last request
|
||||||
|
* returned an error), an error object, with fields 'errcode' and 'error'.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function InteractiveAuth(opts) {
|
||||||
|
this._data = opts.authData;
|
||||||
|
this._requestCallback = opts.doRequest;
|
||||||
|
this._startAuthStageCallback = opts.startAuthStage;
|
||||||
|
this._completionDeferred = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InteractiveAuth.prototype = {
|
||||||
|
/**
|
||||||
|
* begin the authentication process.
|
||||||
|
*
|
||||||
|
* @return {module:client.Promise} which resolves to the response on success,
|
||||||
|
* or rejects with the error on failure.
|
||||||
|
*/
|
||||||
|
attemptAuth: function() {
|
||||||
|
this._completionDeferred = q.defer();
|
||||||
|
|
||||||
|
if (!this._data) {
|
||||||
|
this._doRequest(null);
|
||||||
|
} else {
|
||||||
|
this._startNextAuthStage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._completionDeferred.promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the auth session ID
|
||||||
|
*
|
||||||
|
* @return {string} session id
|
||||||
|
*/
|
||||||
|
getSessionId: function() {
|
||||||
|
return this._data ? this._data.session : undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the server params for a given stage
|
||||||
|
*
|
||||||
|
* @param {string} login type for the stage
|
||||||
|
* @return {object?} any parameters from the server for this stage
|
||||||
|
*/
|
||||||
|
getStageParams: function(loginType) {
|
||||||
|
var params = {};
|
||||||
|
if (this._data && this._data.params) {
|
||||||
|
params = this._data.params;
|
||||||
|
}
|
||||||
|
return params[loginType];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* submit a new auth dict and fire off the request. This will either
|
||||||
|
* make attemptAuth resolve/reject, or cause the startAuthStage callback
|
||||||
|
* to be called for a new stage.
|
||||||
|
*
|
||||||
|
* @param {object} authData new auth dict to send to the server. Should
|
||||||
|
* include a `type` propterty denoting the login type, as well as any
|
||||||
|
* other params for that stage.
|
||||||
|
*/
|
||||||
|
submitAuthDict: function(authData) {
|
||||||
|
if (!this._completionDeferred) {
|
||||||
|
throw new Error("submitAuthDict() called before attemptAuth()");
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the sessionid from the last request.
|
||||||
|
var auth = {
|
||||||
|
session: this._data.session,
|
||||||
|
};
|
||||||
|
Object.assign(auth, authData);
|
||||||
|
|
||||||
|
this._doRequest(auth);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire off a request, and either resolve the promise, or call
|
||||||
|
* startAuthStage.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {object?} auth new auth dict, including session id
|
||||||
|
*/
|
||||||
|
_doRequest: function(auth) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// hackery to make sure that synchronous exceptions end up in the catch
|
||||||
|
// handler (without the additional event loop entailed by q.fcall or an
|
||||||
|
// extra q().then)
|
||||||
|
var prom;
|
||||||
|
try {
|
||||||
|
prom = this._requestCallback(auth);
|
||||||
|
} catch (e) {
|
||||||
|
prom = q.reject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
prom.then(
|
||||||
|
function(result) {
|
||||||
|
console.log("result from request: ", result);
|
||||||
|
self._completionDeferred.resolve(result);
|
||||||
|
}, function(error) {
|
||||||
|
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||||
|
// doesn't look like an interactive-auth failure. fail the whole lot.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
self._data = error.data;
|
||||||
|
self._startNextAuthStage();
|
||||||
|
}
|
||||||
|
).catch(this._completionDeferred.reject).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick the next stage and call the callback
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_startNextAuthStage: function() {
|
||||||
|
var nextStage = this._chooseStage();
|
||||||
|
if (!nextStage) {
|
||||||
|
throw new Error("No incomplete flows from the server");
|
||||||
|
}
|
||||||
|
|
||||||
|
var stageError = null;
|
||||||
|
if (this._data.errcode || this._data.error) {
|
||||||
|
stageError = {
|
||||||
|
errcode: this._data.errcode || "",
|
||||||
|
error: this._data.error || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this._startAuthStageCallback(nextStage, stageError);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick the next auth stage
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @return {string?} login type
|
||||||
|
*/
|
||||||
|
_chooseStage: function() {
|
||||||
|
var flow = this._chooseFlow();
|
||||||
|
console.log("Active flow => %s", JSON.stringify(flow));
|
||||||
|
var nextStage = this._firstUncompletedStage(flow);
|
||||||
|
console.log("Next stage: %s", nextStage);
|
||||||
|
return nextStage;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick one of the flows from the returned list
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @return {object} flow
|
||||||
|
*/
|
||||||
|
_chooseFlow: function() {
|
||||||
|
var flows = this._data.flows || [];
|
||||||
|
// always use the first flow for now
|
||||||
|
return flows[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first uncompleted stage in the given flow
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {object} flow
|
||||||
|
* @return {string} login type
|
||||||
|
*/
|
||||||
|
_firstUncompletedStage: function(flow) {
|
||||||
|
var completed = (this._data || {}).completed || [];
|
||||||
|
for (var i = 0; i < flow.stages.length; ++i) {
|
||||||
|
var stageType = flow.stages[i];
|
||||||
|
if (completed.indexOf(stageType) === -1) {
|
||||||
|
return stageType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** */
|
||||||
|
module.exports = InteractiveAuth;
|
||||||
@@ -55,6 +55,9 @@ module.exports.ContentRepo = require("./content-repo");
|
|||||||
module.exports.Filter = require("./filter");
|
module.exports.Filter = require("./filter");
|
||||||
/** The {@link module:timeline-window~TimelineWindow} class. */
|
/** The {@link module:timeline-window~TimelineWindow} class. */
|
||||||
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
|
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
|
||||||
|
/** The {@link module:interactive-auth} class. */
|
||||||
|
module.exports.InteractiveAuth = require("./interactive-auth");
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Matrix Call.
|
* Create a new Matrix Call.
|
||||||
|
|||||||
141
spec/unit/interactive-auth.spec.js
Normal file
141
spec/unit/interactive-auth.spec.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var q = require("q");
|
||||||
|
var sdk = require("../..");
|
||||||
|
var utils = require("../test-utils");
|
||||||
|
|
||||||
|
var InteractiveAuth = sdk.InteractiveAuth;
|
||||||
|
var MatrixError = sdk.MatrixError;
|
||||||
|
|
||||||
|
describe("InteractiveAuth", function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
utils.beforeEach(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should start an auth stage and complete it", function(done) {
|
||||||
|
var doRequest = jasmine.createSpy('doRequest');
|
||||||
|
var startAuthStage = jasmine.createSpy('startAuthStage');
|
||||||
|
|
||||||
|
var ia = new InteractiveAuth({
|
||||||
|
doRequest: doRequest,
|
||||||
|
startAuthStage: startAuthStage,
|
||||||
|
authData: {
|
||||||
|
session: "sessionId",
|
||||||
|
flows: [
|
||||||
|
{ stages: ["logintype"] },
|
||||||
|
],
|
||||||
|
params: {
|
||||||
|
"logintype": { param: "aa" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ia.getSessionId()).toEqual("sessionId");
|
||||||
|
expect(ia.getStageParams("logintype")).toEqual({
|
||||||
|
param: "aa",
|
||||||
|
});
|
||||||
|
|
||||||
|
// first we expect a call here
|
||||||
|
startAuthStage.andCallFake(function(stage) {
|
||||||
|
expect(stage).toEqual("logintype");
|
||||||
|
ia.submitAuthDict({
|
||||||
|
type: "logintype",
|
||||||
|
foo: "bar",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// .. which should trigger a call here
|
||||||
|
var requestRes = {"a": "b"};
|
||||||
|
doRequest.andCallFake(function(authData) {
|
||||||
|
expect(authData).toEqual({
|
||||||
|
session: "sessionId",
|
||||||
|
type: "logintype",
|
||||||
|
foo: "bar",
|
||||||
|
});
|
||||||
|
return q(requestRes);
|
||||||
|
});
|
||||||
|
|
||||||
|
ia.attemptAuth().then(function(res) {
|
||||||
|
expect(res).toBe(requestRes);
|
||||||
|
expect(doRequest.calls.length).toEqual(1);
|
||||||
|
expect(startAuthStage.calls.length).toEqual(1);
|
||||||
|
}).catch(utils.failTest).done(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should make a request if no authdata is provided", function(done) {
|
||||||
|
var doRequest = jasmine.createSpy('doRequest');
|
||||||
|
var startAuthStage = jasmine.createSpy('startAuthStage');
|
||||||
|
|
||||||
|
var ia = new InteractiveAuth({
|
||||||
|
doRequest: doRequest,
|
||||||
|
startAuthStage: startAuthStage,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ia.getSessionId()).toBe(undefined);
|
||||||
|
expect(ia.getStageParams("logintype")).toBe(undefined);
|
||||||
|
|
||||||
|
// first we expect a call to doRequest
|
||||||
|
doRequest.andCallFake(function(authData) {
|
||||||
|
console.log("request1", authData);
|
||||||
|
expect(authData).toBe(null);
|
||||||
|
var err = new MatrixError({
|
||||||
|
session: "sessionId",
|
||||||
|
flows: [
|
||||||
|
{ stages: ["logintype"] },
|
||||||
|
],
|
||||||
|
params: {
|
||||||
|
"logintype": { param: "aa" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
err.httpStatus = 401;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// .. which should be followed by a call to startAuthStage
|
||||||
|
var requestRes = {"a": "b"};
|
||||||
|
startAuthStage.andCallFake(function(stage) {
|
||||||
|
expect(stage).toEqual("logintype");
|
||||||
|
expect(ia.getSessionId()).toEqual("sessionId");
|
||||||
|
expect(ia.getStageParams("logintype")).toEqual({
|
||||||
|
param: "aa",
|
||||||
|
});
|
||||||
|
|
||||||
|
// submitAuthDict should trigger another call to doRequest
|
||||||
|
doRequest.andCallFake(function(authData) {
|
||||||
|
console.log("request2", authData);
|
||||||
|
expect(authData).toEqual({
|
||||||
|
session: "sessionId",
|
||||||
|
type: "logintype",
|
||||||
|
foo: "bar",
|
||||||
|
});
|
||||||
|
return q(requestRes);
|
||||||
|
});
|
||||||
|
|
||||||
|
ia.submitAuthDict({
|
||||||
|
type: "logintype",
|
||||||
|
foo: "bar",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ia.attemptAuth().then(function(res) {
|
||||||
|
expect(res).toBe(requestRes);
|
||||||
|
expect(doRequest.calls.length).toEqual(2);
|
||||||
|
expect(startAuthStage.calls.length).toEqual(1);
|
||||||
|
}).catch(utils.failTest).done(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user