token.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import _ from "lodash";
  2. import errs from "../lib/error.js";
  3. import { parseDatePeriod } from "../lib/helpers.js";
  4. import authModel from "../models/auth.js";
  5. import TokenModel from "../models/token.js";
  6. import userModel from "../models/user.js";
  7. import twoFactor from "./2fa.js";
  8. const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
  9. const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
  10. const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code";
  11. const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa";
  12. export default {
  13. /**
  14. * @param {Object} data
  15. * @param {String} data.identity
  16. * @param {String} data.secret
  17. * @param {String} [data.scope]
  18. * @param {String} [data.expiry]
  19. * @param {String} [issuer]
  20. * @returns {Promise}
  21. */
  22. getTokenFromEmail: async (data, issuer) => {
  23. const Token = TokenModel();
  24. data.scope = data.scope || "user";
  25. data.expiry = data.expiry || "1d";
  26. const user = await userModel
  27. .query()
  28. .where("email", data.identity.toLowerCase().trim())
  29. .andWhere("is_deleted", 0)
  30. .andWhere("is_disabled", 0)
  31. .first();
  32. if (!user) {
  33. throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
  34. }
  35. const auth = await authModel
  36. .query()
  37. .where("user_id", "=", user.id)
  38. .where("type", "=", "password")
  39. .first();
  40. if (!auth) {
  41. throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
  42. }
  43. const valid = await auth.verifyPassword(data.secret);
  44. if (!valid) {
  45. throw new errs.AuthError(
  46. ERROR_MESSAGE_INVALID_AUTH,
  47. ERROR_MESSAGE_INVALID_AUTH_I18N,
  48. );
  49. }
  50. if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
  51. // The scope requested doesn't exist as a role against the user,
  52. // you shall not pass.
  53. throw new errs.AuthError(`Invalid scope: ${data.scope}`);
  54. }
  55. // Check if 2FA is enabled
  56. const has2FA = await twoFactor.isEnabled(user.id);
  57. if (has2FA) {
  58. // Return challenge token instead of full token
  59. const challengeToken = await Token.create({
  60. iss: issuer || "api",
  61. attrs: {
  62. id: user.id,
  63. },
  64. scope: ["2fa-challenge"],
  65. expiresIn: "5m",
  66. });
  67. return {
  68. requires_2fa: true,
  69. challenge_token: challengeToken.token,
  70. };
  71. }
  72. // Create a moment of the expiry expression
  73. const expiry = parseDatePeriod(data.expiry);
  74. if (expiry === null) {
  75. throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
  76. }
  77. const signed = await Token.create({
  78. iss: issuer || "api",
  79. attrs: {
  80. id: user.id,
  81. },
  82. scope: [data.scope],
  83. expiresIn: data.expiry,
  84. });
  85. return {
  86. token: signed.token,
  87. expires: expiry.toISOString(),
  88. };
  89. },
  90. /**
  91. * @param {Access} access
  92. * @param {Object} [data]
  93. * @param {String} [data.expiry]
  94. * @param {String} [data.scope] Only considered if existing token scope is admin
  95. * @returns {Promise}
  96. */
  97. getFreshToken: async (access, data) => {
  98. const Token = TokenModel();
  99. const thisData = data || {};
  100. thisData.expiry = thisData.expiry || "1d";
  101. if (access?.token.getUserId(0)) {
  102. // Create a moment of the expiry expression
  103. const expiry = parseDatePeriod(thisData.expiry);
  104. if (expiry === null) {
  105. throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`);
  106. }
  107. const token_attrs = {
  108. id: access.token.getUserId(0),
  109. };
  110. // Only admins can request otherwise scoped tokens
  111. let scope = access.token.get("scope");
  112. if (thisData.scope && access.token.hasScope("admin")) {
  113. scope = [thisData.scope];
  114. if (thisData.scope === "job-board" || thisData.scope === "worker") {
  115. token_attrs.id = 0;
  116. }
  117. }
  118. const signed = await Token.create({
  119. iss: "api",
  120. scope: scope,
  121. attrs: token_attrs,
  122. expiresIn: thisData.expiry,
  123. });
  124. return {
  125. token: signed.token,
  126. expires: expiry.toISOString(),
  127. };
  128. }
  129. throw new error.AssertionFailedError("Existing token contained invalid user data");
  130. },
  131. /**
  132. * Verify 2FA code and return full token
  133. * @param {string} challengeToken
  134. * @param {string} code
  135. * @param {string} [expiry]
  136. * @returns {Promise}
  137. */
  138. verify2FA: async (challengeToken, code, expiry) => {
  139. const Token = TokenModel();
  140. const tokenExpiry = expiry || "1d";
  141. // Verify challenge token
  142. let tokenData;
  143. try {
  144. tokenData = await Token.load(challengeToken);
  145. } catch {
  146. throw new errs.AuthError("Invalid or expired challenge token");
  147. }
  148. // Check scope
  149. if (!tokenData.scope || tokenData.scope[0] !== "2fa-challenge") {
  150. throw new errs.AuthError("Invalid challenge token");
  151. }
  152. const userId = tokenData.attrs?.id;
  153. if (!userId) {
  154. throw new errs.AuthError("Invalid challenge token");
  155. }
  156. // Verify 2FA code
  157. const valid = await twoFactor.verifyForLogin(userId, code);
  158. if (!valid) {
  159. throw new errs.AuthError(
  160. ERROR_MESSAGE_INVALID_2FA,
  161. ERROR_MESSAGE_INVALID_2FA_I18N,
  162. );
  163. }
  164. // Create full token
  165. const expiryDate = parseDatePeriod(tokenExpiry);
  166. if (expiryDate === null) {
  167. throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`);
  168. }
  169. const signed = await Token.create({
  170. iss: "api",
  171. attrs: {
  172. id: userId,
  173. },
  174. scope: ["user"],
  175. expiresIn: tokenExpiry,
  176. });
  177. return {
  178. token: signed.token,
  179. expires: expiryDate.toISOString(),
  180. };
  181. },
  182. /**
  183. * @param {Object} user
  184. * @returns {Promise}
  185. */
  186. getTokenFromUser: async (user) => {
  187. const expire = "1d";
  188. const Token = TokenModel();
  189. const expiry = parseDatePeriod(expire);
  190. const signed = await Token.create({
  191. iss: "api",
  192. attrs: {
  193. id: user.id,
  194. },
  195. scope: ["user"],
  196. expiresIn: expire,
  197. });
  198. return {
  199. token: signed.token,
  200. expires: expiry.toISOString(),
  201. user: user,
  202. };
  203. },
  204. };