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 support for OpenID Connect implicit authentication flow. #42069

Merged
merged 7 commits into from
Aug 8, 2019

Conversation

azasypkin
Copy link
Member

@azasypkin azasypkin commented Jul 26, 2019

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:


How to test

  1. Run Elasticsearch with (ping me to get values for client_id and client_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
  1. Add role mapping for users authenticated with OIDC realm, e.g.:
POST http://localhost:9200/_security/role_mapping/oidc1
Content-Type: application/json

{
  "roles": [ "superuser" ],
  "enabled": true,
  "rules": { "field" : { "realm.name" : "oidc1" } }
}
  1. Run Kibana with xpack.security.authc.providers: [oidc, basic]

Blocked by: #36959
Fixes: #41984

"Release Note: Adding support for the OpenID Connect Authentication using the Implicit Flow"

/cc @jkakavas

@azasypkin azasypkin added blocked Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! Feature:Security/Authentication Platform Security - Authentication Feature:New Feature New feature not correlating to an existing feature label v7.5.0 labels Jul 26, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-security

.setRootController('oidc_implicit', ($scope: any, $window: any) => {
const urlFragment: string = window.location.hash;

// HACK: an ugly hack to overcome automatic Kibana URL fragment modifications.
Copy link
Member Author

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

Copy link
Member Author

@azasypkin azasypkin Jul 29, 2019

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(
Copy link
Member Author

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 ? {
Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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'];
Copy link
Member Author

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.

@azasypkin azasypkin force-pushed the issue-xxx-oidc-implicit-flow branch from 653d4e8 to f82980a Compare July 30, 2019 13:33
const basePath = legacyConfig.get('server.basePath');

const nonce = await generateCSPNonce();
const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce);
Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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;
Copy link
Member Author

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?

Copy link
Contributor

@kobelb kobelb Jul 30, 2019

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.

Copy link
Member Author

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.

@azasypkin
Copy link
Member Author

@kobelb would you mind giving a preliminary feedback on the approach itself? Feel free to ping me to get user credentials and client secret for the test environment.

Please, skip changes in @kbn/es, @kbn/test and x-pack/test - these are from #42239 that is the blocker for the current PR.

const nonce = await generateCSPNonce();
const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules'), nonce);
return h.response(`
<script nonce="${nonce}">
Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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!

Copy link
Member Author

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. 🙂

@kobelb kobelb self-requested a review July 30, 2019 18:46
Copy link
Contributor

@kobelb kobelb left a 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 ? {
Copy link
Contributor

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.

@azasypkin
Copy link
Member Author

Perhaps long-term we can add the ability to render custom HTML content without all of the other "cruft" in the core application

++, 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.

@azasypkin azasypkin force-pushed the issue-xxx-oidc-implicit-flow branch from a47f882 to dcfabee Compare August 1, 2019 15:25
@azasypkin azasypkin marked this pull request as ready for review August 1, 2019 15:25
@azasypkin azasypkin requested a review from a team as a code owner August 1, 2019 15:25
@elasticmachine

This comment has been minimized.

@azasypkin azasypkin force-pushed the issue-xxx-oidc-implicit-flow branch from dcfabee to 776cda2 Compare August 1, 2019 15:44
@azasypkin azasypkin requested a review from a team as a code owner August 1, 2019 15:44
@azasypkin azasypkin added v7.4.0 and removed v7.5.0 labels Aug 1, 2019
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@azasypkin azasypkin requested a review from kobelb August 2, 2019 12:02
@azasypkin
Copy link
Member Author

@kobelb this PR should be ready for review whenever you have time 🙂 Thanks!

Copy link
Member

@jbudz jbudz left a 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 || {};
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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 :)

Copy link
Member Author

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.

Copy link
Contributor

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(`
Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

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.

Copy link
Member

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

Copy link
Contributor

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) {
Copy link
Contributor

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?

Copy link
Member Author

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.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@azasypkin
Copy link
Member Author

7.x/7.4.0: 4c396f8

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backported Feature:New Feature New feature not correlating to an existing feature label Feature:Security/Authentication Platform Security - Authentication release_note:enhancement Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v7.4.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants