Add an optional authentication mode for HTTP resources (#58589) (#59614)

* add authRequred: 'optional'

* expose auth status via request context

* update security plugin to use notHandled auth outcome

* capabilities service uses optional auth

* update tests

* attach security headers only to unauthorised response

* add isAuthenticated tests for 'optional' auth mode

* security plugin relies on http.auth.isAuthenticated to calc capabilities

* generate docs

* reword test suit names

* update tests

* update test checking isAuth on optional auth path

* address Oleg comments

* add test for auth: try

* fix

* pass isAuthenticted as boolean via context

* remove response header from notHandled

* update docs

* add redirected for auth interceptor

* security plugin uses t.redirected to be compat with auth: optional

* update docs

* require location header in the interface

* address comments #1

* declare isAuthenticated on KibanaRequest

* remove auth.isAuthenticated from scope

* update docs

* remove unnecessary comment

* do not fail on FakrRequest

* small improvements
This commit is contained in:
Mikhail Shustov 2020-03-07 20:15:48 +01:00 committed by GitHub
parent 1d7c6af44d
commit 1582299967
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 920 additions and 97 deletions

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthNotHandled](./kibana-plugin-server.authnothandled.md)
## AuthNotHandled interface
<b>Signature:</b>
```typescript
export interface AuthNotHandled
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [type](./kibana-plugin-server.authnothandled.type.md) | <code>AuthResultType.notHandled</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthNotHandled](./kibana-plugin-server.authnothandled.md) &gt; [type](./kibana-plugin-server.authnothandled.type.md)
## AuthNotHandled.type property
<b>Signature:</b>
```typescript
type: AuthResultType.notHandled;
```

View file

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthRedirected](./kibana-plugin-server.authredirected.md)
## AuthRedirected interface
<b>Signature:</b>
```typescript
export interface AuthRedirected extends AuthRedirectedParams
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [type](./kibana-plugin-server.authredirected.type.md) | <code>AuthResultType.redirected</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthRedirected](./kibana-plugin-server.authredirected.md) &gt; [type](./kibana-plugin-server.authredirected.type.md)
## AuthRedirected.type property
<b>Signature:</b>
```typescript
type: AuthResultType.redirected;
```

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) &gt; [headers](./kibana-plugin-server.authredirectedparams.headers.md)
## AuthRedirectedParams.headers property
Headers to attach for auth redirect. Must include "location" header
<b>Signature:</b>
```typescript
headers: {
location: string;
} & ResponseHeaders;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md)
## AuthRedirectedParams interface
Result of auth redirection.
<b>Signature:</b>
```typescript
export interface AuthRedirectedParams
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [headers](./kibana-plugin-server.authredirectedparams.headers.md) | <code>{</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders</code> | Headers to attach for auth redirect. Must include "location" header |

View file

@ -8,5 +8,5 @@
<b>Signature:</b>
```typescript
export declare type AuthResult = Authenticated;
export declare type AuthResult = Authenticated | AuthNotHandled | AuthRedirected;
```

View file

@ -4,7 +4,7 @@
## AuthResultParams interface
Result of an incoming request authentication.
Result of successful authentication.
<b>Signature:</b>

View file

@ -16,4 +16,6 @@ export declare enum AuthResultType
| Member | Value | Description |
| --- | --- | --- |
| authenticated | <code>&quot;authenticated&quot;</code> | |
| notHandled | <code>&quot;notHandled&quot;</code> | |
| redirected | <code>&quot;redirected&quot;</code> | |

View file

@ -17,4 +17,6 @@ export interface AuthToolkit
| Property | Type | Description |
| --- | --- | --- |
| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | <code>(data?: AuthResultParams) =&gt; AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
| [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) | <code>() =&gt; AuthResult</code> | User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(headers: {</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders) =&gt; AuthResult</code> | Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md)
## AuthToolkit.notHandled property
User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true
<b>Signature:</b>
```typescript
notHandled: () => AuthResult;
```

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [redirected](./kibana-plugin-server.authtoolkit.redirected.md)
## AuthToolkit.redirected property
Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional'
<b>Signature:</b>
```typescript
redirected: (headers: {
location: string;
} & ResponseHeaders) => AuthResult;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [auth](./kibana-plugin-server.kibanarequest.auth.md)
## KibanaRequest.auth property
<b>Signature:</b>
```typescript
readonly auth: {
isAuthenticated: boolean;
};
```

View file

@ -22,6 +22,7 @@ export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unk
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [auth](./kibana-plugin-server.kibanarequest.auth.md) | | <code>{</code><br/><code> isAuthenticated: boolean;</code><br/><code> }</code> | |
| [body](./kibana-plugin-server.kibanarequest.body.md) | | <code>Body</code> | |
| [events](./kibana-plugin-server.kibanarequest.events.md) | | <code>KibanaRequestEvents</code> | Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) |
| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | <code>Headers</code> | Readonly copy of incoming request headers. |

View file

@ -53,7 +53,10 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [AssistanceAPIResponse](./kibana-plugin-server.assistanceapiresponse.md) | |
| [AssistantAPIClientParams](./kibana-plugin-server.assistantapiclientparams.md) | |
| [Authenticated](./kibana-plugin-server.authenticated.md) | |
| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. |
| [AuthNotHandled](./kibana-plugin-server.authnothandled.md) | |
| [AuthRedirected](./kibana-plugin-server.authredirected.md) | |
| [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) | Result of auth redirection. |
| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of successful authentication. |
| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. |
| [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. |
| [Capabilities](./kibana-plugin-server.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |

View file

@ -4,12 +4,12 @@
## RouteConfigOptions.authRequired property
A flag shows that authentication for a route: `enabled` when true `disabled` when false
Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.
Enabled by default.
Defaults to `true` if an auth mechanism is registered.
<b>Signature:</b>
```typescript
authRequired?: boolean;
authRequired?: boolean | 'optional';
```

View file

@ -16,7 +16,7 @@ export interface RouteConfigOptions<Method extends RouteMethod>
| Property | Type | Description |
| --- | --- | --- |
| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | <code>boolean</code> | A flag shows that authentication for a route: <code>enabled</code> when true <code>disabled</code> when false<!-- -->Enabled by default. |
| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | <code>boolean &#124; 'optional'</code> | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.<!-- -->Defaults to <code>true</code> if an auth mechanism is registered. |
| [body](./kibana-plugin-server.routeconfigoptions.body.md) | <code>Method extends 'get' &#124; 'options' ? undefined : RouteConfigOptionsBody</code> | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md)<!-- -->. |
| [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | <code>readonly string[]</code> | Additional metadata tag strings to attach to the route. |
| [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) | <code>Method extends 'get' ? never : boolean</code> | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain <code>kbn-xsrf</code> header. - false. Disables xsrf protection.<!-- -->Set to true by default |

View file

@ -37,8 +37,7 @@ export interface CapabilitiesStart {
*/
export class CapabilitiesService {
public async start({ appIds, http }: StartDeps): Promise<CapabilitiesStart> {
const route = http.anonymousPaths.isAnonymous(window.location.pathname) ? '/defaults' : '';
const capabilities = await http.post<Capabilities>(`/api/core/capabilities${route}`, {
const capabilities = await http.post<Capabilities>('/api/core/capabilities', {
body: JSON.stringify({ applications: appIds }),
});

View file

@ -41,8 +41,8 @@ describe('CapabilitiesService', () => {
});
it('registers the capabilities routes', async () => {
expect(http.createRouter).toHaveBeenCalledWith('/api/core/capabilities');
expect(router.post).toHaveBeenCalledTimes(2);
expect(http.createRouter).toHaveBeenCalledWith('');
expect(router.post).toHaveBeenCalledTimes(1);
expect(router.post).toHaveBeenCalledWith(expect.any(Object), expect.any(Function));
});

View file

@ -22,6 +22,6 @@ import { InternalHttpServiceSetup } from '../../http';
import { registerCapabilitiesRoutes } from './resolve_capabilities';
export function registerRoutes(http: InternalHttpServiceSetup, resolver: CapabilitiesResolver) {
const router = http.createRouter('/api/core/capabilities');
const router = http.createRouter('');
registerCapabilitiesRoutes(router, resolver);
}

View file

@ -22,30 +22,24 @@ import { IRouter } from '../../http';
import { CapabilitiesResolver } from '../resolve_capabilities';
export function registerCapabilitiesRoutes(router: IRouter, resolver: CapabilitiesResolver) {
// Capabilities are fetched on both authenticated and anonymous routes.
// However when `authRequired` is false, authentication is not performed
// and only default capabilities are returned (all disabled), even for authenticated users.
// So we need two endpoints to handle both scenarios.
[true, false].forEach(authRequired => {
router.post(
{
path: authRequired ? '' : '/defaults',
options: {
authRequired,
},
validate: {
body: schema.object({
applications: schema.arrayOf(schema.string()),
}),
},
router.post(
{
path: '/api/core/capabilities',
options: {
authRequired: 'optional',
},
async (ctx, req, res) => {
const { applications } = req.body;
const capabilities = await resolver(req, applications);
return res.ok({
body: capabilities,
});
}
);
});
validate: {
body: schema.object({
applications: schema.arrayOf(schema.string()),
}),
},
},
async (ctx, req, res) => {
const { applications } = req.body;
const capabilities = await resolver(req, applications);
return res.ok({
body: capabilities,
});
}
);
}

View file

@ -36,6 +36,7 @@ import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
interface RequestFixtureOptions<P = any, Q = any, B = any> {
auth?: { isAuthenticated: boolean };
headers?: Record<string, string>;
params?: Record<string, any>;
body?: Record<string, any>;
@ -65,11 +66,13 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
routeAuthRequired,
validation = {},
kibanaRouteState = { xsrfRequired: true },
auth = { isAuthenticated: true },
}: RequestFixtureOptions<P, Q, B> = {}) {
const queryString = stringify(query, { sort: false });
return KibanaRequest.from<P, Q, B>(
createRawRequestMock({
auth,
headers,
params,
query,
@ -113,6 +116,9 @@ function createRawRequestMock(customization: DeepPartial<Request> = {}) {
{},
{
app: { xsrfRequired: true } as any,
auth: {
isAuthenticated: true,
},
headers: {},
path: '/',
route: { settings: {} },

View file

@ -26,8 +26,7 @@ import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';
import { IRouter, KibanaRouteState, isSafeMethod } from './router';
import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
@ -148,7 +147,7 @@ export class HttpServer {
this.log.debug(`registering route handler for [${route.path}]`);
// Hapi does not allow payload validation to be specified for 'head' or 'get' requests
const validate = isSafeMethod(route.method) ? undefined : { payload: true };
const { authRequired = true, tags, body = {} } = route.options;
const { authRequired, tags, body = {} } = route.options;
const { accepts: allow, maxBytes, output, parse } = body;
const kibanaRouteState: KibanaRouteState = {
@ -160,8 +159,7 @@ export class HttpServer {
method: route.method,
path: route.path,
options: {
// Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }`
auth: authRequired === true ? undefined : false,
auth: this.getAuthOption(authRequired),
app: kibanaRouteState,
tags: tags ? Array.from(tags) : undefined,
// TODO: This 'validate' section can be removed once the legacy platform is completely removed.
@ -196,6 +194,22 @@ export class HttpServer {
this.server = undefined;
}
private getAuthOption(
authRequired: RouteConfigOptions<any>['authRequired'] = true
): undefined | false | { mode: 'required' | 'optional' } {
if (this.authRegistered === false) return undefined;
if (authRequired === true) {
return { mode: 'required' };
}
if (authRequired === 'optional') {
return { mode: 'optional' };
}
if (authRequired === false) {
return false;
}
}
private setupBasePathRewrite(config: HttpConfig, basePathService: BasePath) {
if (config.basePath === undefined || !config.rewriteBasePath) {
return;

View file

@ -115,6 +115,8 @@ const createOnPostAuthToolkitMock = (): jest.Mocked<OnPostAuthToolkit> => ({
const createAuthToolkitMock = (): jest.Mocked<AuthToolkit> => ({
authenticated: jest.fn(),
notHandled: jest.fn(),
redirected: jest.fn(),
});
const createOnPreResponseToolkitMock = (): jest.Mocked<OnPreResponseToolkit> => ({

View file

@ -67,9 +67,12 @@ export {
AuthenticationHandler,
AuthHeaders,
AuthResultParams,
AuthRedirected,
AuthRedirectedParams,
AuthToolkit,
AuthResult,
Authenticated,
AuthNotHandled,
AuthResultType,
} from './lifecycle/auth';
export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth';

View file

@ -50,7 +50,7 @@ describe('http service', () => {
await root.shutdown();
});
describe('#isAuthenticated()', () => {
it('returns true if has been authorized', async () => {
it('returns true if has been authenticated', async () => {
const { http } = await root.setup();
const { registerAuth, createRouter, auth } = http;
@ -65,11 +65,11 @@ describe('http service', () => {
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true });
});
it('returns false if has not been authorized', async () => {
it('returns false if has not been authenticated', async () => {
const { http } = await root.setup();
const { registerAuth, createRouter, auth } = http;
await registerAuth((req, res, toolkit) => toolkit.authenticated());
registerAuth((req, res, toolkit) => toolkit.authenticated());
const router = createRouter('');
router.get(
@ -81,7 +81,7 @@ describe('http service', () => {
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
});
it('returns false if no authorization mechanism has been registered', async () => {
it('returns false if no authentication mechanism has been registered', async () => {
const { http } = await root.setup();
const { createRouter, auth } = http;
@ -94,6 +94,37 @@ describe('http service', () => {
await root.start();
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
});
it('returns true if authenticated on a route with "optional" auth', async () => {
const { http } = await root.setup();
const { createRouter, auth, registerAuth } = http;
registerAuth((req, res, toolkit) => toolkit.authenticated());
const router = createRouter('');
router.get(
{ path: '/is-auth', validate: false, options: { authRequired: 'optional' } },
(context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } })
);
await root.start();
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true });
});
it('returns false if not authenticated on a route with "optional" auth', async () => {
const { http } = await root.setup();
const { createRouter, auth, registerAuth } = http;
registerAuth((req, res, toolkit) => toolkit.notHandled());
const router = createRouter('');
router.get(
{ path: '/is-auth', validate: false, options: { authRequired: 'optional' } },
(context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } })
);
await root.start();
await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false });
});
});
describe('#get()', () => {
it('returns authenticated status and allow associate auth state with request', async () => {

View file

@ -57,7 +57,7 @@ interface StorageData {
}
describe('OnPreAuth', () => {
it('supports registering request inceptors', async () => {
it('supports registering a request interceptor', async () => {
const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@ -415,6 +415,23 @@ describe('Auth', () => {
.expect(200, { content: 'ok' });
});
it('blocks access to a resource if credentials are not provided', async () => {
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get({ path: '/', validate: false }, (context, req, res) =>
res.ok({ body: { content: 'ok' } })
);
registerAuth((req, res, t) => t.notHandled());
await server.start();
const result = await supertest(innerServer.listener)
.get('/')
.expect(401);
expect(result.body.message).toBe('Unauthorized');
});
it('enables auth for a route by default if registerAuth has been called', async () => {
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@ -492,11 +509,9 @@ describe('Auth', () => {
router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
const redirectTo = '/redirect-url';
registerAuth((req, res) =>
res.redirected({
headers: {
location: redirectTo,
},
registerAuth((req, res, t) =>
t.redirected({
location: redirectTo,
})
);
await server.start();
@ -507,6 +522,19 @@ describe('Auth', () => {
expect(response.header.location).toBe(redirectTo);
});
it('throws if redirection url is not provided', async () => {
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
registerAuth((req, res, t) => t.redirected({} as any));
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(500);
});
it(`doesn't expose internal error details`, async () => {
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@ -865,7 +893,7 @@ describe('Auth', () => {
]
`);
});
// eslint-disable-next-line
it(`doesn't share request object between interceptors`, async () => {
const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');

View file

@ -45,6 +45,89 @@ afterEach(async () => {
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
describe('KibanaRequest', () => {
describe('auth', () => {
describe('isAuthenticated', () => {
it('returns false if no auth interceptor was registered', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
isAuthenticated: false,
});
});
it('returns false if not authenticated on a route with authRequired: "optional"', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
registerAuth((req, res, toolkit) => toolkit.notHandled());
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
isAuthenticated: false,
});
});
it('returns false if redirected on a route with authRequired: "optional"', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
registerAuth((req, res, toolkit) => toolkit.redirected({ location: '/any' }));
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
isAuthenticated: false,
});
});
it('returns true if authenticated on a route with authRequired: "optional"', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
registerAuth((req, res, toolkit) => toolkit.authenticated());
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
isAuthenticated: true,
});
});
it('returns true if authenticated', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
registerAuth((req, res, toolkit) => toolkit.authenticated());
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
isAuthenticated: true,
});
});
});
});
describe('events', () => {
describe('aborted$', () => {
it('emits once and completes when request aborted', async done => {

View file

@ -46,6 +46,286 @@ afterEach(async () => {
await server.stop();
});
describe('Options', () => {
describe('authRequired', () => {
describe('optional', () => {
it('User has access to a route if auth mechanism not registered', async () => {
const { server: innerServer, createRouter, auth } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) =>
res.ok({
body: {
httpAuthIsAuthenticated: auth.isAuthenticated(req),
requestIsAuthenticated: req.auth.isAuthenticated,
},
})
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
httpAuthIsAuthenticated: false,
requestIsAuthenticated: false,
});
});
it('Authenticated user has access to a route', async () => {
const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
setupDeps
);
const router = createRouter('/');
registerAuth((req, res, toolkit) => {
return toolkit.authenticated();
});
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) =>
res.ok({
body: {
httpAuthIsAuthenticated: auth.isAuthenticated(req),
requestIsAuthenticated: req.auth.isAuthenticated,
},
})
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
httpAuthIsAuthenticated: true,
requestIsAuthenticated: true,
});
});
it('User with no credentials can access a route', async () => {
const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
setupDeps
);
const router = createRouter('/');
registerAuth((req, res, toolkit) => toolkit.notHandled());
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) =>
res.ok({
body: {
httpAuthIsAuthenticated: auth.isAuthenticated(req),
requestIsAuthenticated: req.auth.isAuthenticated,
},
})
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
httpAuthIsAuthenticated: false,
requestIsAuthenticated: false,
});
});
it('User with invalid credentials cannot access a route', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
registerAuth((req, res, toolkit) => res.unauthorized());
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) => res.ok({ body: 'ok' })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(401);
});
it('does not redirect user and allows access to a resource', async () => {
const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
setupDeps
);
const router = createRouter('/');
registerAuth((req, res, toolkit) =>
toolkit.redirected({
location: '/redirect-to',
})
);
router.get(
{ path: '/', validate: false, options: { authRequired: 'optional' } },
(context, req, res) =>
res.ok({
body: {
httpAuthIsAuthenticated: auth.isAuthenticated(req),
requestIsAuthenticated: req.auth.isAuthenticated,
},
})
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
httpAuthIsAuthenticated: false,
requestIsAuthenticated: false,
});
});
});
describe('true', () => {
it('User has access to a route if auth interceptor is not registered', async () => {
const { server: innerServer, createRouter, auth } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) =>
res.ok({
body: {
httpAuthIsAuthenticated: auth.isAuthenticated(req),
requestIsAuthenticated: req.auth.isAuthenticated,
},
})
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
httpAuthIsAuthenticated: false,
requestIsAuthenticated: false,
});
});
it('Authenticated user has access to a route', async () => {
const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
setupDeps
);
const router = createRouter('/');
registerAuth((req, res, toolkit) => {
return toolkit.authenticated();
});
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) =>
res.ok({
body: {
httpAuthIsAuthenticated: auth.isAuthenticated(req),
requestIsAuthenticated: req.auth.isAuthenticated,
},
})
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
httpAuthIsAuthenticated: true,
requestIsAuthenticated: true,
});
});
it('User with no credentials cannot access a route', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
registerAuth((req, res, toolkit) => toolkit.notHandled());
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => res.ok({ body: 'ok' })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(401);
});
it('User with invalid credentials cannot access a route', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
registerAuth((req, res, toolkit) => res.unauthorized());
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => res.ok({ body: 'ok' })
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(401);
});
it('allows redirecting an user', async () => {
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps);
const router = createRouter('/');
const redirectUrl = '/redirect-to';
registerAuth((req, res, toolkit) =>
toolkit.redirected({
location: redirectUrl,
})
);
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => res.ok({ body: 'ok' })
);
await server.start();
const result = await supertest(innerServer.listener)
.get('/')
.expect(302);
expect(result.header.location).toBe(redirectUrl);
});
});
describe('false', () => {
it('does not try to authenticate a user', async () => {
const { server: innerServer, createRouter, registerAuth, auth } = await server.setup(
setupDeps
);
const router = createRouter('/');
const authHook = jest.fn();
registerAuth(authHook);
router.get(
{ path: '/', validate: false, options: { authRequired: false } },
(context, req, res) =>
res.ok({
body: {
httpAuthIsAuthenticated: auth.isAuthenticated(req),
requestIsAuthenticated: req.auth.isAuthenticated,
},
})
);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, {
httpAuthIsAuthenticated: false,
requestIsAuthenticated: false,
});
expect(authHook).toHaveBeenCalledTimes(0);
});
});
});
});
describe('Handler', () => {
it("Doesn't expose error details if handler throws", async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);

View file

@ -25,11 +25,14 @@ import {
lifecycleResponseFactory,
LifecycleResponseFactory,
isKibanaResponse,
ResponseHeaders,
} from '../router';
/** @public */
export enum AuthResultType {
authenticated = 'authenticated',
notHandled = 'notHandled',
redirected = 'redirected',
}
/** @public */
@ -38,10 +41,20 @@ export interface Authenticated extends AuthResultParams {
}
/** @public */
export type AuthResult = Authenticated;
export interface AuthNotHandled {
type: AuthResultType.notHandled;
}
/** @public */
export interface AuthRedirected extends AuthRedirectedParams {
type: AuthResultType.redirected;
}
/** @public */
export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected;
const authResult = {
authenticated(data: Partial<AuthResultParams> = {}): AuthResult {
authenticated(data: AuthResultParams = {}): AuthResult {
return {
type: AuthResultType.authenticated,
state: data.state,
@ -49,8 +62,25 @@ const authResult = {
responseHeaders: data.responseHeaders,
};
},
notHandled(): AuthResult {
return {
type: AuthResultType.notHandled,
};
},
redirected(headers: { location: string } & ResponseHeaders): AuthResult {
return {
type: AuthResultType.redirected,
headers,
};
},
isAuthenticated(result: AuthResult): result is Authenticated {
return result && result.type === AuthResultType.authenticated;
return result?.type === AuthResultType.authenticated;
},
isNotHandled(result: AuthResult): result is AuthNotHandled {
return result?.type === AuthResultType.notHandled;
},
isRedirected(result: AuthResult): result is AuthRedirected {
return result?.type === AuthResultType.redirected;
},
};
@ -62,7 +92,7 @@ const authResult = {
export type AuthHeaders = Record<string, string | string[]>;
/**
* Result of an incoming request authentication.
* Result of successful authentication.
* @public
*/
export interface AuthResultParams {
@ -82,6 +112,18 @@ export interface AuthResultParams {
responseHeaders?: AuthHeaders;
}
/**
* Result of auth redirection.
* @public
*/
export interface AuthRedirectedParams {
/**
* Headers to attach for auth redirect.
* Must include "location" header
*/
headers: { location: string } & ResponseHeaders;
}
/**
* @public
* A tool set defining an outcome of Auth interceptor for incoming request.
@ -89,10 +131,23 @@ export interface AuthResultParams {
export interface AuthToolkit {
/** Authentication is successful with given credentials, allow request to pass through */
authenticated: (data?: AuthResultParams) => AuthResult;
/**
* User has no credentials.
* Allows user to access a resource when authRequired: 'optional'
* Rejects a request when authRequired: true
* */
notHandled: () => AuthResult;
/**
* Redirects user to another location to complete authentication when authRequired: true
* Allows user to access a resource without redirection when authRequired: 'optional'
* */
redirected: (headers: { location: string } & ResponseHeaders) => AuthResult;
}
const toolkit: AuthToolkit = {
authenticated: authResult.authenticated,
notHandled: authResult.notHandled,
redirected: authResult.redirected,
};
/**
@ -109,30 +164,51 @@ export type AuthenticationHandler = (
export function adoptToHapiAuthFormat(
fn: AuthenticationHandler,
log: Logger,
onSuccess: (req: Request, data: AuthResultParams) => void = () => undefined
onAuth: (request: Request, data: AuthResultParams) => void = () => undefined
) {
return async function interceptAuth(
request: Request,
responseToolkit: ResponseToolkit
): Promise<Lifecycle.ReturnValue> {
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
const kibanaRequest = KibanaRequest.from(request, undefined, false);
try {
const result = await fn(
KibanaRequest.from(request, undefined, false),
lifecycleResponseFactory,
toolkit
);
const result = await fn(kibanaRequest, lifecycleResponseFactory, toolkit);
if (isKibanaResponse(result)) {
return hapiResponseAdapter.handle(result);
}
if (authResult.isAuthenticated(result)) {
onSuccess(request, {
onAuth(request, {
state: result.state,
requestHeaders: result.requestHeaders,
responseHeaders: result.responseHeaders,
});
return responseToolkit.authenticated({ credentials: result.state || {} });
}
if (authResult.isRedirected(result)) {
// we cannot redirect a user when resources with optional auth requested
if (kibanaRequest.route.options.authRequired === 'optional') {
return responseToolkit.continue;
}
return hapiResponseAdapter.handle(
lifecycleResponseFactory.redirected({
// hapi doesn't accept string[] as a valid header
headers: result.headers as any,
})
);
}
if (authResult.isNotHandled(result)) {
if (kibanaRequest.route.options.authRequired === 'optional') {
return responseToolkit.continue;
}
return hapiResponseAdapter.handle(lifecycleResponseFactory.unauthorized());
}
throw new Error(
`Unexpected result from Authenticate. Expected AuthResult or KibanaResponse, but given: ${result}.`
);

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { RouteOptions } from 'hapi';
import { KibanaRequest } from './request';
import { httpServerMock } from '../http_server.mocks';
import { schema } from '@kbn/config-schema';
@ -117,6 +118,106 @@ describe('KibanaRequest', () => {
});
});
describe('route.options.authRequired property', () => {
it('handles required auth: undefined', () => {
const auth: RouteOptions['auth'] = undefined;
const request = httpServerMock.createRawRequest({
route: {
settings: {
auth,
},
},
});
const kibanaRequest = KibanaRequest.from(request);
expect(kibanaRequest.route.options.authRequired).toBe(true);
});
it('handles required auth: false', () => {
const auth: RouteOptions['auth'] = false;
const request = httpServerMock.createRawRequest({
route: {
settings: {
auth,
},
},
});
const kibanaRequest = KibanaRequest.from(request);
expect(kibanaRequest.route.options.authRequired).toBe(false);
});
it('handles required auth: { mode: "required" }', () => {
const auth: RouteOptions['auth'] = { mode: 'required' };
const request = httpServerMock.createRawRequest({
route: {
settings: {
auth,
},
},
});
const kibanaRequest = KibanaRequest.from(request);
expect(kibanaRequest.route.options.authRequired).toBe(true);
});
it('handles required auth: { mode: "optional" }', () => {
const auth: RouteOptions['auth'] = { mode: 'optional' };
const request = httpServerMock.createRawRequest({
route: {
settings: {
auth,
},
},
});
const kibanaRequest = KibanaRequest.from(request);
expect(kibanaRequest.route.options.authRequired).toBe('optional');
});
it('handles required auth: { mode: "try" } as "optional"', () => {
const auth: RouteOptions['auth'] = { mode: 'try' };
const request = httpServerMock.createRawRequest({
route: {
settings: {
auth,
},
},
});
const kibanaRequest = KibanaRequest.from(request);
expect(kibanaRequest.route.options.authRequired).toBe('optional');
});
it('throws on auth: strategy name', () => {
const auth: RouteOptions['auth'] = 'session';
const request = httpServerMock.createRawRequest({
route: {
settings: {
auth,
},
},
});
expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot(
`"unexpected authentication options: \\"session\\" for route: /"`
);
});
it('throws on auth: { mode: unexpected mode }', () => {
const auth: RouteOptions['auth'] = { mode: undefined };
const request = httpServerMock.createRawRequest({
route: {
settings: {
auth,
},
},
});
expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot(
`"unexpected authentication options: {} for route: /"`
);
});
});
describe('RouteSchema type inferring', () => {
it('should work with config-schema', () => {
const body = Buffer.from('body!');

View file

@ -143,6 +143,10 @@ export class KibanaRequest<
public readonly socket: IKibanaSocket;
/** Request events {@link KibanaRequestEvents} */
public readonly events: KibanaRequestEvents;
public readonly auth: {
/* true if the request has been successfully authenticated, otherwise false. */
isAuthenticated: boolean;
};
/** @internal */
protected readonly [requestSymbol]: Request;
@ -172,6 +176,11 @@ export class KibanaRequest<
this.route = deepFreeze(this.getRouteInfo(request));
this.socket = new KibanaSocket(request.raw.req.socket);
this.events = this.getEvents(request);
this.auth = {
// missing in fakeRequests, so we cast to false
isAuthenticated: Boolean(request.auth?.isAuthenticated),
};
}
private getEvents(request: Request): KibanaRequestEvents {
@ -189,7 +198,7 @@ export class KibanaRequest<
const { parse, maxBytes, allow, output } = request.route.settings.payload || {};
const options = ({
authRequired: request.route.settings.auth !== false,
authRequired: this.getAuthRequired(request),
// some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true,
tags: request.route.settings.tags || [],
@ -209,6 +218,31 @@ export class KibanaRequest<
options,
};
}
private getAuthRequired(request: Request): boolean | 'optional' {
const authOptions = request.route.settings.auth;
if (typeof authOptions === 'object') {
// 'try' is used in the legacy platform
if (authOptions.mode === 'optional' || authOptions.mode === 'try') {
return 'optional';
}
if (authOptions.mode === 'required') {
return true;
}
}
// legacy platform routes
if (authOptions === undefined) {
return true;
}
if (authOptions === false) return false;
throw new Error(
`unexpected authentication options: ${JSON.stringify(authOptions)} for route: ${
this.url.href
}`
);
}
}
/**

View file

@ -116,13 +116,15 @@ export interface RouteConfigOptionsBody {
*/
export interface RouteConfigOptions<Method extends RouteMethod> {
/**
* A flag shows that authentication for a route:
* `enabled` when true
* `disabled` when false
* Defines authentication mode for a route:
* - true. A user has to have valid credentials to access a resource
* - false. A user can access a resource without any credentials.
* - 'optional'. A user can access a resource if has valid credentials or no credentials at all.
* Can be useful when we grant access to a resource but want to identify a user if possible.
*
* Enabled by default.
* Defaults to `true` if an auth mechanism is registered.
*/
authRequired?: boolean;
authRequired?: boolean | 'optional';
/**
* Defines xsrf protection requirements for a route:

View file

@ -100,9 +100,12 @@ export {
AuthResultParams,
AuthStatus,
AuthToolkit,
AuthRedirected,
AuthRedirectedParams,
AuthResult,
AuthResultType,
Authenticated,
AuthNotHandled,
BasePath,
IBasePath,
CustomHttpResponseOptions,

View file

@ -419,7 +419,26 @@ export type AuthenticationHandler = (request: KibanaRequest, response: Lifecycle
export type AuthHeaders = Record<string, string | string[]>;
// @public (undocumented)
export type AuthResult = Authenticated;
export interface AuthNotHandled {
// (undocumented)
type: AuthResultType.notHandled;
}
// @public (undocumented)
export interface AuthRedirected extends AuthRedirectedParams {
// (undocumented)
type: AuthResultType.redirected;
}
// @public
export interface AuthRedirectedParams {
headers: {
location: string;
} & ResponseHeaders;
}
// @public (undocumented)
export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected;
// @public
export interface AuthResultParams {
@ -431,7 +450,11 @@ export interface AuthResultParams {
// @public (undocumented)
export enum AuthResultType {
// (undocumented)
authenticated = "authenticated"
authenticated = "authenticated",
// (undocumented)
notHandled = "notHandled",
// (undocumented)
redirected = "redirected"
}
// @public
@ -444,6 +467,10 @@ export enum AuthStatus {
// @public
export interface AuthToolkit {
authenticated: (data?: AuthResultParams) => AuthResult;
notHandled: () => AuthResult;
redirected: (headers: {
location: string;
} & ResponseHeaders) => AuthResult;
}
// @public
@ -970,6 +997,10 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Me
protected readonly [requestSymbol]: Request;
constructor(request: Request, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean);
// (undocumented)
readonly auth: {
isAuthenticated: boolean;
};
// (undocumented)
readonly body: Body;
readonly events: KibanaRequestEvents;
// Warning: (ae-forgotten-export) The symbol "RouteValidator" needs to be exported by the entry point index.d.ts
@ -1470,7 +1501,7 @@ export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
// @public
export interface RouteConfigOptions<Method extends RouteMethod> {
authRequired?: boolean;
authRequired?: boolean | 'optional';
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
tags?: readonly string[];
xsrfRequired?: Method extends 'get' ? never : boolean;

View file

@ -133,7 +133,7 @@ describe('setupAuthentication()', () => {
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith();
expect(mockResponse.redirected).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).not.toHaveBeenCalled();
@ -156,7 +156,7 @@ describe('setupAuthentication()', () => {
state: mockUser,
requestHeaders: mockAuthHeaders,
});
expect(mockResponse.redirected).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).toHaveBeenCalledTimes(1);
@ -185,7 +185,7 @@ describe('setupAuthentication()', () => {
requestHeaders: mockAuthHeaders,
responseHeaders: mockAuthResponseHeaders,
});
expect(mockResponse.redirected).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).toHaveBeenCalledTimes(1);
@ -198,9 +198,9 @@ describe('setupAuthentication()', () => {
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.redirected).toHaveBeenCalledTimes(1);
expect(mockResponse.redirected).toHaveBeenCalledWith({
headers: { location: '/some/url' },
expect(mockAuthToolkit.redirected).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.redirected).toHaveBeenCalledWith({
location: '/some/url',
});
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
@ -217,7 +217,7 @@ describe('setupAuthentication()', () => {
expect(error).toBeUndefined();
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockResponse.redirected).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(loggingServiceMock.collect(mockSetupAuthenticationParams.loggers).error)
.toMatchInlineSnapshot(`
Array [
@ -240,7 +240,7 @@ describe('setupAuthentication()', () => {
expect(response.body).toBe(esError);
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockResponse.redirected).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
it('includes `WWW-Authenticate` header if `authenticate` fails to authenticate user and provides challenges', async () => {
@ -265,22 +265,19 @@ describe('setupAuthentication()', () => {
expect(options!.headers).toEqual({ 'WWW-Authenticate': 'Negotiate' });
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockResponse.redirected).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
it('returns `unauthorized` when authentication can not be handled', async () => {
it('returns `notHandled` when authentication can not be handled', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
authenticate.mockResolvedValue(AuthenticationResult.notHandled());
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.unauthorized).toHaveBeenCalledTimes(1);
const [[response]] = mockResponse.unauthorized.mock.calls;
expect(response!.body).toBeUndefined();
expect(mockAuthToolkit.notHandled).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockResponse.redirected).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
});

View file

@ -139,10 +139,8 @@ export async function setupAuthentication({
// authentication (username and password) or arbitrary external page managed by 3rd party
// Identity Provider for SSO authentication mechanisms. Authentication provider is the one who
// decides what location user should be redirected to.
return response.redirected({
headers: {
location: authenticationResult.redirectURL!,
},
return t.redirected({
location: authenticationResult.redirectURL!,
});
}
@ -165,9 +163,7 @@ export async function setupAuthentication({
}
authLogger.debug('Could not handle authentication attempt');
return response.unauthorized({
headers: authenticationResult.authResponseHeaders,
});
return t.notHandled();
});
authLogger.debug('Successfully registered core authentication handler.');

View file

@ -112,8 +112,7 @@ export function setupAuthorization({
authz
);
// if we're an anonymous route, we disable all ui capabilities
if (request.route.options.authRequired === false) {
if (!request.auth.isAuthenticated) {
return disableUICapabilities.all(capabilities);
}