Currently I have a working solution with following components:
This allows me to authenticate using keycloak.
Because my webserver also exposes a websocket host, I would like to authenticate these websockets as well. Does anyone have an example (both the nginx file as the lua file) available to authenticate websocket connections using openresty? I've had a look at https://github.com/openresty/lua-resty-websocket but can't seem to find where to plugin in the authentication part.
An example client application to test this would be great as well!
I've figured it out myself. Posting my solution here to help others achieving the same.
I have following code snippets:
only for websocket, should be place inside the server section:
set $resty_user 'not_authenticated_resty_user';
location /ws {
access_by_lua_file /usr/local/openresty/nginx/conf/lua_access.lua;
proxy_pass http://<backend-websocket-host>/ws;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwared-User $resty_user;
proxy_read_timeout 1d;
proxy_send_timeout 1d;
}
local opts = {
redirect_uri = "/*",
discovery = "http://<keycloak-url>/auth/realms/realm/.well-known/openid-configuration",
client_id = "<client-id>",
client_secret = "<client-secret>",
redirect_uri_scheme = "https",
logout_path = "/logout",
redirect_after_logout_uri = "http://<keycloak-url>/auth/realms/realm/protocol/openid-connect/logout?redirect_uri=http%3A%2F%2google.com",
redirect_after_logout_with_id_token_hint = false,
session_contents = {id_token=true},
ssl_verify=no
}
-- call introspect for OAuth 2.0 Bearer Access Token validation
local res, err = require("resty.openidc").bearer_jwt_verify(opts)
if err or not res then
print("Token authentication not succeeded")
if err then
print("jwt_verify error message:")
print(err)
end
if res then
print("jwt_verify response:")
tprint(res)
end
res, err = require("resty.openidc").authenticate(opts)
if err then
ngx.status = 403
ngx.say(err)
ngx.exit(ngx.HTTP_FORBIDDEN)
end
end
if res.id_token and res.id_token.preferred_username then
ngx.var.resty_user = res.id_token.preferred_username
else
ngx.var.resty_user = res.preferred_username
end
This allows websocket connections only when they have a valid token retrieved from the keycloak service.
At the end, the resty user is filled in to pass on the authenticated user to the backend application.
package test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.AccessTokenResponse;
public class KeycloakConnection {
private Keycloak _keycloak;
public KeycloakConnection(final String host, String username, String password, String clientSecret, String realm, String clientId) {
_keycloak = Keycloak.getInstance(
"http://" + host + "/auth",
realm,
username,
password,
clientId,
clientSecret);
}
public String GetAccessToken()
{
final AccessTokenResponse accessToken = _keycloak.tokenManager().getAccessToken();
return accessToken.getToken();
}
}
This snippet only contains the function I call to setup the websocket connection. You still have to instantiate the _keycloakConnection object and in my case I have a general _session field to keep reuse the session each time I need it.
private Session GetWebsocketSession(String host)
{
URI uri = URI.create("wss://" + host);
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader("Authorization", "Bearer " + _keycloakConnection.GetAccessToken());
_client = new WebSocketClient();
try {
_client.start();
// The socket that receives events
WebsocketEventHandler socketEventHandler = new WebsocketEventHandler(this::NewLiveMessageReceivedInternal);
// Attempt Connect
Future<Session> fut = _client.connect(socketEventHandler, uri, request);
// Wait for Connect
_session = fut.get();
return _session;
} catch (Throwable t) {
_logger.error("Error during websocket session creation", t);
}
return null;
}
A consumer is injected in this class to consume the messages in another class
package test;
import org.apache.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import java.util.function.Consumer;
public class WebsocketEventHandler extends WebSocketAdapter
{
private final Logger _logger;
private Consumer<String> _onMessage;
public WebsocketEventHandler(Consumer<String> onMessage) {
_onMessage = onMessage;
_logger = Logger.getLogger(WebsocketEventHandler.class);
}
@Override
public void onWebSocketConnect(Session sess)
{
super.onWebSocketConnect(sess);
_logger.info("Socket Connected: " + sess);
}
@Override
public void onWebSocketText(String message)
{
super.onWebSocketText(message);
_logger.info("Received TEXT message: " + message);
_onMessage.accept(message);
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
super.onWebSocketClose(statusCode,reason);
_logger.info("Socket Closed: [" + statusCode + "] " + reason);
}
@Override
public void onWebSocketError(Throwable cause)
{
super.onWebSocketError(cause);
_logger.error("Websocket error", cause);
}
}
When the _session is created you can use following line to send data:
_session.getRemote().sendString("Hello world");
These snippets are all a small part of my whole solution. I might have missed something. If somebody has a question or this is not working in your case, please reach out and I'll provide more information.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With