-
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
[skip ci] feedback on approach for token provider #25431
Conversation
Pinging @elastic/kibana-platform |
BasicCredentials.decorateRequest(request, username, password) | ||
); | ||
request.loginAttempt.setCredentials(username, password); | ||
const authenticationResult = await server.plugins.security.authenticate(request); |
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.
Rather than extracting the username and password from the payload and faking a basic auth header on the request to force a new login during authenticate
, login attempts in authenticate
will now be handled exclusively through the new loginAttempt
object on the request.
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 like this much more
import { canRedirectRequest } from '../../../lib/can_redirect_request'; | ||
|
||
export function initAuthenticateApi(server) { | ||
|
||
server.decorate('request', 'loginAttempt', (route, options) => { |
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.
Feedback on the loginAttempt
implementation is welcome, but it's not critical at this point. The internal details of this decorator are not yet my priority.
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.
question: I'm wondering if we want to make this a bit more generic/opaque (without hardcoding of username
and password
parameters, like we do state - every providers knows how to decode its state) so that we can use loginAttempt
for SAML Response payload as well?
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 that's a good thing to consider, but I'm not sure if I'll implement it that way initially. I'll ponder it more, but my current thinking is that broader refactoring across all authentication providers should wait until we have one more two more SSO-based providers like kerberos and oauth2. SAML's working pretty well at the moment, and at least currently this loginAttempt logic only matters to providers that explicitly need to power the login form.
* @returns {Promise.<AuthenticationResult>} | ||
* @private | ||
*/ | ||
async _authenticateViaLoginAttempt(request) { |
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 the key function that is invoked when login credentials exist for a request, which it retrieves from the loginAttempt
object on the request.
The basic provider would be updated to use this mechanism as well. The only real difference between this function in the token provider and the basic provider is the interaction with Elasticsearch using those credentials.
* @param {Object} [state] Optional state object associated with the provider. | ||
* @returns {Promise.<AuthenticationResult>} | ||
*/ | ||
async authenticate(request, state) { |
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 the entire proposed authentication flow for the token provider. It is sort of a merging of logic between the basic and saml providers.
It supports true basic auth headers, similar to both basic and saml.
It supports login from credentials, similar to the basic provider.
It supports token auth and token refresh similar to saml, but it does not include the handshake process handling that saml does.
I'd appreciate feedback on the high level logic in the flow itself.
Pinging @elastic/kibana-security |
@azasypkin @kobelb Can y'all take a look at this when you get a moment? Hopefully none of it is a surprise since it's based on the discussions I've had with both of you. |
* @param {Hapi.Request} request HapiJS request instance. | ||
* @returns {Promise.<DeauthenticationResult>} | ||
*/ | ||
async deauthenticate(request) { |
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.
We'll want to also invalidate the access/refresh tokens when they deauthenticate, like SAML does here: https://github.com/elastic/kibana/blob/master/x-pack/plugins/security/server/lib/authentication/providers/saml.js#L374
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.
Good catch
|
||
this._options.log(['debug', 'security', 'token'], 'Request has been authenticated via header.'); | ||
|
||
return AuthenticationResult.succeeded(user, { authorization }); |
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.
When we authenticate using the basic auth headers, I don't think that we want to put the user
into the session, as this would be putting the username/password encrypted in the session cookie, which can't be invalidated with the logout. We'll likely want to perform the same flow as the "authenticate via login attempt"
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 have to double check, but I think passing user
to succeeded
isn't the problem here as that shouldn't include the credentials (at least not the password). But I think passing authorization
here will have the effect you describe, so we definitely don't want that.
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 have to double check, but I think passing
user
tosucceeded
isn't the problem here as that shouldn't include the credentials (at least not the password)
It shouldn't be a problem, and moreover it's required for succeeded
and it's not used for session.
which can't be invalidated with the logout. We'll likely want to perform the same flow as the "authenticate via login attempt"
Hmm, correct me if I'm wrong, but if it's in cookie, the deauthenticate
that is called on logout should clear it as well. But having said that I agree that we don't want to store Bearer xxxx
in the cookie when we authenticate user via header
(and should remove this "feature" from basic provider auth via header
as well since now we have login attempt
thing), but if we don't store it in the session cookie during "login attempt", where do we get access/refresh tokens from for the next request?
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.
Agreed on the user
vs authenticated
point, I was wrong.
But having said that I agree that we don't want to store Bearer xxxx in the cookie when we authenticate user via header (and should remove this "feature" from basic provider auth via header as well since now we have login attempt thing), but if we don't store it in the session cookie during "login attempt", where do we get access/refresh tokens from for the next request?
So, just to be clear, we're talking about not using tokens when the user authenticates using basic auth headers, and additionally not storing the username/password in the session cookie itself; but instead requiring subsequent requests to provide the basic auth headers themselves?
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.
Hmm, I was talking about two cases:
-
_authenticateViaHeader
- here we expect to receiveAuthorization: Bearer xxxx
and hence we shouldn't store anything in the cookie since we expect every request to have this header (e.g. they have some OAuth proxy in front of Kibana that supplies it). Currently basic auth provider stores it in the cookie, but withloginAttempt
we can finally fix that. -
_authenticateViaLoginAttempt
- here we expect to receive credentials that we exchange for the token pair. It's probably one time action (e.g. initiated from the login page) so we should store token pair in the cookie (and "forget" credentials) and allow consequent requests to rely on generated tokens from the cookie instead.
Or I'm misunderstanding something? :)
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 believe we're on the same page, I forgot that we could see either the Authorization: Bearer xxxx
or the Authorization: Basic xxxx
headers when using the _authenticateViaHeader
method, but I believe it makes sense to not store these in the cookie in either situation.
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 believe we're on the same page
Great!
, but I believe it makes sense to not store these in the cookie in either situation.
Yes, that's where loginAttempt
that we didn't have before can help us.
BasicCredentials.decorateRequest(request, username, password) | ||
); | ||
request.loginAttempt.setCredentials(username, password); | ||
const authenticationResult = await server.plugins.security.authenticate(request); |
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 like this much more
if (authenticationResult.notHandled()) { | ||
authenticationResult = await this._authenticateViaState(request, state); | ||
if (authenticationResult.failed() && isAccessTokenExpiredError(authenticationResult.error)) { | ||
authenticationResult = this._authenticateViaRefreshToken(request, state); |
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 not seeing the _authenticateViaRefreshToken
implementation yet. It's likely this is known and intentional, but I wanted to note it just in case it wasn't.
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 yeah, I meant to call that out. It's not implemented yet, but it's essentially a copy+paste job from saml.
} | ||
|
||
const authenticationSchema = authorization.split(/\s+/)[0]; | ||
if (authenticationSchema.toLowerCase() !== 'basic') { |
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.
nit: basic
---> bearer
or you want this provider to support both or just basic
for some reason? If someone is using Basic ...
header directly I think they are supposed to use basic
auth provider. Using Bearer
here would allow us to use any ES realm that understands tokens (e.g custom OAuth realms + OAuth proxy in front of Kibana).
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 double checked the SAML provider logic and sure enough it uses bearer
. I was under the impression that we had hardcoded true basic auth header support into each provider to make sure people could still do API calls programmatically while also supporting UI-based auth workflows, but clearly that's not the case.
Unfortunately, this gets more tricky with basic/token providers. The login form itself can only work with one of those two providers, so I worry about the subtly in ordering of those providers in the configuration accidentally exposing people to issues they didn't realize. Like when you switch between token and basic auth for the login form, they will appear to work in exactly the same way, and you'd need to examine the contents of the encrypted cookie (not possible for end users) to even know which provider was handling login.
I wonder if it makes sense to introduce an explicit login handling configuration so you can configure things like basic and token providers at the same time while simultaneously ensuring that the login form only works with one specific provider.
Thoughts?
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.
Just a couple of thoughts:
The order of auth providers in [basic, token]
or [token, basic]
only influences login page, for programmatic API it won't matter (basic
intentionally fails if it notices non-empty Authorization
header that it doesn't understand to allow next provider to handle that, token
should do the same). For [saml, token]
the order will matter though. But as far as I understand the order of realms can be tricky for ES as well and administrators should know what they do when they configure the order of realms.
Having said that, I agree, "the first auth provider always manages login" rule isn't obvious, we can introduce config option to make it explicit which auth provider should handle login and then we can "sign" loginAttempt
with provider name/id (like we do for session) based on this config. Otherwise there may be an issue when provider can try to use loginAttempt
to authenticate user just because its shape looks like what provider expects even though it was supposed to be used by another provider (idk, like ldap-based auth provider that expects username and password and basic that expects the same).
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 feel like I'm missing something. I thought this provider was going to essentially be a "basic auth provider using tokens for sessions internally". In what situations would the requests from the end-users browser be presenting "bearer" authorization headers?
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.
@kobelb That was how I was thinking about it originally, but I'm coming around to the idea that the token provider is not a special basic auth provider at all. So the idea would be that if you want to support both token auth and basic auth, you'd have to enable both providers. You could lock out basic auth entirely if you wanted.
With this in mind, we don't have to support header-based auth at all with the token provider. The only real use case I can think of would be people building custom SSO solutions (ldap perhaps?) that leverage the token provider rather than the basic provider as they do today. Correct me if I'm wrong about that possibility, 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.
@epixa gotcha, I'm with you all now, thanks for catching me up.
Allowing custom SSO using the header makes sense conceptually, I don't see any real harm in including it if it opens up this possibility. I honestly haven't played around with various intelligent reverse proxies that would make this type of workflow possible enough to know the feasibility, but it'd be a ton nicer than the run-as
approach that we have now.
Replaced with #25971 |
do not merge
I highlighted the key parts via inline comments.
This isn't a functional change yet, so don't bother running it. I'm looking for feedback on the authentication flow for the token provider and the proposed change to how login attempts are handled in authentication as configured through the login HTTP endpoint.