-
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
Add support for OpenID Connect implicit authentication flow. #42069
Conversation
Pinging @elastic/kibana-security |
e30d427
to
653d4e8
Compare
.setRootController('oidc_implicit', ($scope: any, $window: any) => { | ||
const urlFragment: string = window.location.hash; | ||
|
||
// HACK: an ugly hack to overcome automatic Kibana URL fragment modifications. |
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: that the main thing I need to get rid of (hopefully) before this PR is ready for review
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.
Alternatively we could just render tiny HTML page outside of Kibana context with inline "nonced" onliner script.... That way we won't have rison that garbles URL fragment right now.
const startKibanaStateIndex = urlFragment.indexOf('?_g='); | ||
const endFragmentIndex = startKibanaStateIndex >= 0 ? startKibanaStateIndex : undefined; | ||
|
||
$window.location.href = chrome.addBasePath( |
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: I was thinking about using POST
here, but couldn't convince myself enough since all these args are already came through URL fragment 🤷♂️ Would be glad to hear any thoughts on this.
// - 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, | ||
value: request.query && request.query.redirectURI ? { |
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: actually, it probably makes sense to have a dedicated endpoint for implicit flow, haven't thought thoroughly about this.
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.
I'll defer to your opinion on this one. I don't think the current implementation is un-readable, but if you think it makes it easier to have a separate end-point, that seems reasonable also.
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.
I don't think the current implementation is un-readable
Great, I don't have strong opinion on that. Let's keep it as is for now then and split whenever we feel we really need to split.
@@ -6,9 +6,10 @@ | |||
|
|||
import chrome from 'ui/chrome'; | |||
|
|||
const paths = ['/login', '/logout', '/logged_out', '/status', '/oidc_implicit']; |
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: we should find a better way of solving the underlying problem, but not in this PR likely.
653d4e8
to
f82980a
Compare
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to do anything about server.customResponseHeaders
explicitly, or does the global hapi configuration apply automatically?
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to do anything about server.customResponseHeaders explicitly, or does the global hapi configuration apply automatically?
Hapi's onPreResponse
hooks do everything for us "automagically". I actually was surprised that CSP isn't done via onPreResponse
too... But I admit I don't know much about all the variables we had to account for when we were implementing it.
const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce); | ||
return h.response(` | ||
<script nonce="${nonce}"> | ||
const redirectURI = window.location.pathname + window.location.search + window.location.hash; |
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: we can just pass entire window.location.href
instead, it will also work and make this code a tad easier. Now when I look at this I can't think of any reason to not use entire window.location.href
🙈 What do you think?
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.
That seems easy enough to me, and potentially less fragile.
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.
Yep, will switch to window.location.href
.
f82980a
to
a47f882
Compare
const nonce = await generateCSPNonce(); | ||
const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce); | ||
return h.response(` | ||
<script nonce="${nonce}"> |
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: It seems it'd be better to include <!DOCTYPE html>
at least, will do.
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.
The w3c validator makes it seem like we should include <title>foo</title>
as well: https://validator.w3.org/nu/#textarea
Borrowing from https://mathiasbynens.be/notes/minimal-html who found whatwg/html@5c7cf94, the title
can be omitted when dealing with e-mails, but it sounds like we should be using it here.
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.
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 comment
The 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: Neither tag is omissible.
🙂
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.
I think the approach is fine. Perhaps long-term we can add the ability to render custom HTML content without all of the other "cruft" in the core application, but what we have now seems reasonable.
// - 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, | ||
value: request.query && request.query.redirectURI ? { |
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.
I'll defer to your opinion on this one. I don't think the current implementation is un-readable, but if you think it makes it easier to have a separate end-point, that seems reasonable also.
++, that's what we'll need to have for sure - custom HTML with only the absolutely necessary stuff (CSP, xsrf, HSTS, CORS etc. etc.). Not a blocker for now, but at some point managing this manually and outside of core's control in every such case will hurt us. |
a47f882
to
dcfabee
Compare
This comment has been minimized.
This comment has been minimized.
dcfabee
to
776cda2
Compare
💚 Build Succeeded |
@kobelb this PR should be ready for review whenever you have time 🙂 Thanks! |
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.
renovate portion LGTM
@@ -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 comment
The 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think?
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 payload.iss
which was an Object as opposed to a string, we could potentially have more informative error messages instead of something breaking in a somewhat obscure manner downstream.
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.
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 comment
The 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 payload
validation if route defines multiple methods and some of them don't support payload. The only alternative is to split this route into two very similar ones. Since you're fine with the current implementation I'll keep it simple and leave it as is for now. Let's get back to it when we migrate this route to NP (I leave a code comment).
I'll add a validation for authenticationResponseURI
query parameter as you suggested below though.
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.
Sounds good!!
|
||
const nonce = await generateCSPNonce(); | ||
const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce); | ||
return h.response(` |
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.
This is likely a naive question. When reading through the OIDC spec, section 3.2.2.5 states
When using the Implicit Flow, all response parameters are added to the fragment component of the Redirection URI, as specified in OAuth 2.0 Multiple Response Type Encoding Practices [OAuth.Responses], unless a different Response Mode was specified.
And section 2.1 of the OAuth2.0 Response Type Encoding Practices makes it appear that we could potentially use the query
response mode https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#rfc.section.2.1 to get around needing to use this page to parse the fragment.
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.
This is likely a naive question.
And what is the question? 🙂 So, I assume you're asking why we can't just say OP and RP (ES) to use response_mode=query
for the implicit flow. Per OAuth 2.0 Multiple Response Type Encoding Practices it seems that query is forbidden for id_token
and token
response types:
The default Response Mode for this Response Type is the fragment encoding and the query encoding MUST NOT be used.
cc @jkakavas
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.
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 comment
The 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 comment
The 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 response_type
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.
Thanks @jkakavas
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add authenticationResponseURI
to the route's query validation?
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.
Replied above, but let me know if you think it makes sense to treat our own authenticationResponseURI
differently.
💔 Build Failed |
💚 Build Succeeded |
💚 Build Succeeded |
7.x/7.4.0: 4c396f8 |
💚 Build Succeeded |
Kibana OIDC authentication provider currently supports Authentication using the Authorization Code Flow only. The main reason is that it is a recommended authentication flow and should be used by default whenever it's possible.
The authorization code flow implies that Elasticsearch has access to the Token endpoint of the OpenID Connect Provider (OP), but it's neither always possible nor desired. So in the scenarios like this having support for another, so called Implicit Flow would be beneficial.
This PR adds support for the OpenID Connect Authentication using the Implicit Flow. The tricky part here is that OP passes arguments required for the authentication using the implicit flow via URL fragment. That means we can't directly receive these arguments on the server side and have to provide a client-side proxy page that UA will be forwarded to and where we'll be extracting URL fragment and only then sending them to the Kibana Server OIDC endpoint to perform actual authentication.
Since this PR modifies OIDC authentication provider we should consider enabling OIDC integration tests first, that's blocked by #36959(tests were re-enabled).Notes to myself:
oidc/implicit
URL into ES Docs (Document support of OIDC Implicit flow in Kibana. elasticsearch#45693)oidc/implicit
URL into Cloud Docs (only when this PR is merged/backported)How to test
client_id
andclient_secret
):$ yarn es snapshot --license trial \ -E xpack.security.authc.token.enabled=true \ -E xpack.security.authc.realms.native.native1.order=0 \ -E xpack.security.authc.realms.oidc.oidc1.order=1 \ -E xpack.security.authc.realms.oidc.oidc1.rp.client_id=xxxx \ -E xpack.security.authc.realms.oidc.oidc1.rp.client_secret=xxxx \ -E xpack.security.authc.realms.oidc.oidc1.rp.response_type="id_token token" \ -E xpack.security.authc.realms.oidc.oidc1.rp.requested_scopes=profile \ -E xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri=https://localhost:5601/api/security/v1/oidc/implicit \ -E xpack.security.authc.realms.oidc.oidc1.op.authorization_endpoint=https://elastic-dev.auth0.com/authorize \ -E xpack.security.authc.realms.oidc.oidc1.op.token_endpoint=https://elastic-dev.auth0.com/oauth/token \ -E xpack.security.authc.realms.oidc.oidc1.op.userinfo_endpoint=https://elastic-dev.auth0.com/userinfo \ -E xpack.security.authc.realms.oidc.oidc1.op.issuer=https://elastic-dev.auth0.com/ \ -E xpack.security.authc.realms.oidc.oidc1.op.jwkset_path=https://elastic-dev.auth0.com/.well-known/jwks.json \ -E xpack.security.authc.realms.oidc.oidc1.claims.principal=sub \ -E xpack.security.authc.realms.oidc.oidc1.claims.groups=https://myapp.example.com/kbn_groups \ -E xpack.security.authc.realms.oidc.oidc1.claims.name=name
xpack.security.authc.providers: [oidc, basic]
Blocked by: #36959Fixes: #41984
"Release Note: Adding support for the OpenID Connect Authentication using the Implicit Flow"
/cc @jkakavas