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 privateKey = crypto.createPrivateKey(await readPrivateKey());
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({
cookieKeys,
privateKey,
publicKey,
issuer,
refreshTokenReuseInterval: Number(refreshTokenReuseInterval),
defaultIdTokenTtl: 60 * 60,
defaultRefreshTokenTtl: 14 * 24 * 60 * 60,
});

View file

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