Skip to content

Commit

Permalink
Merge pull request #259 from nulib/5205-retrieve-user-token
Browse files Browse the repository at this point in the history
Add /auth/token route
  • Loading branch information
mbklein authored Aug 30, 2024
2 parents 983cfb2 + 5762efb commit 93cac31
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 0 deletions.
5 changes: 5 additions & 0 deletions node/src/api/api-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function emptyToken() {
isLoggedIn: false,
};
}

class ApiToken {
constructor(signedToken) {
if (signedToken) {
Expand Down Expand Up @@ -79,6 +80,10 @@ class ApiToken {
return this.update();
}

expireAt(dateTime) {
this.token.exp = Math.floor(Number(dateTime) / 1000);
}

update() {
this._updated = true;
return this;
Expand Down
51 changes: 51 additions & 0 deletions node/src/handlers/get-auth-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const { wrap } = require("./middleware");
const Honeybadger = require("../honeybadger-setup");

const DEFAULT_TTL = 86400; // one day
const MAX_TTL = DEFAULT_TTL * 7; // one week;

const makeError = (code, message) => {
return {
statusCode: code,
headers: {
"Content-Type": "text/plain",
},
body: message,
};
};

const present = (value) =>
value !== undefined && value !== null && value !== "";

/**
* Token - Returns auth token for current user with requested expiration
*
*/
exports.handler = wrap(async (event) => {
try {
const ttl = event.queryStringParameters?.ttl;
if (present(ttl) && ttl.match(/\D/)) {
return makeError(400, `'${ttl}' is not a valid value for ttl`);
}
const ttl_in_seconds = Number(ttl) || DEFAULT_TTL;
if (ttl_in_seconds > MAX_TTL) {
return makeError(400, `ttl cannot exceed ${MAX_TTL} seconds`);
}

const token = event.userToken;
const expiration = new Date(new Date().getTime() + ttl_in_seconds * 1000);
expiration.setMilliseconds(0);
token.expireAt(expiration);

return {
statusCode: 200,
body: JSON.stringify({
token: token.sign(),
expires: expiration.toISOString(),
}),
};
} catch (error) {
await Honeybadger.notifyAsync(error);
return makeError(401, "Error verifying API token: " + error.message);
}
});
136 changes: 136 additions & 0 deletions node/test/integration/get-auth-token.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use strict";

const chai = require("chai");
const expect = chai.expect;
const jwt = require("jsonwebtoken");

const getAuthTokenHandler = requireSource("handlers/get-auth-token");

// Utility functions to calculate the number of seconds or milliseconds from the epoch plus
// an offset in seconds, but with one-second resolution
const fromNowSeconds = (seconds) =>
Math.floor((new Date().getTime() + seconds * 1000) / 1000);

describe("auth token", function () {
helpers.saveEnvironment();
let payload;

beforeEach(() => {
payload = {
iss: "https://example.com",
sub: "user123",
name: "Some One",
exp: Math.floor(Number(new Date()) / 1000) + 12 * 60 * 60,
iat: Math.floor(Number(new Date()) / 1000),
email: "user@example.com",
};
});

it("works with anonymous users", async () => {
const event = helpers.mockEvent("GET", "/auth/token").render();

const expectedExpiration = fromNowSeconds(86400);
const result = await getAuthTokenHandler.handler(event);
expect(result.statusCode).to.eq(200);
const body = JSON.parse(result.body);

// Built-in Date will be in millis and our expiration is in seconds
expect(Date.parse(body.expires)).to.be.within(
(expectedExpiration - 1) * 1000,
(expectedExpiration + 1) * 1000
);

const resultToken = jwt.verify(body.token, process.env.API_TOKEN_SECRET);
expect(resultToken.exp).to.be.within(
expectedExpiration - 1,
expectedExpiration + 1
);
expect(resultToken.isLoggedIn).to.eq(false);
});

it("returns a token with a default ttl of 1 day", async () => {
const token = jwt.sign(payload, process.env.API_TOKEN_SECRET);
const event = helpers
.mockEvent("GET", "/auth/token")
.headers({
Cookie: `${process.env.API_TOKEN_NAME}=${token};`,
})
.render();

const expectedExpiration = fromNowSeconds(86400);
const result = await getAuthTokenHandler.handler(event);
expect(result.statusCode).to.eq(200);
const body = JSON.parse(result.body);

// Built-in Date will be in millis and our expiration is in seconds
expect(Date.parse(body.expires)).to.be.within(
(expectedExpiration - 1) * 1000,
(expectedExpiration + 1) * 1000
);

const resultToken = jwt.verify(body.token, process.env.API_TOKEN_SECRET);
expect(resultToken.exp).to.be.within(
expectedExpiration - 1,
expectedExpiration + 1
);
expect(resultToken.name).to.eq("Some One");
});

it("returns a token with the requested ttl", async () => {
const token = jwt.sign(payload, process.env.API_TOKEN_SECRET);
const ttl = 3600 * 18; // 18 hours
const event = helpers
.mockEvent("GET", "/auth/token")
.queryParams({ ttl: ttl.toString() })
.headers({
Cookie: `${process.env.API_TOKEN_NAME}=${token};`,
})
.render();

const expectedExpiration = fromNowSeconds(ttl);
const result = await getAuthTokenHandler.handler(event);
expect(result.statusCode).to.eq(200);
const body = JSON.parse(result.body);

// Built-in Date will be in millis and our expiration is in seconds
expect(Date.parse(body.expires)).to.be.within(
(expectedExpiration - 1) * 1000,
(expectedExpiration + 1) * 1000
);

const resultToken = jwt.verify(body.token, process.env.API_TOKEN_SECRET);
expect(resultToken.exp).to.be.within(
expectedExpiration - 1,
expectedExpiration + 1
);
expect(resultToken.name).to.eq("Some One");
});

it("rejects a request with a non-numeric ttl", async () => {
const token = jwt.sign(payload, process.env.API_TOKEN_SECRET);
const event = helpers
.mockEvent("GET", "/auth/token")
.queryParams({ ttl: "blargh" })
.headers({
Cookie: `${process.env.API_TOKEN_NAME}=${token};`,
})
.render();
const result = await getAuthTokenHandler.handler(event);
expect(result.statusCode).to.eq(400);
expect(result.body).to.eq("'blargh' is not a valid value for ttl");
});

it("rejects a request with a ttl that's too high", async () => {
const token = jwt.sign(payload, process.env.API_TOKEN_SECRET);
const event = helpers
.mockEvent("GET", "/auth/token")
.queryParams({ ttl: "864000" })
.headers({
Cookie: `${process.env.API_TOKEN_NAME}=${token};`,
})
.render();
const result = await getAuthTokenHandler.handler(event);
expect(result.statusCode).to.eq(400);
expect(result.body).to.eq("ttl cannot exceed 604800 seconds");
});
});
24 changes: 24 additions & 0 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,30 @@ Resources:
ApiId: !Ref dcApi
Path: /auth/logout
Method: GET
getAuthTokenFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handlers/get-auth-token.handler
Description: Function to retrieve raw JWT.
#* Layers:
#* - !Ref apiDependencies
Environment:
Variables:
NUSSO_API_KEY: !Ref NussoApiKey
NUSSO_BASE_URL: !Ref NussoBaseUrl
Events:
ApiGet:
Type: HttpApi
Properties:
ApiId: !Ref dcApi
Path: /auth/token
Method: GET
ApiHead:
Type: HttpApi
Properties:
ApiId: !Ref dcApi
Path: /auth/token
Method: HEAD
getAuthWhoAmIFunction:
Type: AWS::Serverless::Function
Properties:
Expand Down

0 comments on commit 93cac31

Please sign in to comment.