-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Changes from 6 commits
89a1eff
0f3a915
05809a4
fc94baa
de22f7e
97ac4a3
006edef
529eeee
4c9f0bc
a2f2546
9bec57f
6c4de35
82835fc
526922b
46b5232
2c2cbe1
0801b08
64ff1ac
afa6d4b
cc3d95c
e96158b
9750ce1
5efb16a
c5cb1fd
fcb4994
f1ecba3
f6dad7f
d1901a8
0275e25
8b2a817
01d995c
f177d69
86f4603
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: why remove the prefix here? |
||
registerCapabilitiesRoutes(router, resolver); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_p | |
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; | ||
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; | ||
|
||
import { IRouter } from './router'; | ||
import { IRouter, RouteConfigOptions } from './router'; | ||
import { | ||
SessionStorageCookieOptions, | ||
createCookieSessionStorageFactory, | ||
|
@@ -148,15 +148,15 @@ 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 = ['head', 'get'].includes(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; | ||
|
||
this.server.route({ | ||
handler: route.handler, | ||
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), | ||
tags: tags ? Array.from(tags) : undefined, | ||
// TODO: This 'validate' section can be removed once the legacy platform is completely removed. | ||
// We are telling Hapi that NP routes can accept any payload, so that it can bypass the default | ||
|
@@ -190,6 +190,20 @@ export class HttpServer { | |
this.server = undefined; | ||
} | ||
|
||
private getAuthOption( | ||
authRequired: RouteConfigOptions<any>['authRequired'] = true | ||
mshustov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
): false | { mode: 'required' | 'optional' } { | ||
if (this.authRegistered) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It sets mode explicitly. I had to change the logic to inline it with |
||
if (authRequired === true) { | ||
return { mode: 'required' }; | ||
} | ||
if (authRequired === 'optional') { | ||
return { mode: 'optional' }; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private setupBasePathRewrite(config: HttpConfig, basePathService: BasePath) { | ||
if (config.basePath === undefined || !config.rewriteBasePath) { | ||
return; | ||
|
@@ -303,6 +317,11 @@ export class HttpServer { | |
// where some plugin read directly from headers to identify whether a user is authenticated. | ||
Object.assign(req.headers, requestHeaders); | ||
} | ||
}, | ||
(req, { responseHeaders }) => { | ||
if (responseHeaders) { | ||
this.authResponseHeaders.set(req, responseHeaders); | ||
} | ||
} | ||
), | ||
})); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -415,6 +415,23 @@ describe('Auth', () => { | |
.expect(200, { content: 'ok' }); | ||
}); | ||
|
||
it('blocks access to a resource if credentials are not recognized', async () => { | ||
mshustov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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('/'); | ||
|
@@ -684,6 +701,27 @@ describe('Auth', () => { | |
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); | ||
}); | ||
|
||
it('attach security header to not handled auth response', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. duplicates tests in src/core/server/http/integration_tests/router.test.ts should I remove them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One seems enough. Not sure in which file it makes more sense to keep it though. There seems to be overlapping 'coverage' between the two test files. |
||
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); | ||
const router = createRouter('/'); | ||
|
||
const authResponseHeader = { | ||
'www-authenticate': 'from auth interceptor', | ||
}; | ||
registerAuth((req, res, toolkit) => { | ||
return toolkit.notHandled({ responseHeaders: authResponseHeader }); | ||
}); | ||
|
||
router.get({ path: '/', validate: false }, (context, req, res) => res.ok()); | ||
await server.start(); | ||
|
||
const response = await supertest(innerServer.listener) | ||
.get('/') | ||
.expect(401); | ||
|
||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); | ||
}); | ||
|
||
it('attach security header to an error response', async () => { | ||
const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); | ||
const router = createRouter('/'); | ||
|
@@ -865,7 +903,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('/'); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,114 @@ afterEach(async () => { | |
await server.stop(); | ||
}); | ||
|
||
describe('Options', () => { | ||
describe('authRequired', () => { | ||
describe('optional', () => { | ||
mshustov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
it('User has access to a route if auth mechanism not registered', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we verify the value of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have separate tests for them, but it doesn't hurt since here we can verify:
|
||
const { server: innerServer, createRouter } = await server.setup(setupDeps); | ||
const router = createRouter('/'); | ||
|
||
router.get( | ||
{ path: '/', validate: false, options: { authRequired: 'optional' } }, | ||
(context, req, res) => res.ok({ body: 'ok' }) | ||
); | ||
await server.start(); | ||
|
||
await supertest(innerServer.listener) | ||
.get('/') | ||
.expect(200, 'ok'); | ||
}); | ||
|
||
it('Authenticated user has access to a route', async () => { | ||
const { server: innerServer, createRouter, registerAuth } = 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: 'ok' }) | ||
); | ||
await server.start(); | ||
|
||
await supertest(innerServer.listener) | ||
.get('/') | ||
.expect(200, 'ok'); | ||
}); | ||
|
||
it('User with not recognized credentials has access to a route', async () => { | ||
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); | ||
const router = createRouter('/'); | ||
const authResponseHeader = { | ||
'www-authenticate': 'from auth interceptor', | ||
}; | ||
|
||
registerAuth((req, res, toolkit) => { | ||
return toolkit.notHandled({ | ||
responseHeaders: authResponseHeader, | ||
}); | ||
}); | ||
router.get( | ||
{ path: '/', validate: false, options: { authRequired: 'optional' } }, | ||
(context, req, res) => res.ok({ body: 'ok' }) | ||
); | ||
await server.start(); | ||
|
||
const response = await supertest(innerServer.listener) | ||
.get('/') | ||
.expect(200, 'ok'); | ||
|
||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); | ||
}); | ||
}); | ||
describe('true', () => { | ||
it('Authenticated user has access to a route', async () => { | ||
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); | ||
const router = createRouter('/'); | ||
|
||
registerAuth((req, res, toolkit) => { | ||
return toolkit.authenticated(); | ||
}); | ||
router.get( | ||
{ path: '/', validate: false, options: { authRequired: 'optional' } }, | ||
mshustov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(context, req, res) => res.ok({ body: 'ok' }) | ||
); | ||
await server.start(); | ||
|
||
await supertest(innerServer.listener) | ||
.get('/') | ||
.expect(200, 'ok'); | ||
}); | ||
|
||
it('User with not recognized credentials has no access to a route', async () => { | ||
const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); | ||
const router = createRouter('/'); | ||
const authResponseHeader = { | ||
'www-authenticate': 'from auth interceptor', | ||
}; | ||
|
||
registerAuth((req, res, toolkit) => { | ||
return toolkit.notHandled({ | ||
responseHeaders: authResponseHeader, | ||
}); | ||
}); | ||
router.get( | ||
{ path: '/', validate: false, options: { authRequired: true } }, | ||
(context, req, res) => res.ok({ body: 'ok' }) | ||
); | ||
await server.start(); | ||
|
||
const response = await supertest(innerServer.listener) | ||
.get('/') | ||
.expect(401); | ||
|
||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('Handler', () => { | ||
it("Doesn't expose error details if handler throws", async () => { | ||
const { server: innerServer, createRouter } = await server.setup(setupDeps); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