Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an optional authentication mode for HTTP resources #58589

Merged
merged 33 commits into from
Mar 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
89a1eff
add authRequred: 'optional'
mshustov Feb 26, 2020
0f3a915
expose auth status via request context
mshustov Feb 26, 2020
05809a4
update security plugin to use notHandled auth outcome
mshustov Feb 26, 2020
fc94baa
capabilities service uses optional auth
mshustov Feb 26, 2020
de22f7e
update tests
mshustov Feb 26, 2020
97ac4a3
Merge branch 'master' into issue-41959-optional-auth
mshustov Feb 26, 2020
006edef
attach security headers only to unauthorised response
mshustov Feb 26, 2020
529eeee
add isAuthenticated tests for 'optional' auth mode
mshustov Feb 26, 2020
4c9f0bc
security plugin relies on http.auth.isAuthenticated to calc capabilities
mshustov Feb 26, 2020
a2f2546
generate docs
mshustov Feb 26, 2020
9bec57f
reword test suit names
mshustov Feb 26, 2020
6c4de35
Merge branch 'master' into issue-41959-optional-auth
mshustov Mar 4, 2020
82835fc
update tests
mshustov Mar 4, 2020
526922b
Merge branch 'master' into issue-41959-optional-auth
mshustov Mar 4, 2020
46b5232
update test checking isAuth on optional auth path
mshustov Mar 4, 2020
2c2cbe1
address Oleg comments
mshustov Mar 4, 2020
0801b08
add test for auth: try
mshustov Mar 4, 2020
64ff1ac
fix
mshustov Mar 4, 2020
afa6d4b
pass isAuthenticted as boolean via context
mshustov Mar 4, 2020
cc3d95c
remove response header from notHandled
mshustov Mar 4, 2020
e96158b
update docs
mshustov Mar 4, 2020
9750ce1
add redirected for auth interceptor
mshustov Mar 5, 2020
5efb16a
security plugin uses t.redirected to be compat with auth: optional
mshustov Mar 5, 2020
c5cb1fd
update docs
mshustov Mar 5, 2020
fcb4994
require location header in the interface
mshustov Mar 5, 2020
f1ecba3
address comments #1
mshustov Mar 6, 2020
f6dad7f
declare isAuthenticated on KibanaRequest
mshustov Mar 6, 2020
d1901a8
remove auth.isAuthenticated from scope
mshustov Mar 6, 2020
0275e25
update docs
mshustov Mar 6, 2020
8b2a817
Merge branch 'master' into issue-41959-optional-auth
mshustov Mar 6, 2020
01d995c
remove unnecessary comment
mshustov Mar 6, 2020
f177d69
do not fail on FakrRequest
mshustov Mar 6, 2020
86f4603
small improvements
mshustov Mar 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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> | |

Original file line number Diff line number Diff line change
@@ -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;
```
Original file line number Diff line number Diff line change
@@ -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> | |
Original file line number Diff line number Diff line change
@@ -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;
```
Original file line number Diff line number Diff line change
@@ -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;
```
Original file line number Diff line number Diff line change
@@ -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 |

Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
<b>Signature:</b>

```typescript
export declare type AuthResult = Authenticated;
export declare type AuthResult = Authenticated | AuthNotHandled | AuthRedirected;
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## AuthResultParams interface

Result of an incoming request authentication.
Result of successful authentication.

<b>Signature:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> | |

Original file line number Diff line number Diff line change
Expand Up @@ -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' |

Original file line number Diff line number Diff line change
@@ -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;
```
Original file line number Diff line number Diff line change
@@ -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;
```
Original file line number Diff line number Diff line change
@@ -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;
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
5 changes: 4 additions & 1 deletion docs/development/core/server/kibana-plugin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

body: JSON.stringify({ applications: appIds }),
});

Expand Down
4 changes: 2 additions & 2 deletions src/core/server/capabilities/capabilities_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/server/capabilities/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: why remove the prefix here?

registerCapabilitiesRoutes(router, resolver);
}
44 changes: 19 additions & 25 deletions src/core/server/capabilities/routes/resolve_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
);
}
6 changes: 6 additions & 0 deletions src/core/server/http/http_server.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -113,6 +116,9 @@ function createRawRequestMock(customization: DeepPartial<Request> = {}) {
{},
{
app: { xsrfRequired: true } as any,
auth: {
isAuthenticated: true,
},
headers: {},
path: '/',
route: { settings: {} },
Expand Down
24 changes: 19 additions & 5 deletions src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand All @@ -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.
Expand Down Expand Up @@ -196,6 +194,22 @@ export class HttpServer {
this.server = undefined;
}

private getAuthOption(
authRequired: RouteConfigOptions<any>['authRequired'] = true
mshustov marked this conversation as resolved.
Show resolved Hide resolved
): 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;
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/http/http_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> => ({
Expand Down
Loading