<NME> authentication.ts
<BEF> ADDFILE
<MSG> Add building of auth token

<DFF> @@ -0,0 +1,72 @@
+import { createCipheriv, createDecipheriv, createHmac, randomBytes } from 'crypto';
+
+const SECRET = process.env.SERVER_SECRET as string;
+const encryptionAlgorithm = 'aes-256-gcm';
+const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24;
+
+function encrypt(plaintext: string): string {
+  const key = Buffer.from(SECRET, 'base64');
+  const iv = randomBytes(16);
+  const cipher = createCipheriv(encryptionAlgorithm, key, iv);
+  const encrypted = [
+    iv.toString('hex'),
+    '--',
+    cipher.update(plaintext, 'utf8', 'hex'),
+    cipher.final('hex'),
+    '--',
+    cipher.getAuthTag().toString('hex'),
+  ];
+  return encrypted.join('');
+}
+
+function decrypt(cipherText: string) {
+  const [ivString, encryptedString, authTagString] = cipherText.split('--');
+  const iv = Buffer.from(ivString, 'hex');
+  const key = Buffer.from(SECRET, 'base64');
+  const decipher = createDecipheriv(encryptionAlgorithm, key, iv);
+  decipher.setAuthTag(Buffer.from(authTagString, 'hex'));
+  let decrypted = decipher.update(encryptedString, 'hex', 'utf8') + decipher.final('utf8');
+  return decrypted;
+}
+
+if (!SECRET) {
+  throw new Error('Missing ENV var SERVER_SECRET');
+}
+
+export class AuthenticationUtils {
+  generateNonce(): string {
+    let timestampInNanoseconds = process.hrtime.bigint().toString();
+    let hmac = createHmac('sha256', SECRET);
+    let nonceSignature = hmac.update(`${timestampInNanoseconds}:${SECRET}`).digest('hex');
+    let nonce = `${Buffer.from(timestampInNanoseconds).toString('base64')}:${nonceSignature}`;
+    return nonce;
+  }
+
+  extractVerifiedTimestamp(nonce: string): bigint {
+    let [base64Timestamp, signature] = nonce.split(':');
+    let timestamp = Buffer.from(base64Timestamp, 'base64').toString('ascii');
+    let hmac = createHmac('sha256', SECRET);
+    let expectedNonceSignature = hmac.update(`${timestamp}:${SECRET}`).digest('hex');
+    if (expectedNonceSignature === signature) {
+      return BigInt(timestamp);
+    } else {
+      throw new Error('Invalid signature');
+    }
+  }
+
+  buildAuthToken(userAddress: string) {
+    let timestamp = new Date(Date.now() + ONE_DAY_IN_MS).toISOString();
+    let token = `current_user_id=${userAddress}&expires_at=${timestamp}`;
+    return encrypt(token);
+  }
+
+  decryptAuthToken(encryptedAuthToken: string) {
+    return decrypt(encryptedAuthToken);
+  }
+}
+
+declare module '@cardstack/hub/dependency-injection' {
+  interface KnownServices {
+    'authentication-utils': AuthenticationUtils;
+  }
+}

