-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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 support for OpenID Connect implicit authentication flow. #42069
Changes from 1 commit
39e6237
1bf2b13
a2932f0
99a593d
d73fd18
293de31
f325744
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 |
---|---|---|
|
@@ -7,8 +7,9 @@ | |
import Boom from 'boom'; | ||
import Joi from 'joi'; | ||
import { schema } from '@kbn/config-schema'; | ||
import { canRedirectRequest, wrapError } from '../../../../../../../plugins/security/server'; | ||
import { canRedirectRequest, wrapError, OIDCAuthenticationFlow } from '../../../../../../../plugins/security/server'; | ||
import { KibanaRequest } from '../../../../../../../../src/core/server'; | ||
import { createCSPRuleString, generateCSPNonce } from '../../../../../../../../src/legacy/server/csp'; | ||
|
||
export function initAuthenticateApi({ authc: { login, logout }, config }, server) { | ||
|
||
|
@@ -82,6 +83,36 @@ export function initAuthenticateApi({ authc: { login, logout }, config }, server | |
} | ||
}); | ||
|
||
/** | ||
* The route should be configured as a redirect URI in OP when OpenID Connect implicit flow | ||
* is used, so that we can extract authentication response from URL fragment and send it to | ||
* the `/api/security/v1/oidc` route. | ||
*/ | ||
server.route({ | ||
method: 'GET', | ||
path: '/api/security/v1/oidc/implicit', | ||
config: { auth: false }, | ||
async handler(request, h) { | ||
const legacyConfig = server.config(); | ||
const basePath = legacyConfig.get('server.basePath'); | ||
|
||
const nonce = await generateCSPNonce(); | ||
const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce); | ||
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. note: using CSP here feels like an overkill here, but it doesn't hurt to have it I think. 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. Do we need to do anything about I've been trying to think of potential issues that we get from rendering all of the HTML ourselves, but I haven't been able to think of any. 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.
Hapi's |
||
return h.response(` | ||
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. This is likely a naive question. When reading through the OIDC spec, section 3.2.2.5 states
And section 2.1 of the OAuth2.0 Response Type Encoding Practices makes it appear that we could potentially use the 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.
And what is the question? 🙂 So, I assume you're asking why we can't just say OP and RP (ES) to use
cc @jkakavas 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. Okta seems to obey this rule too https://developer.okta.com/docs/reference/api/oidc/#request-parameters 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. Ah, thanks for the clarification. I stopped reading the specs too soon. 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. fragment is used on purpose so that the access token and the id token are not sent as part of any requests to a back-end server ( The assumption was that the implicit flow would be best used by javascript clients running in the browser ) and thus don't get leaked in logs or be available more than needed on the wire. We (and many other clients) have the validation on the server side so we end up making a request with it to the kibana server eitherway so there goes that.. In any case as @azasypkin mentioned we're not allowed to use anything else for 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. Thanks @jkakavas |
||
<!DOCTYPE html> | ||
<title>Kibana OpenID Connect Login</title> | ||
<script nonce="${nonce}"> | ||
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. note: It seems it'd be better to include 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. The w3c validator makes it seem like we should include Borrowing from https://mathiasbynens.be/notes/minimal-html who found whatwg/html@5c7cf94, the 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. Huh, would never thought we may need title :) Let me read through this, thanks for sharing! 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. Yeah, the spec agrees with you: https://html.spec.whatwg.org/multipage/semantics.html#the-title-element: |
||
window.location.replace( | ||
'${basePath}/api/security/v1/oidc?authenticationResponseURI=' + encodeURIComponent(window.location.href) | ||
); | ||
</script> | ||
`) | ||
.header('cache-control', 'private, no-cache, no-store') | ||
.header('content-security-policy', cspRulesHeader) | ||
.type('text/html'); | ||
} | ||
}); | ||
|
||
server.route({ | ||
// POST is only allowed for Third Party initiated authentication | ||
method: ['GET', 'POST'], | ||
|
@@ -103,25 +134,48 @@ export function initAuthenticateApi({ authc: { login, logout }, config }, server | |
}, | ||
async handler(request, h) { | ||
try { | ||
const query = request.query || {}; | ||
const payload = request.payload || {}; | ||
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 predates this PR, but should we considering add payload validation to this route? 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. I don't know to be honest, I was even planning to eventually get rid of query validation as well since we (Kibana) neither parse nor try to interpret them and just blindly pass to Elasticsearch anyway (and we allow unknown parameters as well). If it happens so that these args are malformed, imo, it's better to allow ES to parse them and log more detailed error in their log than having this validation in Kibana. What do you think? 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 likely get little benefit from doing it ourselves because we end up passing it directly to Elasticsearch. It could potentially help us if we received the wrong "data-type" for a specific parameter. So if we received a 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. I'm fine with it either way, I defer to your opinion :) 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. I've just tried to add a payload validation and was hit by this limitation 😢 That means we can't have I'll add a validation for 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. Sounds good!! |
||
|
||
// An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID | ||
// Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL | ||
// fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details | ||
// at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth | ||
let loginAttempt; | ||
if (query.authenticationResponseURI) { | ||
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 add 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. Replied above, but let me know if you think it makes sense to treat our own |
||
loginAttempt = { | ||
flow: OIDCAuthenticationFlow.Implicit, | ||
authenticationResponseURI: query.authenticationResponseURI, | ||
}; | ||
} else if (query.code || query.error) { | ||
// An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or | ||
// failed) authentication from an OpenID Connect Provider during authorization code authentication flow. | ||
// See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. | ||
loginAttempt = { | ||
flow: OIDCAuthenticationFlow.AuthorizationCode, | ||
// We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway. | ||
authenticationResponseURI: request.url.path, | ||
}; | ||
} else if (query.iss || payload.iss) { | ||
// An HTTP GET request with a query parameter named `iss` or an HTTP POST request with the same parameter in the | ||
// payload as part of a 3rd party initiated authentication. See more details at | ||
// https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin | ||
loginAttempt = { | ||
flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, | ||
iss: query.iss || payload.iss, | ||
loginHint: query.login_hint || payload.login_hint, | ||
}; | ||
} | ||
|
||
if (!loginAttempt) { | ||
throw Boom.badRequest('Unrecognized login attempt.'); | ||
} | ||
|
||
// We handle the fact that the user might get redirected to Kibana while already having an session | ||
// Return an error notifying the user they are already logged in. | ||
const authenticationResult = await login(KibanaRequest.from(request), { | ||
provider: 'oidc', | ||
// Checks if the request object represents an HTTP request regarding authentication with OpenID Connect. | ||
// This can be | ||
// - An HTTP GET request with a query parameter named `iss` as part of a 3rd party initiated authentication | ||
// - An HTTP POST request with a parameter named `iss` as part of a 3rd party initiated authentication | ||
// - An HTTP GET request with a query parameter named `code` as the response to a successful authentication from | ||
// an OpenID Connect Provider | ||
// - An HTTP GET request with a query parameter named `error` as the response to a failed authentication from | ||
// an OpenID Connect Provider | ||
value: { | ||
code: request.query && request.query.code, | ||
iss: (request.query && request.query.iss) || (request.payload && request.payload.iss), | ||
loginHint: | ||
(request.query && request.query.login_hint) || | ||
(request.payload && request.payload.login_hint), | ||
}, | ||
value: loginAttempt | ||
}); | ||
if (authenticationResult.succeeded()) { | ||
return Boom.forbidden( | ||
|
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.
note: didn't write unit tests for this endpoint as it's covered by integration tests and we'll need to rewrite them soon in Jest and TypeScript anyway (when we migrate to NP routes).