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:
parent
708523ed52
commit
bb245adbb9
2 changed files with 33 additions and 8 deletions
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Loading…
Reference in a new issue