<commit_before>const logger = require('@cardstack/plugin-utils/logger');
const Session = require('@cardstack/plugin-utils/session');
const bearerTokenPattern = /bearer +(.*)$/i;
const compose = require('koa-compose');
const route = require('koa-better-route');
const koaJSONBody = require('koa-json-body');
const Handlebars = require('handlebars');
const { declareInjections } = require('@cardstack/di');


module.exports = declareInjections({
  encryptor: 'hub:encryptor',
  searcher: 'hub:searchers',
  writer: 'hub:writers',
  schemaCache: 'hub:schema-cache'
},

class Authentication {

  constructor() {
    this.log = logger('auth');

    // TODO: move these two settings into config
    this.controllingBranch = 'master';

  }

  get userSearcher() {
    return {
      get: (type, userId) => {
        return this.searcher.get(this.controllingBranch, type, userId);
      },
      search: (params) => {
        return this.searcher.search(this.controllingBranch, params);
      }
    };
  }

  async createToken(sessionPayload, validSeconds) {
    let validUntil = Math.floor(Date.now()/1000 + validSeconds);
    return {
      token: this.encryptor.encryptAndSign([sessionPayload, validUntil]),
      validUntil
    };
  }

  _tokenToSession(token) {
    try {
      let [sessionPayload, validUntil] = this.encryptor.verifyAndDecrypt(token);
      if (validUntil <= Date.now()/1000) {
        this.log.debug("Ignoring expired token");
      } else {
        return new Session(sessionPayload, this.userSearcher);
      }
    } catch (err) {
      if (/unable to authenticate data|invalid key length|Not a valid signed message/.test(err.message)) {
        this.log.warn("Ignoring invalid token");
      } else {
        throw err;
      }
    }
  }

  get category() {
    return 'authentication';
  }

  middleware() {
    const prefix = 'auth';
    return compose([
      this._tokenVerifier(),
      this._tokenIssuerPreflight(prefix),
      this._tokenIssuer(prefix),
      this._exposeConfiguration(prefix)
    ]);
  }

  _tokenVerifier() {
    return async (ctxt, next) => {
      let m = bearerTokenPattern.exec(ctxt.header['authorization']);
      if (m) {
        let session = this._tokenToSession(m[1]);
        if (session) {
          ctxt.state.cardstackSession = session;
        }
      }
      await next();
    };
  }

  _tokenIssuerPreflight(prefix) {
    return route.options(`/${prefix}/:module`,  async (ctxt) => {
      ctxt.response.set('Access-Control-Allow-Origin', '*');
      ctxt.response.set('Access-Control-Allow-Methods', 'POST,OPTIONS');
      ctxt.response.set('Access-Control-Allow-Headers', 'Content-Type');
      ctxt.status = 200;
    });
  }

  async _locateAuthenticationSource(name) {
    let source = await this.searcher.get(this.controllingBranch, 'authentication-sources', name);
    let schema = await this.schemaCache.schemaForControllingBranch();
    let plugin = schema.plugins.lookupFeature('authenticators', source.attributes['authenticator-type']);
    return { plugin, source };
  }

  async _invokeAuthenticationSource(ctxt, sourceAndPlugin) {
    let { source, plugin } = sourceAndPlugin;
    let params = source.attributes.params;
    let result = await plugin.authenticate(ctxt.request.body, params, this.userSearcher);

    if (result && result.partialSession) {
      if (result.partialSession.type == null) {
        result.partialSession.type = 'partial-sessions';
      }
      ctxt.body = {
        data: result.partialSession
      };
      ctxt.status = 200;
      return;
    }

    if (!result || !(result.preloadedUser || result.user)) {
      ctxt.status = 401;
      ctxt.body = {
        errors: [{
          title: "Not authorized",
          detail: "The authentication plugin did not approve your request"
        }]
      };
      return;
    }

    let user = result.preloadedUser || await this._processExternalUser(result.user, source, plugin);

    if (!user) {
      ctxt.status = 401;
      ctxt.body = {
        errors: [{
          title: "Not authorized",
          detail: "The authentication plugin attempted to approve you but we found no corresponding user record"
        }]
      };

      return;
    }

    ctxt.body = {
      data: user,
      meta: await this.createToken({ id: user.id, type: user.type }, 86400)
    };
    ctxt.status = 200;
  }

  async _processExternalUser(externalUser, source, plugin) {
    let user = this._rewriteExternalUser(externalUser, source.attributes['user-template'] || plugin.defaultUserTemplate);
    if (user.type == null) { return; }

    let have;

    if (user.id != null) {
      try {
        have = await this.userSearcher.get(user.type, user.id);
      } catch (err) {
        if (err.status !== 404) {
          throw err;
        }
      }
    }
    if (!have && source.attributes['may-create-user']) {
      return this.writer.create(this.controllingBranch, Session.INTERNAL_PRIVLEGED, user.type, user);
    }
    if (have && source.attributes['may-update-user']) {
      user.meta = have.meta;
      return this.writer.update(this.controllingBranch, Session.INTERNAL_PRIVLEGED, user.type, have.id, user);
    }
    return have;
  }<commit_msg>Allow plugin to specify expiry of auth token<commit_after>const logger = require('@cardstack/plugin-utils/logger');
const Session = require('@cardstack/plugin-utils/session');
const bearerTokenPattern = /bearer +(.*)$/i;
const compose = require('koa-compose');
const route = require('koa-better-route');
const koaJSONBody = require('koa-json-body');
const Handlebars = require('handlebars');
const { declareInjections } = require('@cardstack/di');


module.exports = declareInjections({
  encryptor: 'hub:encryptor',
  searcher: 'hub:searchers',
  writer: 'hub:writers',
  schemaCache: 'hub:schema-cache'
},

class Authentication {

  constructor() {
    this.log = logger('auth');

    // TODO: move these two settings into config
    this.controllingBranch = 'master';

  }

  get userSearcher() {
    return {
      get: (type, userId) => {
        return this.searcher.get(this.controllingBranch, type, userId);
      },
      search: (params) => {
        return this.searcher.search(this.controllingBranch, params);
      }
    };
  }

  async createToken(sessionPayload, validSeconds) {
    let validUntil = Math.floor(Date.now()/1000 + validSeconds);
    return {
      token: this.encryptor.encryptAndSign([sessionPayload, validUntil]),
      validUntil
    };
  }

  _tokenToSession(token) {
    try {
      let [sessionPayload, validUntil] = this.encryptor.verifyAndDecrypt(token);
      if (validUntil <= Date.now()/1000) {
        this.log.debug("Ignoring expired token");
      } else {
        return new Session(sessionPayload, this.userSearcher);
      }
    } catch (err) {
      if (/unable to authenticate data|invalid key length|Not a valid signed message/.test(err.message)) {
        this.log.warn("Ignoring invalid token");
      } else {
        throw err;
      }
    }
  }

  get category() {
    return 'authentication';
  }

  middleware() {
    const prefix = 'auth';
    return compose([
      this._tokenVerifier(),
      this._tokenIssuerPreflight(prefix),
      this._tokenIssuer(prefix),
      this._exposeConfiguration(prefix)
    ]);
  }

  _tokenVerifier() {
    return async (ctxt, next) => {
      let m = bearerTokenPattern.exec(ctxt.header['authorization']);
      if (m) {
        let session = this._tokenToSession(m[1]);
        if (session) {
          ctxt.state.cardstackSession = session;
        }
      }
      await next();
    };
  }

  _tokenIssuerPreflight(prefix) {
    return route.options(`/${prefix}/:module`,  async (ctxt) => {
      ctxt.response.set('Access-Control-Allow-Origin', '*');
      ctxt.response.set('Access-Control-Allow-Methods', 'POST,OPTIONS');
      ctxt.response.set('Access-Control-Allow-Headers', 'Content-Type');
      ctxt.status = 200;
    });
  }

  async _locateAuthenticationSource(name) {
    let source = await this.searcher.get(this.controllingBranch, 'authentication-sources', name);
    let schema = await this.schemaCache.schemaForControllingBranch();
    let plugin = schema.plugins.lookupFeature('authenticators', source.attributes['authenticator-type']);
    return { plugin, source };
  }

  async _invokeAuthenticationSource(ctxt, sourceAndPlugin) {
    let { source, plugin } = sourceAndPlugin;
    let params = source.attributes.params;
    let result = await plugin.authenticate(ctxt.request.body, params, this.userSearcher);

    if (result && result.partialSession) {
      if (result.partialSession.type == null) {
        result.partialSession.type = 'partial-sessions';
      }
      ctxt.body = {
        data: result.partialSession
      };
      ctxt.status = 200;
      return;
    }

    if (!result || !(result.preloadedUser || result.user)) {
      ctxt.status = 401;
      ctxt.body = {
        errors: [{
          title: "Not authorized",
          detail: "The authentication plugin did not approve your request"
        }]
      };
      return;
    }

    let user = result.preloadedUser || await this._processExternalUser(result.user, source, plugin);

    if (!user) {
      ctxt.status = 401;
      ctxt.body = {
        errors: [{
          title: "Not authorized",
          detail: "The authentication plugin attempted to approve you but we found no corresponding user record"
        }]
      };

      return;
    }

    ctxt.body = {
      data: user,
      meta: await this.createToken({ id: user.id, type: user.type }, source.attributes['token-expiry'] || 86400)
    };
    ctxt.status = 200;
  }

  async _processExternalUser(externalUser, source, plugin) {
    let user = this._rewriteExternalUser(externalUser, source.attributes['user-template'] || plugin.defaultUserTemplate);
    if (user.type == null) { return; }

    let have;

    if (user.id != null) {
      try {
        have = await this.userSearcher.get(user.type, user.id);
      } catch (err) {
        if (err.status !== 404) {
          throw err;
        }
      }
    }
    if (!have && source.attributes['may-create-user']) {
      return this.writer.create(this.controllingBranch, Session.INTERNAL_PRIVLEGED, user.type, user);
    }
    if (have && source.attributes['may-update-user']) {
      user.meta = have.meta;
      return this.writer.update(this.controllingBranch, Session.INTERNAL_PRIVLEGED, user.type, have.id, user);
    }
    return have;
  }
{'fraction_non_alphanumeric': 0.13555427914991383, 'fraction_numerical': 0.006126747080222095, 'mean_word_length': 2.8183479532163744, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '970697', 'n_tokens_mistral': 3229, 'n_tokens_neox': 3157, 'n_words': 654}