Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@hellocoop/mockin",
"private": false,
"version": "3.1.0",
"version": "3.1.1",
"description": "Hellō Mock Login OpenID Connect Server",
"engines": {
"node": ">=22"
Expand Down
10 changes: 9 additions & 1 deletion src/aauth/issue-auth-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,15 @@ export async function issueAuthToken({
const cfg = getConfig()
const iat = Math.floor(Date.now() / 1000)
let lifetime = Math.min(cfg.token_lifetime || MAX_AUTH_TOKEN_TTL, MAX_AUTH_TOKEN_TTL)
if (Number.isFinite(presented_exp)) {
if (Number.isFinite(presented_exp) && lifetime > 0) {
// Never outlive the presented token (-11 §Auth Token Structure), and
// never let a presented token that is nearly expired turn a positive
// lifetime into a negative one — one second is still a live token.
//
// The floor applies to the clamp only. A NEGATIVE `token_lifetime` is
// the mock switch for minting an already-expired token, which the
// challenge-on-401 suites depend on; flooring that to 1 would issue a
// valid token and quietly break them.
lifetime = Math.max(1, Math.min(lifetime, presented_exp - iat))
}
const { identity, resource } = classifyScopes(scope)
Expand Down
23 changes: 23 additions & 0 deletions test/aauth/token.identity.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,29 @@ describe('AAuth auth_token_endpoint — identity flow (no R3)', function () {
expect(claims.exp - claims.iat).to.equal(3600)
})

it('a negative token_lifetime still mints an expired token', async function () {
// The switch exists so a resource-side suite can drive its
// challenge-on-401 path. Clamping to the presented token's exp must
// not floor a deliberately negative lifetime up to 1s — that issues a
// live token and the challenge never fires.
// Mint the person token FIRST — token_lifetime shapes it too, and an
// expired presented token is a different (correct) 400.
const { agentToken, body } = await personAndResourceToken(fastify, {
resource: { scope: 'openid' },
})
await fastify.inject({
method: 'PUT',
url: '/mock/aauth',
headers: { 'content-type': 'application/json' },
payload: JSON.stringify({ token_lifetime: -60 }),
})
const response = await postAuthToken(fastify, { body, agentToken })
expect(response.statusCode).to.equal(200)
const claims = decodeJwt(response.json().auth_token)
expect(claims.exp - claims.iat).to.equal(-60)
expect(claims.exp).to.be.below(Math.floor(Date.now() / 1000))
})

it('never outlives the presented token (agent token 600s → auth token ≤ 600s)', async function () {
const { response, personClaims } = await postToken({
person: { agentToken: await mintAgentToken({ ttl: 600 }) },
Expand Down