Custom Realm - Building UsernamePasswordToken

Hello there,
I'm developing a custom realm based on

The plan is for browser to kibana to be HTTPS, but kibana to elasticsearch is HTTP.

I'm attempting to authenticate with
curl -H "User-Agent: Mozilla" -H "Origin: https://tangotelecom.com" -H "Host: tangotelecom.com:5601" -H "x-proxy-user: irldf" -H "Authorization: Basic YWRtaW46dIzNDU=" -i http://tangotelecom.com:9200

HTTP/1.1 401 Unauthorized
access-control-allow-origin: https://tangotelecom.com
access-control-allow-credentials: true
WWW-Authenticate: Basic realm=tango-ims-realm
content-type: application/json; charset=UTF-8
content-length: 353

{"error":{"root_cause":[{"type":"security_exception","reason":"unable to authenticate user [admin] for REST request [/]","header":{"WWW-Authenticate":"Basic realm=tango-ims-realm"}}],"type":"security_exception","reason":"unable to authenticate user [admin] for REST request [/]","header":{"WWW-Authenticate":"Basic realm=tango-ims-realm"}},"status":401}

From the logs I can see that in my IMSRealm class (which extends Realm) that #authenticate method seems to be getting invoked before #token method?

[2017-04-18T09:02:06,236][DEBUG][c.t.x.r.IMSRealm ] About to process request to authenticate.
[2017-04-18T09:02:06,236][DEBUG][c.t.x.r.IMSRealm ] Tenant and User are not available in the request.
[2017-04-18T09:02:06,242][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] Failed Authentication
[2017-04-18T09:02:06,242][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] URI:
[2017-04-18T09:02:06,242][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] Headers:
[2017-04-18T09:02:06,243][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] Accept=/
[2017-04-18T09:02:06,243][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] User-Agent=Mozilla
[2017-04-18T09:02:06,243][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] Origin=https://tangotelecom.com
[2017-04-18T09:02:06,243][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] Host:5601=tangotelecom.com
[2017-04-18T09:02:06,244][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] x-proxy-user=irldf
[2017-04-18T09:02:06,244][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] Authorization=Basic YWRtaW46dIzNDU=
[2017-04-18T09:02:06,244][DEBUG][c.t.x.r.IMSAuthenticationFailureHandler] content-length=0

....
[2017-04-18T09:02:18,301][DEBUG][c.t.x.r.IMSRealm ] Received request for Tenant: null
[2017-04-18T09:02:18,302][DEBUG][c.t.x.r.IMSRealm ] Received request for Tenant: null

The #token method seems to be getting invoked periodically?
It looks like #token method doesn't get invoked at all as part of the CURL request I sent in. The #token method builds the UsernamePasswordToken that I was expecting to be passed into the #authenticate method but perhaps I've misunderstood and this is not how it is meant to work?
The log 'Tenant and User are not available in the request.' is received when it is not possible to extract a tenant and user from the AuthenticationToken.

A code snippet looks like so:

public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String TENANT_ID_HEADER = "x-proxy-user";

@Override
public UsernamePasswordToken token(final ThreadContext threadContext) {
UsernamePasswordToken usernamePasswordToken = null;
final String tenantId = threadContext.getHeader(TENANT_ID_HEADER);
final String authorization = threadContext.getHeader(AUTHORIZATION_HEADER);
LOGGER.debug("Received request for Tenant: " + tenantId);
if (tenantId != null && authorization != null && authorization.startsWith("Basic")) {
final String base64Credentials = authorization.substring("Basic".length()).trim();
final String credentials = new String(Base64.getDecoder().decode(base64Credentials), Charset.forName("UTF-8"));
final String[] values = credentials.split(":", 2);
if (values.length == 2) {
LOGGER.debug("Received request for User: " + values[0]);
usernamePasswordToken = new UsernamePasswordToken(tenantId + "-" + values[0], new SecuredString(values[1].toCharArray()));
}
}
return usernamePasswordToken;
}

