-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #259 from nulib/5205-retrieve-user-token
Add /auth/token route
- Loading branch information
Showing
4 changed files
with
216 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters