"use strict"; const Promise = require("bluebird"); const expressPromiseRouter = require("express-promise-router"); const scryptForHumans = require("scrypt-for-humans"); const errors = require("../../errors"); const normalize = require("../../middlewares/normalize-payload"); const validate = require("../../middlewares/validate-payload"); const processDeviceId = require("../../process-device-id"); const matchValue = require("../../match-value"); const canonicalizePhoneNumber = require("../../canonicalize-phone-number"); function normalizeIdentifier(payload) { if (payload.identifier != null) { /* Never overwrite the `identifier` key if it already exists */ return null; } else if (payload.user != null) { return { identifier: { type: "m.id.user", user: payload.user } }; } else if (payload.address != null) { return { identifier: { type: "m.id.thirdparty", medium: payload.medium, address: payload.address } }; } } function normalizePhoneNumber(payload) { if (payload.identifier != null && payload.identifier.type === "m.id.phone") { return { identifier: { type: "m.id.thirdparty", medium: "msisdn", address: canonicalizePhoneNumber(payload.identifier.country, payload.identifier.phone) } }; } } function validateUserIdentifier(userIdentifier) { let {assertProperties, isPresent, isString, isOneOf} = require("../../validate.js"); assertProperties(userIdentifier, { type: [ isPresent, isOneOf("m.id.user", "m.id.thirdparty", "m.id.phone") ] }); matchValue(userIdentifier.type, { "m.id.user": () => { assertProperties(userIdentifier, { user: [ isPresent, isString ] }); }, "m.id.thirdparty": () => { assertProperties(userIdentifier, { medium: [ isPresent, isString ], address: [ isPresent, isString ] }); }, "m.id.phone": () => { assertProperties(userIdentifier, { country: [ isPresent, isString ], phone: [ isPresent, isString ] }); } }); } module.exports = function(state) { const requireAccessToken = require("../../middlewares/require-access-token")(state); let {db, configuration, fullyQualifyUser} = state; let router = expressPromiseRouter(); router.get("/r0/login", (req, res) => { res.json({ flows: [{ type: "m.login.password" }] }); }); function validateLoginPayload(payload) { let {assertProperties, isPresent, isString, isOneOf} = require("../../validate.js"); assertProperties(payload, { type: [ isPresent, isOneOf("m.login.password", "m.login.token") ], device_id: [ isString ], initial_device_display_name: [ isString ], }); matchValue(payload.type, { "m.login.password": () => { assertProperties(payload, { identifier: [ isPresent, validateUserIdentifier ], password: [ isPresent, isString ] }); }, "m.login.token": () => { assertProperties(payload, { token: [ isPresent, isString ] }); } }); } router.post("/r0/login", normalize([normalizeIdentifier, normalizePhoneNumber]), validate(validateLoginPayload), (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-login // Authenticates the user, and issues an access token they can use to authorize themself in subsequent requests. /* FIXME: Rate-limit */ return Promise.try(() => { if (req.body.identifier.type === "m.id.user") { return db.accounts.byUsername({ username: req.body.identifier.user }); } else if (req.body.identifier.type === "m.id.thirdparty") { // FIXME } else { /* NOTE: We're not handling m.id.phone here, as that's normalized into m.id.thirdparty in normalizePhoneNumber */ throw new errors.Unreachable("Invalid identifier type"); } }).then((user) => { let deviceId = processDeviceId(req.body.device_id); return Promise.try(() => { if (req.body.type === "m.login.password") { return scryptForHumans.verifyHash(req.body.password, user.password_hash); } else if (req.body.type === "m.login.token") { // FIXME } else { throw new errors.Unreachable("Invalid login type"); } }).then(() => { return db.devices.createDeviceSession({ name: req.body.initial_device_display_name, deviceId: deviceId, userId: user.id }).then((device) => { res.send({ access_token: device.token, user_id: fullyQualifyUser(user.username), home_server: configuration.hostname, device_id: device.deviceId }); }); }); }).catch(errors.InvalidUsername, errors.Forbidden.chain("Invalid username specified")) .catch(scryptForHumans.PasswordError, errors.Forbidden.chain("Invalid password specified")); /* FIXME: Formatting above */ }); router.post("/r0/logout", requireAccessToken, (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-logout // Invalidates an existing access token, so that it can no longer be used for authorization. }); router.post("/r0/logout/all", requireAccessToken, (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-logout-all // Invalidates all access tokens for a user, so that they can no longer be used for authorization. This includes the access token that made this request. }); router.post("/r0/register/available", (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-register-available // Checks to see if a username is available, and valid, for the server. }); router.post("/r0/register", (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register // Register for an account on this homeserver. This API endpoint uses the User-Interactive Authentication API. // Optionally allows user-interactive auth }); router.post("/r0/register/:source/requestToken", (req, res) => { // source === "email" // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register-email-requesttoken // Proxies the Identity Service API validate/email/requestToken, but first checks that the given email address is not already associated with an account on this homeserver. See the Identity Service API for further information. // source === "msisdn" // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register-msisdn-requesttoken // Proxies the Identity Service API validate/msisdn/requestToken, but first checks that the given phone number is not already associated with an account on this homeserver. See the Identity Service API for further information. // NOTE: Unclear whether it requires authentication or not. }); router.post("/r0/account/password", (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password // Changes the password for an account on this homeserver. // NOTE: Requires access token *or* interactive auth. }); router.post("/r0/account/password/:source/requestToken", (req, res) => { // source === "email" // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password-email-requesttoken // Proxies the Identity Service API validate/email/requestToken, but first checks that the given email address is associated with an account on this homeserver. This API should be used to request validation tokens when authenticating for the account/password endpoint. // source === "msisdn" // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password-msisdn-requesttoken // Proxies the Identity Service API validate/msisdn/requestToken, but first checks that the given phone number is associated with an account on this homeserver. This API should be used to request validation tokens when authenticating for the account/password endpoint. // NOTE: Unclear whether it requires authentication or not. }); router.post("/r0/account/deactivate", (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-deactivate // Deactivate the user's account, removing all ability for the user to login again. This API endpoint uses the User-Interactive Authentication API. // NOTE: Requires access token *or* interactive auth. }); // FIXME: Modularize out 'create session for device' logic for both login and register endpoints return router; }