Fix infinite redirect loop when multiple cookies are sent (#50452)
Cookies are now checked for attributes that match the current Kibana configuration. Invalid cookies are cleared more reliably.
This commit is contained in:
parent
c790ef277c
commit
7830946f6a
|
@ -115,6 +115,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. |
|
||||
| [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | |
|
||||
| [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | |
|
||||
| [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) | Return type from a function to validate cookie contents. |
|
||||
| [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. |
|
||||
| [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. |
|
||||
| [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) > [isValid](./kibana-plugin-server.sessioncookievalidationresult.isvalid.md)
|
||||
|
||||
## SessionCookieValidationResult.isValid property
|
||||
|
||||
Whether the cookie is valid or not.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isValid: boolean;
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md)
|
||||
|
||||
## SessionCookieValidationResult interface
|
||||
|
||||
Return type from a function to validate cookie contents.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SessionCookieValidationResult
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [isValid](./kibana-plugin-server.sessioncookievalidationresult.isvalid.md) | <code>boolean</code> | Whether the cookie is valid or not. |
|
||||
| [path](./kibana-plugin-server.sessioncookievalidationresult.path.md) | <code>string</code> | The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) > [path](./kibana-plugin-server.sessioncookievalidationresult.path.md)
|
||||
|
||||
## SessionCookieValidationResult.path property
|
||||
|
||||
The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
path?: string;
|
||||
```
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## SessionStorageCookieOptions.encryptionKey property
|
||||
|
||||
A key used to encrypt a cookie value. Should be at least 32 characters long.
|
||||
A key used to encrypt a cookie's value. Should be at least 32 characters long.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ export interface SessionStorageCookieOptions<T>
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) | <code>string</code> | A key used to encrypt a cookie value. Should be at least 32 characters long. |
|
||||
| [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) | <code>string</code> | A key used to encrypt a cookie's value. Should be at least 32 characters long. |
|
||||
| [isSecure](./kibana-plugin-server.sessionstoragecookieoptions.issecure.md) | <code>boolean</code> | Flag indicating whether the cookie should be sent only via a secure connection. |
|
||||
| [name](./kibana-plugin-server.sessionstoragecookieoptions.name.md) | <code>string</code> | Name of the session cookie. |
|
||||
| [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) | <code>(sessionValue: T) => boolean | Promise<boolean></code> | Function called to validate a cookie content. |
|
||||
| [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) | <code>(sessionValue: T | T[]) => SessionCookieValidationResult</code> | Function called to validate a cookie's decrypted value. |
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
## SessionStorageCookieOptions.validate property
|
||||
|
||||
Function called to validate a cookie content.
|
||||
Function called to validate a cookie's decrypted value.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
validate: (sessionValue: T) => boolean | Promise<boolean>;
|
||||
validate: (sessionValue: T | T[]) => SessionCookieValidationResult;
|
||||
```
|
||||
|
|
|
@ -34,19 +34,34 @@ export interface SessionStorageCookieOptions<T> {
|
|||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A key used to encrypt a cookie value. Should be at least 32 characters long.
|
||||
* A key used to encrypt a cookie's value. Should be at least 32 characters long.
|
||||
*/
|
||||
encryptionKey: string;
|
||||
/**
|
||||
* Function called to validate a cookie content.
|
||||
* Function called to validate a cookie's decrypted value.
|
||||
*/
|
||||
validate: (sessionValue: T) => boolean | Promise<boolean>;
|
||||
validate: (sessionValue: T | T[]) => SessionCookieValidationResult;
|
||||
/**
|
||||
* Flag indicating whether the cookie should be sent only via a secure connection.
|
||||
*/
|
||||
isSecure: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type from a function to validate cookie contents.
|
||||
* @public
|
||||
*/
|
||||
export interface SessionCookieValidationResult {
|
||||
/**
|
||||
* Whether the cookie is valid or not.
|
||||
*/
|
||||
isValid: boolean;
|
||||
/**
|
||||
* The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it.
|
||||
*/
|
||||
path?: string;
|
||||
}
|
||||
|
||||
class ScopedCookieSessionStorage<T extends Record<string, any>> implements SessionStorage<T> {
|
||||
constructor(
|
||||
private readonly log: Logger,
|
||||
|
@ -98,15 +113,31 @@ export async function createCookieSessionStorageFactory<T>(
|
|||
cookieOptions: SessionStorageCookieOptions<T>,
|
||||
basePath?: string
|
||||
): Promise<SessionStorageFactory<T>> {
|
||||
function clearInvalidCookie(req: Request | undefined, path: string = basePath || '/') {
|
||||
// if the cookie did not include the 'path' attribute in the session value, it is a legacy cookie
|
||||
// we will assume that the cookie was created with the current configuration
|
||||
log.debug(`Clearing invalid session cookie`);
|
||||
// need to use Hapi toolkit to clear cookie with defined options
|
||||
if (req) {
|
||||
(req.cookieAuth as any).h.unstate(cookieOptions.name, { path });
|
||||
}
|
||||
}
|
||||
|
||||
await server.register({ plugin: hapiAuthCookie });
|
||||
|
||||
server.auth.strategy('security-cookie', 'cookie', {
|
||||
cookie: cookieOptions.name,
|
||||
password: cookieOptions.encryptionKey,
|
||||
validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }),
|
||||
validateFunc: async (req, session: T | T[]) => {
|
||||
const result = cookieOptions.validate(session);
|
||||
if (!result.isValid) {
|
||||
clearInvalidCookie(req, result.path);
|
||||
}
|
||||
return { valid: result.isValid };
|
||||
},
|
||||
isSecure: cookieOptions.isSecure,
|
||||
path: basePath,
|
||||
clearInvalid: true,
|
||||
clearInvalid: false,
|
||||
isHttpOnly: true,
|
||||
isSameSite: false,
|
||||
});
|
||||
|
|
|
@ -80,6 +80,7 @@ interface User {
|
|||
interface Storage {
|
||||
value: User;
|
||||
expires: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function retrieveSessionCookie(cookies: string) {
|
||||
|
@ -92,13 +93,21 @@ function retrieveSessionCookie(cookies: string) {
|
|||
|
||||
const userData = { id: '42' };
|
||||
const sessionDurationMs = 1000;
|
||||
const path = '/';
|
||||
const sessVal = () => ({ value: userData, expires: Date.now() + sessionDurationMs, path });
|
||||
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: (session: Storage) => session.expires > Date.now(),
|
||||
validate: (session: Storage | Storage[]) => {
|
||||
if (Array.isArray(session)) {
|
||||
session = session[0];
|
||||
}
|
||||
const isValid = session.path === path && session.expires > Date.now();
|
||||
return { isValid, path: session.path };
|
||||
},
|
||||
isSecure: false,
|
||||
path: '/',
|
||||
path,
|
||||
};
|
||||
|
||||
describe('Cookie based SessionStorage', () => {
|
||||
|
@ -107,9 +116,9 @@ describe('Cookie based SessionStorage', () => {
|
|||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
const router = createRouter('');
|
||||
|
||||
router.get({ path: '/', validate: false }, (context, req, res) => {
|
||||
router.get({ path, validate: false }, (context, req, res) => {
|
||||
const sessionStorage = factory.asScoped(req);
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
sessionStorage.set(sessVal());
|
||||
return res.ok({});
|
||||
});
|
||||
|
||||
|
@ -136,6 +145,7 @@ describe('Cookie based SessionStorage', () => {
|
|||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get()', () => {
|
||||
it('reads from session storage', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
|
@ -145,7 +155,7 @@ describe('Cookie based SessionStorage', () => {
|
|||
const sessionStorage = factory.asScoped(req);
|
||||
const sessionValue = await sessionStorage.get();
|
||||
if (!sessionValue) {
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
sessionStorage.set(sessVal());
|
||||
return res.ok();
|
||||
}
|
||||
return res.ok({ body: { value: sessionValue.value } });
|
||||
|
@ -173,6 +183,7 @@ describe('Cookie based SessionStorage', () => {
|
|||
.set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`)
|
||||
.expect(200, { value: userData });
|
||||
});
|
||||
|
||||
it('returns null for empty session', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
|
||||
|
@ -198,7 +209,7 @@ describe('Cookie based SessionStorage', () => {
|
|||
expect(cookies).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('returns null for invalid session & clean cookies', async () => {
|
||||
it('returns null for invalid session (expired) & clean cookies', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
|
||||
const router = createRouter('');
|
||||
|
@ -208,7 +219,7 @@ describe('Cookie based SessionStorage', () => {
|
|||
const sessionStorage = factory.asScoped(req);
|
||||
if (!setOnce) {
|
||||
setOnce = true;
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
sessionStorage.set(sessVal());
|
||||
return res.ok({ body: { value: userData } });
|
||||
}
|
||||
const sessionValue = await sessionStorage.get();
|
||||
|
@ -242,6 +253,50 @@ describe('Cookie based SessionStorage', () => {
|
|||
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns null for invalid session (incorrect path) & clean cookies accurately', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
|
||||
const router = createRouter('');
|
||||
|
||||
let setOnce = false;
|
||||
router.get({ path: '/', validate: false }, async (context, req, res) => {
|
||||
const sessionStorage = factory.asScoped(req);
|
||||
if (!setOnce) {
|
||||
setOnce = true;
|
||||
sessionStorage.set({ ...sessVal(), path: '/foo' });
|
||||
return res.ok({ body: { value: userData } });
|
||||
}
|
||||
const sessionValue = await sessionStorage.get();
|
||||
return res.ok({ body: { value: sessionValue } });
|
||||
});
|
||||
|
||||
const factory = await createCookieSessionStorageFactory(
|
||||
logger.get(),
|
||||
innerServer,
|
||||
cookieOptions
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { value: userData });
|
||||
|
||||
const cookies = response.get('set-cookie');
|
||||
expect(cookies).toBeDefined();
|
||||
|
||||
const sessionCookie = retrieveSessionCookie(cookies[0]);
|
||||
const response2 = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`)
|
||||
.expect(200, { value: null });
|
||||
|
||||
const cookies2 = response2.get('set-cookie');
|
||||
expect(cookies2).toEqual([
|
||||
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/foo',
|
||||
]);
|
||||
});
|
||||
|
||||
// use mocks to simplify test setup
|
||||
it('returns null if multiple session cookies are detected.', async () => {
|
||||
const mockServer = {
|
||||
|
@ -342,7 +397,7 @@ describe('Cookie based SessionStorage', () => {
|
|||
sessionStorage.clear();
|
||||
return res.ok({});
|
||||
}
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
sessionStorage.set(sessVal());
|
||||
return res.ok({});
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import { HttpServer } from './http_server';
|
|||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: () => true,
|
||||
validate: () => ({ isValid: true }),
|
||||
isSecure: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -60,6 +60,9 @@ export {
|
|||
} from './lifecycle/auth';
|
||||
export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth';
|
||||
export { SessionStorageFactory, SessionStorage } from './session_storage';
|
||||
export { SessionStorageCookieOptions } from './cookie_session_storage';
|
||||
export {
|
||||
SessionStorageCookieOptions,
|
||||
SessionCookieValidationResult,
|
||||
} from './cookie_session_storage';
|
||||
export * from './types';
|
||||
export { BasePath, IBasePath } from './base_path_service';
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('http service', () => {
|
|||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: (session: StorageData) => true,
|
||||
validate: () => ({ isValid: true }),
|
||||
isSecure: false,
|
||||
path: '/',
|
||||
};
|
||||
|
|
|
@ -408,7 +408,7 @@ describe('Auth', () => {
|
|||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: () => true,
|
||||
validate: () => ({ isValid: true }),
|
||||
isSecure: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -117,6 +117,7 @@ export {
|
|||
RouteRegistrar,
|
||||
SessionStorage,
|
||||
SessionStorageCookieOptions,
|
||||
SessionCookieValidationResult,
|
||||
SessionStorageFactory,
|
||||
} from './http';
|
||||
export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
|
||||
|
|
|
@ -1577,6 +1577,12 @@ export class ScopedClusterClient implements IScopedClusterClient {
|
|||
callAsInternalUser(endpoint: string, clientParams?: Record<string, any>, options?: CallAPIOptions): Promise<any>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SessionCookieValidationResult {
|
||||
isValid: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SessionStorage<T> {
|
||||
clear(): void;
|
||||
|
@ -1589,7 +1595,7 @@ export interface SessionStorageCookieOptions<T> {
|
|||
encryptionKey: string;
|
||||
isSecure: boolean;
|
||||
name: string;
|
||||
validate: (sessionValue: T) => boolean | Promise<boolean>;
|
||||
validate: (sessionValue: T | T[]) => SessionCookieValidationResult;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
|
|
@ -78,12 +78,20 @@ describe('Authenticator', () => {
|
|||
let authenticator: Authenticator;
|
||||
let mockOptions: ReturnType<typeof getMockOptions>;
|
||||
let mockSessionStorage: jest.Mocked<SessionStorage<ProviderSession>>;
|
||||
let mockSessVal: any;
|
||||
beforeEach(() => {
|
||||
mockOptions = getMockOptions({
|
||||
authc: { providers: ['basic'], oidc: {}, saml: {} },
|
||||
});
|
||||
mockSessionStorage = sessionStorageMock.create();
|
||||
mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage);
|
||||
mockSessVal = {
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic',
|
||||
path: mockOptions.basePath.serverBasePath,
|
||||
};
|
||||
|
||||
authenticator = new Authenticator(mockOptions);
|
||||
});
|
||||
|
@ -159,10 +167,8 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
...mockSessVal,
|
||||
state: { authorization },
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -176,18 +182,12 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('clears session if it belongs to a different provider.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const user = mockAuthenticatedUser();
|
||||
const credentials = { username: 'user', password: 'password' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user));
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'token',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' });
|
||||
|
||||
const authenticationResult = await authenticator.login(request, {
|
||||
provider: 'basic',
|
||||
|
@ -299,12 +299,20 @@ describe('Authenticator', () => {
|
|||
let authenticator: Authenticator;
|
||||
let mockOptions: ReturnType<typeof getMockOptions>;
|
||||
let mockSessionStorage: jest.Mocked<SessionStorage<ProviderSession>>;
|
||||
let mockSessVal: any;
|
||||
beforeEach(() => {
|
||||
mockOptions = getMockOptions({
|
||||
authc: { providers: ['basic'], oidc: {}, saml: {} },
|
||||
});
|
||||
mockSessionStorage = sessionStorageMock.create<ProviderSession>();
|
||||
mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage);
|
||||
mockSessVal = {
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic',
|
||||
path: mockOptions.basePath.serverBasePath,
|
||||
};
|
||||
|
||||
authenticator = new Authenticator(mockOptions);
|
||||
});
|
||||
|
@ -360,10 +368,8 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
...mockSessVal,
|
||||
state: { authorization },
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -383,28 +389,20 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
...mockSessVal,
|
||||
state: { authorization },
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not extend session for system API calls.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(true);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.succeeded(user)
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
|
@ -416,37 +414,25 @@ describe('Authenticator', () => {
|
|||
|
||||
it('extends session for non-system API calls.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(false);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.succeeded(user)
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual(user);
|
||||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith(mockSessVal);
|
||||
expect(mockSessionStorage.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('properly extends session expiration if it is defined.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf();
|
||||
|
||||
|
@ -460,12 +446,7 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
mockSessionStorage = sessionStorageMock.create();
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage);
|
||||
|
||||
authenticator = new Authenticator(mockOptions);
|
||||
|
@ -482,17 +463,14 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
...mockSessVal,
|
||||
idleTimeoutExpiration: currentDate + 3600 * 24,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
expect(mockSessionStorage.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not extend session lifespan expiration.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf();
|
||||
const hr = 1000 * 60 * 60;
|
||||
|
@ -508,12 +486,11 @@ describe('Authenticator', () => {
|
|||
|
||||
mockSessionStorage = sessionStorageMock.create();
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
...mockSessVal,
|
||||
// this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan)
|
||||
// it was last extended 1 hour ago, which means it will expire in 1 hour
|
||||
idleTimeoutExpiration: currentDate + hr * 1,
|
||||
lifespanExpiration: currentDate + hr * 1.5,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage);
|
||||
|
||||
|
@ -531,17 +508,15 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
...mockSessVal,
|
||||
idleTimeoutExpiration: currentDate + hr * 2,
|
||||
lifespanExpiration: currentDate + hr * 1.5,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
expect(mockSessionStorage.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('only updates the session lifespan expiration if it does not match the current server config.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const hr = 1000 * 60 * 60;
|
||||
|
||||
|
@ -560,10 +535,9 @@ describe('Authenticator', () => {
|
|||
|
||||
mockSessionStorage = sessionStorageMock.create();
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
...mockSessVal,
|
||||
idleTimeoutExpiration: 1,
|
||||
lifespanExpiration: oldExpiration,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage);
|
||||
|
||||
|
@ -579,10 +553,9 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
...mockSessVal,
|
||||
idleTimeoutExpiration: 1,
|
||||
lifespanExpiration: newExpiration,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
expect(mockSessionStorage.clear).not.toHaveBeenCalled();
|
||||
}
|
||||
|
@ -595,19 +568,13 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(true);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.failed(new Error('some error'))
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
|
@ -617,19 +584,13 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(false);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.failed(new Error('some error'))
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
|
@ -640,7 +601,6 @@ describe('Authenticator', () => {
|
|||
|
||||
it('replaces existing session with the one returned by authentication provider for system API requests', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const existingState = { authorization: 'Basic xxx' };
|
||||
const newState = { authorization: 'Basic yyy' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
|
@ -648,12 +608,7 @@ describe('Authenticator', () => {
|
|||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.succeeded(user, { state: newState })
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state: existingState,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
|
@ -661,17 +616,14 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
...mockSessVal,
|
||||
state: newState,
|
||||
provider: 'basic',
|
||||
});
|
||||
expect(mockSessionStorage.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const existingState = { authorization: 'Basic xxx' };
|
||||
const newState = { authorization: 'Basic yyy' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
|
@ -679,12 +631,7 @@ describe('Authenticator', () => {
|
|||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.succeeded(user, { state: newState })
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state: existingState,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
|
@ -692,28 +639,20 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionStorage.set).toHaveBeenCalledWith({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
...mockSessVal,
|
||||
state: newState,
|
||||
provider: 'basic',
|
||||
});
|
||||
expect(mockSessionStorage.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(true);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.failed(Boom.unauthorized())
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
|
@ -723,19 +662,13 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('clears session if provider failed to authenticate non-system API request with 401 with active session.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(false);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.failed(Boom.unauthorized())
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
|
@ -745,18 +678,12 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('clears session if provider requested it via setting state to `null`.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.redirectTo('some-url', { state: null })
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
|
@ -766,19 +693,13 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('does not clear session if provider can not handle system API request authentication with active session.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(true);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.notHandled()
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
|
@ -788,19 +709,13 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('does not clear session if provider can not handle non-system API request authentication with active session.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(false);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.notHandled()
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
|
@ -810,19 +725,13 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('clears session for system API request if it belongs to not configured provider.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(true);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.notHandled()
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'token',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' });
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
|
@ -832,19 +741,13 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('clears session for non-system API request if it belongs to not configured provider.', async () => {
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
mockOptions.isSystemAPIRequest.mockReturnValue(false);
|
||||
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
||||
AuthenticationResult.notHandled()
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'token',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' });
|
||||
|
||||
const authenticationResult = await authenticator.authenticate(request);
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
|
@ -858,12 +761,20 @@ describe('Authenticator', () => {
|
|||
let authenticator: Authenticator;
|
||||
let mockOptions: ReturnType<typeof getMockOptions>;
|
||||
let mockSessionStorage: jest.Mocked<SessionStorage<ProviderSession>>;
|
||||
let mockSessVal: any;
|
||||
beforeEach(() => {
|
||||
mockOptions = getMockOptions({
|
||||
authc: { providers: ['basic'], oidc: {}, saml: {} },
|
||||
});
|
||||
mockSessionStorage = sessionStorageMock.create();
|
||||
mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage);
|
||||
mockSessVal = {
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic',
|
||||
path: mockOptions.basePath.serverBasePath,
|
||||
};
|
||||
|
||||
authenticator = new Authenticator(mockOptions);
|
||||
});
|
||||
|
@ -886,16 +797,10 @@ describe('Authenticator', () => {
|
|||
|
||||
it('clears session and returns whatever authentication provider returns.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const state = { authorization: 'Basic xxx' };
|
||||
mockBasicAuthenticationProvider.logout.mockResolvedValue(
|
||||
DeauthenticationResult.redirectTo('some-url')
|
||||
);
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'basic',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue(mockSessVal);
|
||||
|
||||
const deauthenticationResult = await authenticator.logout(request);
|
||||
|
||||
|
@ -934,12 +839,7 @@ describe('Authenticator', () => {
|
|||
it('only clears session if it belongs to not configured provider.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const state = { authorization: 'Bearer xxx' };
|
||||
mockSessionStorage.get.mockResolvedValue({
|
||||
idleTimeoutExpiration: null,
|
||||
lifespanExpiration: null,
|
||||
state,
|
||||
provider: 'token',
|
||||
});
|
||||
mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, state, provider: 'token' });
|
||||
|
||||
const deauthenticationResult = await authenticator.logout(request);
|
||||
|
||||
|
@ -978,6 +878,7 @@ describe('Authenticator', () => {
|
|||
lifespanExpiration: mockInfo.lifespanExpiration,
|
||||
state,
|
||||
provider: mockInfo.provider,
|
||||
path: mockOptions.basePath.serverBasePath,
|
||||
});
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => currentDate);
|
||||
|
||||
|
|
|
@ -59,6 +59,11 @@ export interface ProviderSession {
|
|||
* entirely determined by the authentication provider that owns the current session.
|
||||
*/
|
||||
state: unknown;
|
||||
|
||||
/**
|
||||
* Cookie "Path" attribute that is validated against the current Kibana server configuration.
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,6 +164,11 @@ export class Authenticator {
|
|||
*/
|
||||
private readonly providers: Map<string, BaseAuthenticationProvider>;
|
||||
|
||||
/**
|
||||
* Which base path the HTTP server is hosted on.
|
||||
*/
|
||||
private readonly serverBasePath: string;
|
||||
|
||||
/**
|
||||
* Session timeout in ms. If `null` session will stay active until the browser is closed.
|
||||
*/
|
||||
|
@ -213,6 +223,7 @@ export class Authenticator {
|
|||
] as [string, BaseAuthenticationProvider];
|
||||
})
|
||||
);
|
||||
this.serverBasePath = this.options.basePath.serverBasePath || '/';
|
||||
|
||||
// only set these vars if they are defined in options (otherwise coalesce to existing/default)
|
||||
this.idleTimeout = this.options.config.session.idleTimeout;
|
||||
|
@ -277,6 +288,7 @@ export class Authenticator {
|
|||
provider: attempt.provider,
|
||||
idleTimeoutExpiration,
|
||||
lifespanExpiration,
|
||||
path: this.serverBasePath,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -465,6 +477,7 @@ export class Authenticator {
|
|||
provider: providerType,
|
||||
idleTimeoutExpiration,
|
||||
lifespanExpiration,
|
||||
path: this.serverBasePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,22 @@ export async function setupAuthentication({
|
|||
.callAsCurrentUser('shield.authenticate')) as AuthenticatedUser;
|
||||
};
|
||||
|
||||
const isValid = (sessionValue: ProviderSession) => {
|
||||
// ensure that this cookie was created with the current Kibana configuration
|
||||
const { path, idleTimeoutExpiration, lifespanExpiration } = sessionValue;
|
||||
if (path !== undefined && path !== (http.basePath.serverBasePath || '/')) {
|
||||
authLogger.debug(`Outdated session value with path "${sessionValue.path}"`);
|
||||
return false;
|
||||
}
|
||||
// ensure that this cookie is not expired
|
||||
if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) {
|
||||
return false;
|
||||
} else if (lifespanExpiration && lifespanExpiration < Date.now()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const authenticator = new Authenticator({
|
||||
clusterClient,
|
||||
basePath: http.basePath,
|
||||
|
@ -75,14 +91,14 @@ export async function setupAuthentication({
|
|||
encryptionKey: config.encryptionKey,
|
||||
isSecure: config.secureCookies,
|
||||
name: config.cookieName,
|
||||
validate: (sessionValue: ProviderSession) => {
|
||||
const { idleTimeoutExpiration, lifespanExpiration } = sessionValue;
|
||||
if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) {
|
||||
return false;
|
||||
} else if (lifespanExpiration && lifespanExpiration < Date.now()) {
|
||||
return false;
|
||||
validate: (session: ProviderSession | ProviderSession[]) => {
|
||||
const array: ProviderSession[] = Array.isArray(session) ? session : [session];
|
||||
for (const sess of array) {
|
||||
if (!isValid(sess)) {
|
||||
return { isValid: false, path: sess.path };
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return { isValid: true };
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue