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
11 changes: 9 additions & 2 deletions src/utils/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ export async function setUser(userId, expiration, reqHeaders, env) {
orgs,
});

await env.DA_AUTH.put(userId, value, { expiration });
try {
await env.DA_AUTH.put(userId, value, { expiration });
} catch (e) {
// KV rejects expiration timestamps < 60s in the future (near-expiry tokens).
// Log and continue — user is still authenticated, just not cached.
// eslint-disable-next-line no-console
console.error('Failed to cache user in KV', e);
}
return value;
}

Expand Down Expand Up @@ -147,7 +154,7 @@ export async function getUsers(req, env) {
if (type !== 'access_token') return { email: 'anonymous' };

const expires = Number(createdAt) + Number(expiresIn);
const now = Math.floor(new Date().getTime() / 1000);
const now = Date.now();
Comment thread
kptdobe marked this conversation as resolved.

if (expires < now) return { email: 'anonymous' };
// Find the user in recent sessions
Expand Down
56 changes: 56 additions & 0 deletions test/utils/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,39 @@ describe('DA auth', () => {
assert.strictEqual(users[0].email, 'anonymous');
});

it('anonymous if token expired with realistic IMS ms-scale timestamps', async () => {
// IMS JWT fields: created_at and expires_in are in milliseconds.
// Bug: auth.js computes `now` in seconds; ms-scale `expires` is always larger,
// so expired tokens are never rejected.
// Token issued 2h ago, valid for only 1h — clearly expired.
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
const ONE_HOUR_MS = 60 * 60 * 1000;

const { getUsers: getUsersMsExpired } = await esmock('../../src/utils/auth.js', {
jose: {
createRemoteJWKSet: () => null,
jwksCache: 'cache-key',
jwtVerify: () => ({
payload: {
type: 'access_token',
user_id: 'user@example.com',
created_at: Date.now() - TWO_HOURS_MS,
expires_in: ONE_HOUR_MS,
},
}),
},
});

const req = new Request('https://da.live/source/cq/test', {
headers: new Headers({ Authorization: 'Bearer sometoken' }),
});

await withMockedFetch(async () => {
const users = await getUsersMsExpired(req, env);
assert.strictEqual(users[0].email, 'anonymous');
});
});

it('authorized if email matches', async () => {
await withMockedFetch(async () => {
const users = await getUsers(reqs.site, env);
Expand Down Expand Up @@ -118,6 +151,29 @@ describe('DA auth', () => {
];
assert.deepStrictEqual(expectedOrgs, userValue.orgs);
});

it('returns user value when KV PUT fails for near-expiry token', async () => {
// Near-expiry tokens have < 60s remaining; KV rejects the expiration timestamp.
// Bug: the thrown error propagates up through getUsers -> getDaCtx -> 500.
// Fix: setUser must catch the KV PUT failure and still return the user value.
const headers = new Headers({ Authorization: 'Bearer aparker@geometrixx.info' });
const kvPutError = new Error(
'KV PUT failed: 400 Invalid expiration of 1777144621.'
+ ' Expiration times must be at least 60 seconds in the future.',
);
const failEnv = {
...env,
DA_AUTH: { ...env.DA_AUTH, put: () => { throw kvPutError; } },
};

let userValue;
await withMockedFetch(async () => {
const userValStr = await setUser('aparker@geometrixx.info', 100, headers, failEnv);
userValue = JSON.parse(userValStr);
});

assert.strictEqual(userValue.email, 'aparker@geometrixx.info');
});
});

describe('path authorization', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/utils/mocks/jose.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
const jwtVerify = (token) => {
// eslint-disable-next-line prefer-const
let [email, created_at = 0, expires_in = 0] = token.split(':');
created_at += Math.floor(new Date().getTime() / 1000);
created_at += new Date().getTime();
expires_in += created_at;
return {
payload: {
Expand Down
4 changes: 2 additions & 2 deletions test/utils/offlineValidation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ async function generateToken(kid, privateKey) {
return new SignJWT({
user_id: 'mocked_example_com',
type: 'access_token',
created_at: Date.now() / 1000,
expires_in: 60,
created_at: Date.now(),
expires_in: 60000,
})
.setProtectedHeader({
alg: 'RS256',
Expand Down
Loading