So only difference for normal is that I'm adding a tenant into the UsernamePasswordToken.

@Override
public User authenticate(@NonNull final AuthenticationToken authenticationToken) {
LOGGER.debug("About to process request to authenticate. ");
final UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
final String[] tenantAndUser = token.principal().split("-");
if(tenantAndUser.length != 2) {
LOGGER.debug("Tenant and User are not available in the request. ");
return null;
}
final String tenant = tenantAndUser[0];
final String actualUser = tenantAndUser[1];
...
}

This is not getting invoked in your realm since the reserved realm (elastic / kibana users) will extract the username password token from the request and no more realms will be looked at for a token.

I see how this can be problematic for your use case. One thing you could do is use a different header as a workaround. I think we'll need to make the threadcontext available when building the realm. It is thread safe and is backed by a thread local so it doesn't need to be passed on each request.

Thanks for your response Jay.
I can confirm that changing the 'Authorization' header name to something like 'Credentials' works for me so I'm happy.
I wouldn't have thought there was anything unusual about my use case as a lot of custom realm plugins are going to want to access tenant, username and password from HTTP headers I guess.
Also typically a plugin would probably want to de-serialize an Authorization DTO they requested from the third party system (using jackson for example) and they may want particular permissions set for that. In my case I updated, 'x-pack-extension-security.policy' with the following permissions so jackson would work

grant {
permission java.net.NetPermission "getProxySelector";
permission java.lang.RuntimePermission "getClassLoader";
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.lang.RuntimePermission "accessDeclaredMembers";
};

Thanks again for your support Jay. This plugin feature is a big plus for x-pack and much better IMHO than alternatives with proxies.

I realize now that there is a further problem with this higher up the stack. Even though my plugin successfully authenticates against a third party user-store, no kibana cookie is created so the browser can detect that I'm authenticated. Is it my responsibility to create the kibana cookie? I read some where that the structure of the cookie is not advertised as you guys reserve the right to change the structure in the future and to I don't want my plugin to then break. This is a duplicate of a question already asked but not answered

Hi John,

The kibana cookie aspect is really internal and I know that it will be changed again at some point in the near future.

Will your proxy be injecting the custom header for every request?

Jay

Hello Jay,
I guess that's the problem with my solution, the headers need to be present on every kibana ajax request?

Well at the moment my proxy is serving multiple tenants and users and is not injecting the headers. The headers are coming directly from the browser via login request. What I want to do is to authenticate with kibana via an ajax request with my custom headers, and then take the ajax response and open it in a new window/tab so a user of my app doesn't need to also provide credentials to kibana. The issue I guess is that when I put the response into a new window it gives me a 401 and kibana is requesting I enter credentials again. I was hoping to leverage the kibana cookie, as this browser has just authenticated, or perhaps I could intercept the http response with 401 and log in another time in the new window/tab but if I need my tenant and credentials on every request I might be in trouble.

If my approach isn't going to work I'm open to any alternatives you suggest. The option of having credentials injected by the proxy could potentially work if I set up multiple instances of kibana for each tenant. However, my initial hope/plan was to wait for kibana 5.4 as it is delivering a read-only kibana user which I think would allow users on multiple tenants to have read-only access to a single kibana instance and then they would just navigate to the dashboards that are relevant for them, and would not be able to see results for other tenants.

If I have to introduce a proxy location for multiple kibana instances and inject headers at proxy as there are no other alternatives then so be it. It would just be difficult synching up the credentials in the proxy with third-party user stores &c. Any guidance here would be a big help for me. Thanking you in advance,
//John.

As a test I updated my proxy to pass the headers required by my custom realm on every request. Those headers are x-proxy-user and Credentials headers. For reasons detailed at the start of this post I'm not using the Authorization header, and if the Authorization header is present my custom realm fails to authenticate as #token method is not called.

However, when my custom realm successfully authenticates I still get to the kibana login page? I'm wondering is it mandatory to use the Authorization header to login to kibana, and whether the .kibana cookie is only created if the Authorization header is present?

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.