0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): refresh token rotation reuse interval (#1617)

* feat(core): refresh token rotation reuse interval

* refactor: apply suggestions from code review

Co-authored-by: Gao Sun <gao@silverhand.io>
This commit is contained in:
Wang Sijie 2022-07-20 15:50:39 +08:00 committed by GitHub
parent 708523ed52
commit bb245adbb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 33 additions and 8 deletions

View file

@ -112,12 +112,19 @@ const loadOidcValues = async (issuer: string) => {
const cookieKeys = await readCookieKeys(); const cookieKeys = await readCookieKeys();
const privateKey = crypto.createPrivateKey(await readPrivateKey()); const privateKey = crypto.createPrivateKey(await readPrivateKey());
const publicKey = crypto.createPublicKey(privateKey); const publicKey = crypto.createPublicKey(privateKey);
/**
* This interval helps to avoid concurrency issues when exchanging the rotating refresh token multiple times within a given timeframe.
* During the leeway window (in seconds), the consumed refresh token will be considered as valid.
* This is useful for distributed apps and serverless apps like Next.js, in which there is no shared memory.
*/
const refreshTokenReuseInterval = getEnv('OIDC_REFRESH_TOKEN_REUSE_INTERVAL', '3');
return Object.freeze({ return Object.freeze({
cookieKeys, cookieKeys,
privateKey, privateKey,
publicKey, publicKey,
issuer, issuer,
refreshTokenReuseInterval: Number(refreshTokenReuseInterval),
defaultIdTokenTtl: 60 * 60, defaultIdTokenTtl: 60 * 60,
defaultRefreshTokenTtl: 14 * 24 * 60 * 60, defaultRefreshTokenTtl: 14 * 24 * 60 * 60,
}); });

View file

@ -4,7 +4,8 @@ import {
OidcModelInstancePayload, OidcModelInstancePayload,
OidcModelInstances, OidcModelInstances,
} from '@logto/schemas'; } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional, Nullable } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { sql, ValueExpression } from 'slonik'; import { sql, ValueExpression } from 'slonik';
import { buildInsertInto } from '@/database/insert-into'; import { buildInsertInto } from '@/database/insert-into';
@ -16,15 +17,32 @@ export type QueryResult = Pick<OidcModelInstance, 'payload' | 'consumedAt'>;
const { table, fields } = convertToIdentifiers(OidcModelInstances); const { table, fields } = convertToIdentifiers(OidcModelInstances);
// eslint-disable-next-line @typescript-eslint/ban-types const isConsumed = (modelName: string, consumedAt: Nullable<number>): boolean => {
const withConsumed = <T>(data: T, consumedAt?: number | null): WithConsumed<T> => ({ if (!consumedAt) {
return false;
}
const { refreshTokenReuseInterval } = envSet.values.oidc;
if (modelName !== 'RefreshToken' || !refreshTokenReuseInterval) {
return Boolean(consumedAt);
}
return dayjs(consumedAt).add(refreshTokenReuseInterval, 'seconds').isBefore(dayjs());
};
const withConsumed = <T>(
data: T,
modelName: string,
consumedAt: Nullable<number>
): WithConsumed<T> => ({
...data, ...data,
...(consumedAt ? { consumed: true } : undefined), ...(isConsumed(modelName, consumedAt) ? { consumed: true } : undefined),
}); });
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
const convertResult = (result: QueryResult | null) => const convertResult = (result: QueryResult | null, modelName: string) =>
conditional(result && withConsumed(result.payload, result.consumedAt)); conditional(result && withConsumed(result.payload, modelName, result.consumedAt));
export const upsertInstance = buildInsertInto<CreateOidcModelInstance>(OidcModelInstances, { export const upsertInstance = buildInsertInto<CreateOidcModelInstance>(OidcModelInstances, {
onConflict: { onConflict: {
@ -45,7 +63,7 @@ export const findPayloadById = async (modelName: string, id: string) => {
and ${fields.id}=${id} and ${fields.id}=${id}
`); `);
return convertResult(result); return convertResult(result, modelName);
}; };
export const findPayloadByPayloadField = async < export const findPayloadByPayloadField = async <
@ -61,7 +79,7 @@ export const findPayloadByPayloadField = async <
and ${fields.payload}->>${field}=${value} and ${fields.payload}->>${field}=${value}
`); `);
return convertResult(result); return convertResult(result, modelName);
}; };
export const consumeInstanceById = async (modelName: string, id: string) => { export const consumeInstanceById = async (modelName: string, id: string) => {