From 636d1baec85ad05b44993d877df50a1a981207f3 Mon Sep 17 00:00:00 2001 From: Michael Marshall Date: Thu, 23 Jun 2022 23:01:51 -0500 Subject: [PATCH] Fix oauth2 forwarding (#77) ## Motivation In testing 2.1.1, I found that the forwarding did not work correctly when using `auth_mode: openidconnect`. Specifically, loading the main page would result in loading keycloak. These changes improve the `openidconnect` auth mode and ensure that the other auth modes still work. ## Changes * Simplify the `oauth2` configuration. Since we haven't actually released any of these configurations, I am changing them now. * Add the `openid` scope to the browser's request, since it is expected by some providers, like Okta. * Load only one of the `/api/v1/auth/token` endpoints. ## Verifying the change I verified these changes with all 4 auth modes an EKS cluster and against the DataStax Pulsar Helm Chart. --- README.md | 13 ++--- config/default.json | 11 ++-- dashboard/src/components/auth/login/auth.js | 1 + server/server.js | 60 ++++++++++----------- 4 files changed, 37 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 0961c9e..2766955 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,9 @@ In a geo-replication configuration, you will want to use the cluster name for th | server_config.kubernetes.service_port | | When using `k8s` auth_mode, specify a custom Kubernetes port. | | server_config.user_auth.username | | When using `user` auth_mode, the login user name. | | server_config.user_auth.password | | When using `user` auth_mode, the login password. | -| server_config.oauth2.enabled | | When using `openidconnect` set to `true` to forward token requests. | -| server_config.oauth2.hostname | | When using `openidconnect` set to your hostname ex: `localhost` | -| server_config.oauth2.forwardingPath | | When using `openidconnect` set to the path you need to forward to to get the token | -| server_config.oauth2.enableTls | | When using `openidconnect` set to `true` if you wish to use `HTTPS` | -| server_config.oauth2.http | | When using `openidconnect` and only using `HTTP` set to your port | -| server_config.oauth2.https | | When using `openidconnect` and using `HTTPS` set to your port | +| server_config.oauth2.identity_provider_url | `""` | When using `auth_mode: openidconnect` set to your hostname and port. ex: `https://keycloak:8443`| +| server_config.oauth2.token_endpoint | `""` | When using `auth_mode: openidconnect` set to the path you need to forward to to get the token. ex: `/token` | +| server_config.oauth2.grant_type | `password` | When using `auth_mode: openidconnect` set to the grant type. Only `password` is support at this time. | | polling_interval | 10000 | How often the console polls Pulsar for updated values. In milliseconds. | | ca_certificate | | String of CA certificate to display in the console under Credentials. | | api_version | 2.8.3 | Version of the Pulsar client API to recommend under Samples. | @@ -117,8 +114,8 @@ Once the user is authenticated using one of the Kubernetes secrets, the token fo ### Auth Mode: OpenID Connect In this auth mode, the dashboard will use your login credentials to attempt to retrieve a JWT from an authentication -provider. In the [DataStax Pulsar Helm Chart](https://github.com/datastax/pulsar-helm-chart), this is implemented by -integrating the Pulsar Admin Console with Keycloak. Upon successful retrieval of the JWT, the Pulsar Admin Console will +provider by following the `password` grant type. In the [DataStax Pulsar Helm Chart](https://github.com/datastax/pulsar-helm-chart), this is implemented by +integrating the Pulsar Admin Console with an identity provider, like Keycloak. Upon successful retrieval of the JWT, the Pulsar Admin Console will use the retrieved JWT as the bearer token when making calls to Pulsar. In addition to configuring the `auth_mode`, you also need to configure the `oauth_client_id`. This is the client id that diff --git a/config/default.json b/config/default.json index 8fe5a4a..d143fd0 100644 --- a/config/default.json +++ b/config/default.json @@ -2,7 +2,7 @@ "auth_mode": "none", "cluster_name": "standalone", "tenant": "public", - "oauth_client_id": "console", + "oauth_client_id": "", "server_config": { "port": "6454", "pulsar_url": "http://localhost:8080", @@ -28,12 +28,9 @@ "password": "" }, "oauth2": { - "enabled": false, - "hostname": "", - "forwardingPath": "/token", - "enableTls": false, - "httpPort": "", - "httpsPort": "" + "identity_provider_url": "", + "token_endpoint": "", + "grant_type": "password" } }, "polling_interval": "10000", diff --git a/dashboard/src/components/auth/login/auth.js b/dashboard/src/components/auth/login/auth.js index 63d3e43..f22eb80 100644 --- a/dashboard/src/components/auth/login/auth.js +++ b/dashboard/src/components/auth/login/auth.js @@ -75,6 +75,7 @@ export function getPulsarToken (accessToken) { function buildLoginBody(username, password) { if (globalConf.auth_mode === 'openidconnect') { return new URLSearchParams({ + scope: 'openid', username: username, password: password, client_id: globalConf.oauth_client_id, diff --git a/server/server.js b/server/server.js index 70ed232..b04dc02 100644 --- a/server/server.js +++ b/server/server.js @@ -192,23 +192,14 @@ app.use(`/api/v1/${cluster}/sources`, createProxyMiddleware({ selfHandleResponse: true })); -const keycloakTarget = cfg.globalConf.server_config.oauth2.enableTls ? - `https://${cfg.globalConf.server_config.oauth2.hostname}:${cfg.globalConf.server_config.oauth2.httpsPort}` : - `http://${cfg.globalConf.server_config.oauth2.hostname}:${cfg.globalConf.server_config.oauth2.httpPort}` - -app.use(createProxyMiddleware({ - target: keycloakTarget, - pathFilter: (path, req) => { - if (cfg.globalConf.server_config.oauth2.enabled && path.includes('/api/v1/auth/token')) { - return true; - } - return false; - }, - pathRewrite: (path, req) => { - return path.replace('/api/v1/auth/token', cfg.globalConf.server_config.oauth2.forwardingPath) - }, - secure: cfg.globalConf.server_config.ssl.verify_certs, -})) +if (cfg.globalConf.auth_mode === 'openidconnect' && cfg.globalConf.server_config.oauth2.grant_type === 'password') { + app.use('/api/v1/auth/token', createProxyMiddleware({ + target: cfg.globalConf.server_config.oauth2.identity_provider_url, + pathFilter: '/api/v1/auth/token', + pathRewrite: {'^/api/v1/auth/token': cfg.globalConf.server_config.oauth2.token_endpoint}, + changeOrigin: true, // By changingOrigin, we're able to make internal k8s networking work for the token + })) +} app.use(`/api/v1/${cluster}`, createProxyMiddleware({ target: cfg.globalConf.server_config.pulsar_url, @@ -284,24 +275,27 @@ app.post('/api/user', (req, res) => { res.json("user addedd"); }); -app.post('/api/v1/auth/token', async (req, res) => { - const username = req.body.username; - const password = req.body.password; - try { - if (username && password) { - if (await isUserAuthenticated(username, password)) { - const secret = process.env.TOKEN_SECRET || cfg.globalConf.server_config.token_secret || "default-secret" - // This loosely complies with https://openid.net/specs/openid-connect-core-1_0.html section 3.2.2.5. Successful Authentication Response - res.send({access_token: jwt.sign({user: username}, secret, {expiresIn: '12h'})}); - return; +// OpenID Connect has a different model defined above. +if (cfg.globalConf.auth_mode !== 'openidconnect') { + app.post('/api/v1/auth/token', async (req, res) => { + const username = req.body.username; + const password = req.body.password; + try { + if (username && password) { + if (await isUserAuthenticated(username, password)) { + const secret = process.env.TOKEN_SECRET || cfg.globalConf.server_config.token_secret || "default-secret" + // This loosely complies with https://openid.net/specs/openid-connect-core-1_0.html section 3.2.2.5. Successful Authentication Response + res.send({access_token: jwt.sign({user: username}, secret, {expiresIn: '12h'})}); + return; + } } + res.status(401).send("incorrect credentials"); + } catch (e) { + cfg.L.error(e); + res.status(401).send("login exception"); } - res.status(401).send("incorrect credentials"); - } catch (e) { - cfg.L.error(e); - res.status(401).send("login exception"); - } -}); + }); +} app.post('/auth', async (req, res) => { const username = req.body.username;