{'commit': 'diff --git a/packages/hub/main.ts b/packages/hub/main.ts\nindex 052986977..6a01cc8a8 100644\n--- a/packages/hub/main.ts\n+++ b/packages/hub/main.ts\n@@ -9,7 +9,7 @@ import AuthenticationMiddleware from \'./authentication-middleware\';\n import DevelopmentConfig from \'./development-config\';\n import DevelopmentProxyMiddleware from \'./development-proxy-middleware\';\n import SessionRoute from \'./routes/session\';\n-import { NonceGenerator } from \'./utils/session\';\n+import { AuthenticationUtils } from \'./utils/authentication\';\n import JsonapiMiddleware from \'./jsonapi-middleware\';\n \n const log = logger(\'cardstack/hub\');\ndiff --git a/packages/hub/node-tests/routes/session-test.ts b/packages/hub/node-tests/routes/session-test.ts\nindex 3a5937d99..b23272afa 100644\n--- a/packages/hub/node-tests/routes/session-test.ts\n+++ b/packages/hub/node-tests/routes/session-test.ts\n@@ -4,20 +4,24 @@ import { bootEnvironmentForTesting } from \'../../main\';\n import { Registry } from \'../../dependency-injection\';\n \n let stubNonce = \'abc:123\';\n+let stubAuthToken = \'def--456\';\n \n-class StubNonceGenerator {\n-  generate() {\n+class StubAuthenticationUtils {\n+  generateNonce() {\n     return stubNonce;\n   }\n+  buildAuthToken() {\n+    return stubAuthToken;\n+  }\n }\n-describe(\'GET /session\', function () {\n+describe(\'GET /api/session\', function () {\n   let server: Server;\n   let request: supertest.SuperTest<Test>;\n   this.beforeEach(async function () {\n     server = await bootEnvironmentForTesting({\n       port: 3001,\n       registryCallback(registry: Registry) {\n-        registry.register(\'nonce-generator\', StubNonceGenerator);\n+        registry.register(\'authentication-utils\', StubAuthenticationUtils);\n       },\n     });\n     request = supertest(server);\n@@ -29,15 +33,15 @@ describe(\'GET /session\', function () {\n \n   it(\'responds with json\', function (done) {\n     request\n-      .get(\'/session\')\n-      .set(\'Accept\', \'application/json\')\n+      .get(\'/api/session\')\n+      .set(\'Accept\', \'application/vnd.api+json\')\n       .expect(401)\n-      .expect(\'Content-Type\', /json/)\n-      .expect({ nonce: stubNonce, version: \'0.0.1\' }, done);\n+      .expect({ data: { nonce: stubNonce, version: \'0.0.1\' } })\n+      .expect(\'Content-Type\', \'application/vnd.api+json\', done);\n   });\n });\n \n-describe(\'POST /session\', function () {\n+describe(\'POST /api/session\', function () {\n   let server: Server;\n   let request: supertest.SuperTest<Test>;\n   let bodyWithCorrectSignature: any;\n@@ -46,7 +50,7 @@ describe(\'POST /session\', function () {\n     server = await bootEnvironmentForTesting({\n       port: 3001,\n       registryCallback(registry: Registry) {\n-        registry.register(\'nonce-generator\', StubNonceGenerator);\n+        registry.register(\'authentication-utils\', StubAuthenticationUtils);\n       },\n     });\n     request = supertest(server);\n@@ -83,28 +87,43 @@ describe(\'POST /session\', function () {\n \n   it(\'responds with json when signature is correct\', function (done) {\n     request\n-      .post(\'/session\')\n-      .send(bodyWithCorrectSignature)\n-      .set(\'Content-Type\', \'application/json\')\n-      .set(\'Accept\', \'application/json\')\n+      .post(\'/api/session\')\n+      .send({\n+        data: {\n+          attributes: bodyWithCorrectSignature,\n+        },\n+      })\n+      .set(\'Content-Type\', \'application/vnd.api+json\')\n+      .set(\'Accept\', \'application/vnd.api+json\')\n       .expect(200)\n       .expect(\'Content-Type\', /json/)\n-      .expect({ authToken: \'tbdauthtoken\' }, done);\n+      .expect(\n+        {\n+          data: {\n+            authToken: stubAuthToken,\n+          },\n+        },\n+        done\n+      );\n   });\n \n   // * Server use EC recover function to verify that the signature was signed by the signer.\n   // On failure, 401 "Signature not verified"\n   it(\'responds with 401 when signature invalid\', function (done) {\n     let bodyWithIncorrectSignature = bodyWithCorrectSignature;\n-    bodyWithIncorrectSignature.signature = \'0x056e70aaaaaaaaaaaaaaaaa\';\n+    bodyWithIncorrectSignature.signature = bodyWithCorrectSignature.signature.replace(\'a\', \'1\').replace(\'b\', \'2\');\n     request\n-      .post(\'/session\')\n-      .send(bodyWithCorrectSignature)\n-      .set(\'Content-Type\', \'application/json\')\n-      .set(\'Accept\', \'application/json\')\n+      .post(\'/api/session\')\n+      .send({\n+        data: {\n+          attributes: bodyWithIncorrectSignature,\n+        },\n+      })\n+      .set(\'Content-Type\', \'application/vnd.api+json\')\n+      .set(\'Accept\', \'application/vnd.api+json\')\n       .expect(401)\n       .expect(\'Content-Type\', /json/)\n-      .expect({ error: \'Invalid signature\' }, done);\n+      .expect({ error: \'Signature not verified\' }, done);\n   });\n \n   // * Server verifies that nonce is less than 5 minutes old. On failure, 401 "Expired nonce"\ndiff --git a/packages/hub/node-tests/utils/authentication-test.ts b/packages/hub/node-tests/utils/authentication-test.ts\nnew file mode 100644\nindex 000000000..920f288e7\n--- /dev/null\n+++ b/packages/hub/node-tests/utils/authentication-test.ts\n@@ -0,0 +1,42 @@\n+import { AuthenticationUtils } from \'../../utils/authentication\';\n+\n+function delay(ms: number) {\n+  return new Promise((resolve) => setTimeout(resolve, ms));\n+}\n+\n+describe(\'AuthenticationUtils\', function () {\n+  let subject: AuthenticationUtils;\n+\n+  beforeEach(function () {\n+    subject = new AuthenticationUtils();\n+  });\n+\n+  it(\'it can generate a nonce\', async function () {\n+    let nonce1 = subject.generateNonce();\n+    expect(nonce1).to.contain(\':\');\n+    await delay(5);\n+    let nonce2 = subject.generateNonce();\n+    expect(nonce1).not.to.equal(nonce2);\n+  });\n+\n+  it(\'can extract the timestamp from a valid nonce\', function () {\n+    let nonce1 = subject.generateNonce();\n+    let timestamp = subject.extractVerifiedTimestamp(nonce1);\n+    expect(Number(process.hrtime.bigint() - timestamp)).to.be.lessThan(10000000); // within 10ms\n+  });\n+\n+  it(\'throws in the case of an invalid nonce\', function () {\n+    expect(function () {\n+      subject.extractVerifiedTimestamp(\'abc:123\');\n+    }).to.throw(\'Invalid signature\');\n+  });\n+\n+  it(\'can generate an encrypted auth token and decrypt it\', function () {\n+    let address = \'0x2f58630CA445Ab1a6DE2Bb9892AA2e1d60876C13\';\n+    let ciphertext = subject.buildAuthToken(\'0x2f58630CA445Ab1a6DE2Bb9892AA2e1d60876C13\');\n+    expect(ciphertext.split(\'--\').length).to.equal(3);\n+    expect(ciphertext).to.not.include(address);\n+    let plaintext = subject.decryptAuthToken(ciphertext);\n+    expect(plaintext).to.match(/current_user_id=0x2f58630CA445Ab1a6DE2Bb9892AA2e1d60876C13&expires_at=/);\n+  });\n+});\ndiff --git a/packages/hub/node-tests/utils/session-test.ts b/packages/hub/node-tests/utils/session-test.ts\ndeleted file mode 100644\nindex e35277ab9..000000000\n--- a/packages/hub/node-tests/utils/session-test.ts\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-import { NonceGenerator } from \'../../utils/session\';\n-\n-function delay(ms: number) {\n-  return new Promise((resolve) => setTimeout(resolve, ms));\n-}\n-\n-describe(\'NonceGenerator\', function () {\n-  let generator: NonceGenerator;\n-\n-  beforeEach(function () {\n-    generator = new NonceGenerator();\n-  });\n-\n-  it(\'it can generate a nonce\', async function () {\n-    let nonce1 = generator.generate();\n-    expect(nonce1).to.contain(\':\');\n-    await delay(5);\n-    let nonce2 = generator.generate();\n-    expect(nonce1).not.to.equal(nonce2);\n-  });\n-\n-  it(\'can extract the timestamp from a valid nonce\', function () {\n-    let nonce1 = generator.generate();\n-    let timestamp = generator.extractVerifiedTimestamp(nonce1);\n-    expect(Number(process.hrtime.bigint() - timestamp)).to.be.lessThan(10000000); // within 10ms\n-  });\n-\n-  it(\'throws in the case of an invalid nonce\', function () {\n-    expect(function () {\n-      generator.extractVerifiedTimestamp(\'abc:123\');\n-    }).to.throw(\'Invalid signature\');\n-  });\n-});\ndiff --git a/packages/hub/package.json b/packages/hub/package.json\nindex d760b716e..19d0a6470 100644\n--- a/packages/hub/package.json\n+++ b/packages/hub/package.json\n@@ -20,6 +20,7 @@\n     "assert-never": "^1.2.1",\n     "dag-map": "^2.0.2",\n     "esm": "^3.2.25",\n+    "eth-sig-util": "^3.0.1",\n     "fast-json-stable-stringify": "^2.1.0",\n     "fs-extra": "^10.0.0",\n     "glob": "^7.1.7",\n@@ -51,7 +52,7 @@\n   },\n   "scripts": {\n     "test": "mocha -r esm ../test-support/bin/run.js --timeout 60000",\n-    "autotest": "mocha -w -r esm ../test-support/bin/run.js --timeout 60000"\n+    "autotest": "mocha -w --reporter=min -r esm ../test-support/bin/run.js --timeout 60000"\n   },\n   "volta": {\n     "extends": "../../package.json"\ndiff --git a/packages/hub/routes/session.ts b/packages/hub/routes/session.ts\nindex ab8154acd..c2f565e96 100644\n--- a/packages/hub/routes/session.ts\n+++ b/packages/hub/routes/session.ts\n@@ -1,24 +1,57 @@\n import Koa from \'koa\';\n import { inject } from \'../dependency-injection\';\n-import { NonceGenerator } from \'../utils/session\';\n+import { AuthenticationUtils } from \'../utils/session\';\n+import { recoverTypedSignature } from \'eth-sig-util\';\n \n export default class SessionRoute {\n-  nonceGenerator: NonceGenerator = inject(\'nonce-generator\', { as: \'nonceGenerator\' });\n+  authenticationUtils: AuthenticationUtils = inject(\'authentication-utils\', { as: \'authenticationUtils\' });\n \n   get(ctx: Koa.Context) {\n-    ctx.set(\'Content-Type\', \'application/json\');\n     ctx.status = 401;\n     ctx.body = {\n-      nonce: this.nonceGenerator.generate(),\n-      version: \'0.0.1\',\n+      data: {\n+        nonce: this.authenticationUtils.generateNonce(),\n+        version: \'0.0.1\',\n+      },\n     };\n+    ctx.type = \'application/vnd.api+json\';\n   }\n \n-  post(ctx: Koa.Context) {\n-    ctx.set(\'Content-Type\', \'application/json\');\n-    ctx.status = 200;\n+  async post(ctx: Koa.Context) {\n+    let { authData, signature } = ctx.request.body.data.attributes;\n+    let userAddress = authData.message.user;\n+    try {\n+      let recoveredAddress = recoverTypedSignature({\n+        data: authData,\n+        sig: signature,\n+      });\n+      // Do a case-insensitive match because the case of the address returned by pubToAddress\n+      // does not always match the case of the address from the web3 provider.\n+      let isVerified = recoveredAddress.toUpperCase() === userAddress.toUpperCase();\n+      if (isVerified) {\n+        // generate auth token\n+        ctx.status = 200;\n+        ctx.body = {\n+          data: {\n+            authToken: this.authenticationUtils.buildAuthToken(userAddress),\n+          },\n+        };\n+        ctx.type = \'application/vnd.api+json\';\n+        return;\n+      }\n+    } catch (e) {\n+      console.debug(\'Failure recovering address to verify ownership. Session will not be established.\', e);\n+    }\n+    ctx.status = 401;\n     ctx.body = {\n-      authToken: \'\',\n+      error: \'Signature not verified\',\n     };\n+    ctx.type = \'application/vnd.api+json\';\n+  }\n+}\n+\n+declare module \'@cardstack/hub/dependency-injection\' {\n+  interface KnownServices {\n+    \'session-route\': SessionRoute;\n   }\n }\ndiff --git a/packages/hub/utils/authentication.ts b/packages/hub/utils/authentication.ts\nnew file mode 100644\nindex 000000000..a86c7160e\n--- /dev/null\n+++ b/packages/hub/utils/authentication.ts\n@@ -0,0 +1,72 @@\n+import { createCipheriv, createDecipheriv, createHmac, randomBytes } from \'crypto\';\n+\n+const SECRET = process.env.SERVER_SECRET as string;\n+const encryptionAlgorithm = \'aes-256-gcm\';\n+const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24;\n+\n+function encrypt(plaintext: string): string {\n+  const key = Buffer.from(SECRET, \'base64\');\n+  const iv = randomBytes(16);\n+  const cipher = createCipheriv(encryptionAlgorithm, key, iv);\n+  const encrypted = [\n+    iv.toString(\'hex\'),\n+    \'--\',\n+    cipher.update(plaintext, \'utf8\', \'hex\'),\n+    cipher.final(\'hex\'),\n+    \'--\',\n+    cipher.getAuthTag().toString(\'hex\'),\n+  ];\n+  return encrypted.join(\'\');\n+}\n+\n+function decrypt(cipherText: string) {\n+  const [ivString, encryptedString, authTagString] = cipherText.split(\'--\');\n+  const iv = Buffer.from(ivString, \'hex\');\n+  const key = Buffer.from(SECRET, \'base64\');\n+  const decipher = createDecipheriv(encryptionAlgorithm, key, iv);\n+  decipher.setAuthTag(Buffer.from(authTagString, \'hex\'));\n+  let decrypted = decipher.update(encryptedString, \'hex\', \'utf8\') + decipher.final(\'utf8\');\n+  return decrypted;\n+}\n+\n+if (!SECRET) {\n+  throw new Error(\'Missing ENV var SERVER_SECRET\');\n+}\n+\n+export class AuthenticationUtils {\n+  generateNonce(): string {\n+    let timestampInNanoseconds = process.hrtime.bigint().toString();\n+    let hmac = createHmac(\'sha256\', SECRET);\n+    let nonceSignature = hmac.update(`${timestampInNanoseconds}:${SECRET}`).digest(\'hex\');\n+    let nonce = `${Buffer.from(timestampInNanoseconds).toString(\'base64\')}:${nonceSignature}`;\n+    return nonce;\n+  }\n+\n+  extractVerifiedTimestamp(nonce: string): bigint {\n+    let [base64Timestamp, signature] = nonce.split(\':\');\n+    let timestamp = Buffer.from(base64Timestamp, \'base64\').toString(\'ascii\');\n+    let hmac = createHmac(\'sha256\', SECRET);\n+    let expectedNonceSignature = hmac.update(`${timestamp}:${SECRET}`).digest(\'hex\');\n+    if (expectedNonceSignature === signature) {\n+      return BigInt(timestamp);\n+    } else {\n+      throw new Error(\'Invalid signature\');\n+    }\n+  }\n+\n+  buildAuthToken(userAddress: string) {\n+    let timestamp = new Date(Date.now() + ONE_DAY_IN_MS).toISOString();\n+    let token = `current_user_id=${userAddress}&expires_at=${timestamp}`;\n+    return encrypt(token);\n+  }\n+\n+  decryptAuthToken(encryptedAuthToken: string) {\n+    return decrypt(encryptedAuthToken);\n+  }\n+}\n+\n+declare module \'@cardstack/hub/dependency-injection\' {\n+  interface KnownServices {\n+    \'authentication-utils\': AuthenticationUtils;\n+  }\n+}\ndiff --git a/packages/hub/utils/session.ts b/packages/hub/utils/session.ts\ndeleted file mode 100644\nindex 78cec96d2..000000000\n--- a/packages/hub/utils/session.ts\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-import { createHmac } from \'crypto\';\n-\n-const SECRET = process.env.SERVER_SECRET as string;\n-\n-if (!SECRET) {\n-  throw new Error(\'Missing ENV var SERVER_SECRET\');\n-}\n-\n-export class NonceGenerator {\n-  generate(): string {\n-    let timestampInNanoseconds = process.hrtime.bigint().toString();\n-    let hmac = createHmac(\'sha256\', SECRET);\n-    let nonceSignature = hmac.update(`${timestampInNanoseconds}:${SECRET}`).digest(\'hex\');\n-    let nonce = `${Buffer.from(timestampInNanoseconds).toString(\'base64\')}:${nonceSignature}`;\n-    return nonce;\n-  }\n-\n-  extractVerifiedTimestamp(nonce: string): bigint {\n-    let [base64Timestamp, signature] = nonce.split(\':\');\n-    let timestamp = Buffer.from(base64Timestamp, \'base64\').toString(\'ascii\');\n-    let hmac = createHmac(\'sha256\', SECRET);\n-    let expectedNonceSignature = hmac.update(`${timestamp}:${SECRET}`).digest(\'hex\');\n-    if (expectedNonceSignature === signature) {\n-      return BigInt(timestamp);\n-    } else {\n-      throw new Error(\'Invalid signature\');\n-    }\n-  }\n-}\n-\n-declare module \'@cardstack/hub/dependency-injection\' {\n-  interface KnownServices {\n-    \'nonce-generator\': NonceGenerator;\n-  }\n-}\ndiff --git a/packages/test-support/node-test-runner.ts b/packages/test-support/node-test-runner.ts\nindex 8d4be8a7c..0e5dd41f6 100644\n--- a/packages/test-support/node-test-runner.ts\n+++ b/packages/test-support/node-test-runner.ts\n@@ -4,7 +4,8 @@ import requireUncached from \'require-uncached\';\n import prepare from \'./prepare-node-tests\';\n \n export default function () {\n-  process.env.SERVER_SECRET = \'test123abc\';\n+  process.env.SERVER_SECRET = \'2Lhrsi7xSDMv1agfW+hghvQkdkTRSqW/JGApSjLT0NA=\';\n+  // generated with `crypto.randomBytes(32).toString(\'base64\')`\n \n   let patterns = [\'packages/*/node-tests/**/*-test.js\', \'node-tests/**/*-test.js\'];\n \n', 'commit_subject': 'Add building of auth token', 'repo_name': 'cardstack/cardstack', 'committer': {'name': 'Luke Melia', 'email': '6b3799be4300e44489a08090123f3842e6419da5@lukemelia.com', 'time_sec': '1622120347', 'tz_offset': '480', 'date': {'seconds': '1622120347'}}, 'commit_tree': '02b1c4bb0f8d4197041b3b9f3f8be25e9bcc3aca', 'commit_message': 'Add building of auth token\n', 'language_name': 'Shell', 'license': 'mit', 'hunks': "@@ -0,0 +1,72 @@\n+import { createCipheriv, createDecipheriv, createHmac, randomBytes } from 'crypto';\n+\n+const SECRET = process.env.SERVER_SECRET as string;\n+const encryptionAlgorithm = 'aes-256-gcm';\n+const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24;\n+\n+function encrypt(plaintext: string): string {\n+  const key = Buffer.from(SECRET, 'base64');\n+  const iv = randomBytes(16);\n+  const cipher = createCipheriv(encryptionAlgorithm, key, iv);\n+  const encrypted = [\n+    iv.toString('hex'),\n+    '--',\n+    cipher.update(plaintext, 'utf8', 'hex'),\n+    cipher.final('hex'),\n+    '--',\n+    cipher.getAuthTag().toString('hex'),\n+  ];\n+  return encrypted.join('');\n+}\n+\n+function decrypt(cipherText: string) {\n+  const [ivString, encryptedString, authTagString] = cipherText.split('--');\n+  const iv = Buffer.from(ivString, 'hex');\n+  const key = Buffer.from(SECRET, 'base64');\n+  const decipher = createDecipheriv(encryptionAlgorithm, key, iv);\n+  decipher.setAuthTag(Buffer.from(authTagString, 'hex'));\n+  let decrypted = decipher.update(encryptedString, 'hex', 'utf8') + decipher.final('utf8');\n+  return decrypted;\n+}\n+\n+if (!SECRET) {\n+  throw new Error('Missing ENV var SERVER_SECRET');\n+}\n+\n+export class AuthenticationUtils {\n+  generateNonce(): string {\n+    let timestampInNanoseconds = process.hrtime.bigint().toString();\n+    let hmac = createHmac('sha256', SECRET);\n+    let nonceSignature = hmac.update(`${timestampInNanoseconds}:${SECRET}`).digest('hex');\n+    let nonce = `${Buffer.from(timestampInNanoseconds).toString('base64')}:${nonceSignature}`;\n+    return nonce;\n+  }\n+\n+  extractVerifiedTimestamp(nonce: string): bigint {\n+    let [base64Timestamp, signature] = nonce.split(':');\n+    let timestamp = Buffer.from(base64Timestamp, 'base64').toString('ascii');\n+    let hmac = createHmac('sha256', SECRET);\n+    let expectedNonceSignature = hmac.update(`${timestamp}:${SECRET}`).digest('hex');\n+    if (expectedNonceSignature === signature) {\n+      return BigInt(timestamp);\n+    } else {\n+      throw new Error('Invalid signature');\n+    }\n+  }\n+\n+  buildAuthToken(userAddress: string) {\n+    let timestamp = new Date(Date.now() + ONE_DAY_IN_MS).toISOString();\n+    let token = `current_user_id=${userAddress}&expires_at=${timestamp}`;\n+    return encrypt(token);\n+  }\n+\n+  decryptAuthToken(encryptedAuthToken: string) {\n+    return decrypt(encryptedAuthToken);\n+  }\n+}\n+\n+declare module '@cardstack/hub/dependency-injection' {\n+  interface KnownServices {\n+    'authentication-utils': AuthenticationUtils;\n+  }\n+}\n", 'addition_count': 72, 'deletion_count': 0, 'src_file': '/dev/null', 'tgt_file': 'b/packages/hub/utils/authentication.ts', 'file_extension': '.ts', 'before_file': 'ADDFILE', 'message': 'Add building of auth token\n', 'author_name': 'Luke Melia', 'lang': 'ts'}