Re-posting this question as requested by @JasonStoltz
So I have everything working with enterprise-search (app-search) and have customized the generated search-ui. I've loaded in a couple million documents and everything works great. However the generated UI is working with the public search- bearer token on the client side (in the react app).
I would like to secure the ability to search with a login to restrict access to known/authorized users in our domain. I have a simple node/express app that authenticates our users and verifies they are within our domain (e.g. google oauth + directory API).
I understand that from app-search's perspective, signed search keys are the right approach here. I can certainly use the app-search-node lib to generate a per-user search key and then inject that into the react app (static) which is served from within the node app post-authorization. However I am wondering about key invalidation. I don't want that signed search key to continue to be valid after the session has expired.
Is there a recommended approach here for this type of case, I couldn't find any documentation about securing app search beyond giving managing all credentials in app search which is cumbersome since we already have oauth/directory based authentication/authorization with google.
We could just use a tunnel to expose search-UI/app-search without auth on our intranet but if we don't want 100% of our employees to have access...then that isn't a really good option either.
I'm also not a react developer and would like to avoid going down the path of adding a bunch more complexity to the React app and would rather handle authentication external to the React app and then serve the react app (static) with an injected valid (but expiring) credential if the user is authorized. Even that seems brittle as the React/app-search credential would need to somehow be kept in sync (valid state) with the cookie-based login session.
I originally thought the way to do this would be to proxy the app-search requests and inject the bearer token if the user is authorized and redirect them to a login screen if not... but it sounds like that is non trivial.
Would love to see a bare-bones writeup on how to secure search-UI for this type of use case as it would make integrating search-UI, including authorization into other larger (and possible non-React) apps much more obvious.
I originally thought the way to do this would be to proxy the app-search requests and inject the bearer token if the user is authorized and redirect them to a login screen if not... but it sounds like that is non trivial.
This seems like the way to handle this. Proxy all app search requests through your node server and handle authorization there and inject the bearer token (i.e., App Search Search Key).
We have an endpoint that manages issuing short lived signed search keys. This mitigates the key invalidation issue, that the tokens are only valid for 5 minutes.
Sorry for not writing back sooner. I just had time to try this out today.
This is great. 90% there. Wanted to report back this mostly works and specifically what I did for anyone else wanting to go this route.
The reference search-UI from latest enterprise-search release (7.13.3) doesn't include the searchKey optional patch (@elastic/search-ui-app-search-connector and @elastic/react-search-ui were at 1.4.1 I think) . So some npm install ____@latest is required. Didn't appear to break anything moving these deps to 1.7.0.
I removed the searchKey config and rely on the server/proxy to inject this if authorized.
the search UI is built (npm run build) and deployed as static site to be served by nginx under Location /.
I setup an nginx upstream under /api "Location" for enterprise-search with nginx auth_request directive pointing at /auth/test.
/auth/test is an internal-only path pointing at a nodejs app mounted at /auth (under nginx). Returns 204 or 401 depending on whether request is authorized. In the 204 case it also returns the bearer token in response header to nginx.
The bearer token is passed by response header on authorized requests (internal only) and then added to the (when authorized) proxy_pass request to enterprise-search upstream.
login (google oauth) is available from /auth path.
logout is available via /auth/logout
from user perspective
So if I go to: https://myapp.example.com/ I get a red error message from the react app saying: An unexpected error occurred: [401]
If I then go to https://myapp.example.com/auth and login with google it redirects me back to / and I can access the search app.
If I then go to https://myapp.example.com/auth/logout it redirects me back to /auth and prompts me to login again.
If I don't login and go to / I get the red 401 text again.
So the only thing really missing is getting the react app to handle the 401 error from the enterprise-search /api endpoint and redirect the user to the /auth path. It doesn't make sense to do the redirect from the nginx/nodejs end because the /api request is a fetch request.
I could proxy the static requests for the react app through node instead of serving directly with nginx (not optimal)...but that only helps with the first request when the app is first loaded and not the case where auth is lost without page reload.
I poked around in the various libs and this needs to be handled in @elastic/app-search-javascript or in one of the other libs in the stack (the connector or search-ui). I haven't seen an easy way to override the error handler yet...so if you have any tips off the top of your head that don't involve forking and patching the libraries...that would be appreciated. Otherwise I dig into that a bit more myself next week and see if I missed something on first look.
If searchKey wasn't optional, that would be the only way to go for sure.
I did think about that option but want to minimize the customization of the static react app as I am not a react developer and don't really want to become one. I imagine managing login and refreshing tokens from the react side involves a fair bit of customization.
But definitely a good option for some people. Thanks!
For anyone else if you wanted to follow the short lived token approach: the main thing you have to do is modify the AppSearchAPIConnector (search-ui/packages/search-ui-app-search-connector at master · elastic/search-ui · GitHub) and modify getClient with the logic of either returning the existing client if the token is not expired, or getting a new token, then calling ElasticAppSearch.createClient with the new token.
This is the same issue I've run into with handling 401 errors from the enterprise-search /api endpoint. I want to avoid maintaining a fork of a dependency and am hoping there is an existing way to hook into the client for error handling without having to modify dependencies. If there is, I think it will help us both as the authorization error (expired or no token) can be surfaced/handled in the main app (rather than in a dep).
I suppose these connectors are pretty small dependencies and so it probably isn't that big a deal to fork and modify them (ie not a lot of upstream changes or vulnerability issues that would need to be tracked and manually merged back in).
But maybe it makes sense to add an onError hook to the connector to make error handling more flexible? There are already other hooks for customizing functionality but they are all pre-search hooks intended for modifying the query before it goes to enterprise-search.
@JasonStoltz just in case you didn't have a chance to read my entire verbose reply, I wanted to give you a tl;dr on the remaining open question...
Do you know of an easy way to hook into the @elastic/app-search-javascript (or @elastic/search-ui-app-search-connector or @elastic/react-search-ui) error handling? I only see pre-search hooks in the connector. In particular on the 401 error case coming back from /api I would like to be able to do a client-side redirect to the authentication page rather than having search-ui show a red 401 error and relying on the user to know where to go to authenticate. I know I can fork/modify search-ui and its dependencies...but maybe some connector onError hook would be generally useful?
I'm hoping there is already a way to do this without modifying the dependencies but haven't seen anything obvious in the code.
Thanks again for getting me most of the way there.
Apache, Apache Lucene, Apache Hadoop, Hadoop, HDFS and the yellow elephant
logo are trademarks of the
Apache Software Foundation
in the United States and/or other countries.