From b9902e3fa0f6812c6687c7a9df73910f37ce06ac Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Thu, 9 May 2024 05:58:52 +0000 Subject: [PATCH 1/5] Enable public application OIDC client support This change uses oidc-client-ts to enable dashy to authenticate with as OIDC client. It populates the groups and roles so that it can be used the same as keycloak for showing/hiding elements on the dashboard. --- docs/authentication.md | 41 +++++++++++ docs/configuring.md | 11 +++ package.json | 3 +- src/components/Settings/AuthButtons.vue | 15 +++++ src/main.js | 9 ++- src/utils/Auth.js | 5 +- src/utils/ConfigSchema.json | 27 ++++++++ src/utils/OidcAuth.js | 90 +++++++++++++++++++++++++ src/utils/defaults.js | 1 + 9 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 src/utils/OidcAuth.js diff --git a/docs/authentication.md b/docs/authentication.md index 4430d9b0..016e905f 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -251,6 +251,47 @@ Your app is now secured :) When you load Dashy, it will redirect to your Keycloa From within the Keycloak console, you can then configure things like time-outs, password policies, etc. You can also backup your full Keycloak config, and it is recommended to do this, along with your Dashy config. You can spin up both Dashy and Keycloak simultaneously and restore both applications configs using a `docker-compose.yml` file, and this is recommended. +## OIDC + +Dashy also supports using a general [OIDC compatible](https://openid.net/connect/) authentication server. In order to use it, the authentication section needs to be configured: + +```yaml +appConfig: + auth: + enableOidc: true + oidc: + clientId: [registered client id] + endpoint: [OIDC endpoint] +``` + +Because Dashy is a SPA, a [public client](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1) registration with PKCE is needed. + +An example for Authelia is shared below, but other OIDC systems can be used: + +```yaml +identity_providers: + oidc: + clients: + - client_id: dashy + client_name: dashy + public: true + authorization_policy: 'one_factor' + require_pkce: true + pkce_challenge_method: 'S256' + redirect_uris: + - https://dashy.local # should point to your dashy endpoint + grant_types: + - authorization_code + scopes: + - 'openid' + - 'profile' + - 'roles' + - 'email' + - 'groups' +``` + +Groups and roles will be populated and available for controlling display similar to [Keycloak](#Keycloak) abvoe. + --- ## Alternative Authentication Methods diff --git a/docs/configuring.md b/docs/configuring.md index 3bcb0d4a..acf93575 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -158,6 +158,8 @@ The following file provides a reference of all supported configuration options. **`keycloak`** | `object` | _Optional_ | Config options to point Dashy to your Keycloak server. Requires `enableKeycloak: true`. See [`auth.keycloak`](#appconfigauthkeycloak-optional) for more info **`enableHeaderAuth`** | `boolean` | _Optional_ | If set to `true`, then authentication using HeaderAuth will be enabled. Note that you need to have your web server/reverse proxy running, and have also configured `auth.headerAuth`. Defaults to `false` **`headerAuth`** | `object` | _Optional_ | Config options to point Dashy to your headers for authentication. Requires `enableHeaderAuth: true`. See [`auth.headerAuth`](#appconfigauthheaderauth-optional) for more info +**`enableOidc`** | `boolean` | _Optional_ | If set to `true`, then authentication using OIDC will be enabled. Note that you need to have a configured OIDC server and configure it with `auth.oidc`. Defaults to `false` +**`oidc`** | `object` | _Optional_ | Config options to point Dash to your OIDC configuration. Request `enableOidc: true`. See [`auth.oidc`](#appconfigauthoidc-optional) for more info **`enableGuestAccess`** | `boolean` | _Optional_ | When set to `true`, an unauthenticated user will be able to access the dashboard, with read-only access, without having to login. Requires `auth.users` to be configured. Defaults to `false`. For more info, see the **[Authentication Docs](/docs/authentication.md)** @@ -194,6 +196,15 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)** **[⬆️ Back to Top](#configuring)** +## `appConfig.auth.oidc` _(optional)_ + +**Field** | **Type** | **Required**| **Description** +--- | --- | --- | --- +**`clientId`** | `string` | Required | The client id registered in the OIDC server +**`endpoint`** | `string` | Required | The URL of the OIDC server that should be used. + +**[⬆️ Back to Top](#configuring)** + ## `appConfig.webSearch` _(optional)_ **Field** | **Type** | **Required**| **Description** diff --git a/package.json b/package.json index b8cd6c3b..9851af55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dashy", - "version": "3.0.1", + "version": "3.1.0", "license": "MIT", "main": "server", "author": "Alicia Sykes (https://aliciasykes.com)", @@ -30,6 +30,7 @@ "frappe-charts": "^1.6.2", "js-yaml": "^4.1.0", "keycloak-js": "^20.0.3", + "oidc-client-ts": "^3.0.1", "register-service-worker": "^1.7.2", "remedial": "^1.0.8", "rss-parser": "3.13.0", diff --git a/src/components/Settings/AuthButtons.vue b/src/components/Settings/AuthButtons.vue index ad355d6c..37a9f166 100644 --- a/src/components/Settings/AuthButtons.vue +++ b/src/components/Settings/AuthButtons.vue @@ -24,6 +24,13 @@ v-tooltip="tooltip($t('settings.sign-out-tooltip'))" class="layout-icon" tabindex="-2" /> + + @@ -32,6 +39,7 @@ import router from '@/router'; import { logout as registerLogout } from '@/utils/Auth'; import { getKeycloakAuth } from '@/utils/KeycloakAuth'; +import { getOidcAuth } from '@/utils/OidcAuth'; import { localStorageKeys, userStateEnum } from '@/utils/defaults'; import IconLogout from '@/assets/interface-icons/user-logout.svg'; @@ -56,6 +64,13 @@ export default { router.push({ path: '/login' }); }, 500); }, + oidcLogout() { + const oidc = getOidcAuth(); + this.$toasted.show(this.$t('login.logout-message')); + setTimeout(() => { + oidc.logout(); + }, 500); + }, keycloakLogout() { const keycloak = getKeycloakAuth(); this.$toasted.show(this.$t('login.logout-message')); diff --git a/src/main.js b/src/main.js index e112b4a8..5d9b6bae 100644 --- a/src/main.js +++ b/src/main.js @@ -22,6 +22,7 @@ import clickOutside from '@/directives/ClickOutside'; // Directive for closing p import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/utils/defaults'; import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth'; import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth'; +import { initOidcAuth, isOidcEnabled } from '@/utils/OidcAuth'; import Keys from '@/utils/StoreMutations'; import ErrorHandler from '@/utils/ErrorHandler'; @@ -62,7 +63,13 @@ const mount = () => new Vue({ }).$mount('#app'); store.dispatch(Keys.INITIALIZE_CONFIG).then(() => { - if (isKeycloakEnabled()) { // If Keycloak is enabled, initialize auth + if (isOidcEnabled()) { + initOidcAuth() + .then(() => mount()) + .catch((e) => { + ErrorHandler('Failed to authenticate with OIDC', e); + }); + } else if (isKeycloakEnabled()) { // If Keycloak is enabled, initialize auth initKeycloakAuth() .then(() => mount()) .catch((e) => { diff --git a/src/utils/Auth.js b/src/utils/Auth.js index d2b5ce1d..076b99d1 100644 --- a/src/utils/Auth.js +++ b/src/utils/Auth.js @@ -3,6 +3,7 @@ import ConfigAccumulator from '@/utils/ConfigAccumalator'; import ErrorHandler from '@/utils/ErrorHandler'; import { cookieKeys, localStorageKeys, userStateEnum } from '@/utils/defaults'; import { isKeycloakEnabled } from '@/utils/KeycloakAuth'; +import { isOidcEnabled } from '@/utils/OidcAuth'; /* Uses config accumulator to get and return app config */ const getAppConfig = () => { @@ -96,7 +97,7 @@ export const isAuthEnabled = () => { /* Returns true if guest access is enabled */ export const isGuestAccessEnabled = () => { const appConfig = getAppConfig(); - if (appConfig.auth && typeof appConfig.auth === 'object' && !isKeycloakEnabled()) { + if (appConfig.auth && typeof appConfig.auth === 'object' && !isKeycloakEnabled() && !isOidcEnabled()) { return appConfig.auth.enableGuestAccess || false; } return false; @@ -229,8 +230,10 @@ export const getUserState = () => { loggedIn, guestAccess, keycloakEnabled, + oidcEnabled, } = userStateEnum; // Numeric enum options if (isKeycloakEnabled()) return keycloakEnabled; // Keycloak auth configured + if (isOidcEnabled()) return oidcEnabled; if (!isAuthEnabled()) return notConfigured; // No auth enabled if (isLoggedIn()) return loggedIn; // User is logged in if (isGuestAccessEnabled()) return guestAccess; // Guest is viewing diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json index c344261e..6d373227 100644 --- a/src/utils/ConfigSchema.json +++ b/src/utils/ConfigSchema.json @@ -541,6 +541,33 @@ ] } }, + "enableOidc": { + "title": "Enable OIDC?", + "type": "boolean", + "default": false, + "description": "If set to true, enable OIDC. See appConfig.auth.oidc" + }, + "oidc": { + "type": "object", + "description": "Configuration for OIDC", + "additionalProperties": false, + "required": [ + "clientId", + "endpoint" + ], + "properties": { + "endpoint": { + "title": "OIDC Endpoint", + "type": "string", + "description": "Endpoint of OIDC provider" + }, + "clientId": { + "title": "OIDC Client Id", + "type": "string", + "description": "ClientId from OIDC provider" + } + } + }, "enableHeaderAuth": { "title": "Enable HeaderAuth?", "type": "boolean", diff --git a/src/utils/OidcAuth.js b/src/utils/OidcAuth.js new file mode 100644 index 00000000..9cec0959 --- /dev/null +++ b/src/utils/OidcAuth.js @@ -0,0 +1,90 @@ +import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; +import ConfigAccumulator from '@/utils/ConfigAccumalator'; +import { localStorageKeys } from '@/utils/defaults'; +import ErrorHandler from '@/utils/ErrorHandler'; +import { statusMsg, statusErrorMsg } from '@/utils/CoolConsole'; + +const getAppConfig = () => { + const Accumulator = new ConfigAccumulator(); + const config = Accumulator.config(); + return config.appConfig || {}; +}; + +class OidcAuth { + constructor() { + const { auth } = getAppConfig(); + const { clientId, endpoint } = auth.oidc; + const settings = { + userStore: new WebStorageStateStore({ store: window.localStorage }), + authority: endpoint, + client_id: clientId, + redirect_uri: `${window.location.origin}`, + response_type: 'code', + scope: 'openid profile email roles groups', + response_mode: 'query', + filterProtocolClaims: true, + }; + + this.userManager = new UserManager(settings); + } + + async login() { + const url = new URL(window.location.href); + const code = url.searchParams.get('code'); + + if (code) { + await this.userManager.signinCallback(window.location.href); + window.location.href = '/'; + return; + } + + const user = await this.userManager.getUser(); + + if (user === null) { + await this.userManager.signinRedirect(); + } else { + const { roles, groups } = user.profile; + const info = { + groups, + roles, + }; + + statusMsg(`user: ${user.profile.preferred_username}`, JSON.stringify(info)); + + localStorage.setItem(localStorageKeys.KEYCLOAK_INFO, JSON.stringify(info)); + localStorage.setItem(localStorageKeys.USERNAME, user.profile.preferred_username); + } + } + + async logout() { + localStorage.removeItem(localStorageKeys.USERNAME); + localStorage.removeItem(localStorageKeys.KEYCLOAK_INFO); + + try { + await this.userManager.signoutRedirect(); + } catch (reason) { + statusErrorMsg('logout', 'could not log out. Redirecting to OIDC instead', reason); + window.location.href = this.userManager.settings.authority; + } + } +} + +export const isOidcEnabled = () => { + const { auth } = getAppConfig(); + if (!auth) return false; + return auth.enableOidc || false; +}; + +let oidc; + +export const initOidcAuth = () => { + oidc = new OidcAuth(); + return oidc.login(); +}; + +export const getOidcAuth = () => { + if (!oidc) { + ErrorHandler("OIDC not initialized, can't get instance of class"); + } + return oidc; +}; diff --git a/src/utils/defaults.js b/src/utils/defaults.js index eafec4d0..e28c0326 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -305,6 +305,7 @@ module.exports = { guestAccess: 2, notLoggedIn: 3, keycloakEnabled: 4, + oidcEnabled: 5, }, /* Progressive Web App settings, used by Vue Config */ pwa: { From be9a9969287a86d227d4696efd3e36ebc1e62190 Mon Sep 17 00:00:00 2001 From: liss-bot Date: Sun, 12 May 2024 02:27:39 +0100 Subject: [PATCH 2/5] :purple_heart: Updates contributors list --- docs/credits.md | 118 ++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/docs/credits.md b/docs/credits.md index 543efd6d..b6814138 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -32,28 +32,21 @@ Torgny Bjers - - - emlazzarin -
- Eddy Lazzarin -
- AnandChowdhary
Anand Chowdhary
- - + shrippen
Null
- + + bile0026 @@ -88,15 +81,15 @@
Null
- - + bmcgonag
Brian McGonagill
- + + vlad-timofeev @@ -131,15 +124,15 @@
Göksel Yeşiller
- - + allesauseinerhand
Null
- + + forwardemail @@ -174,15 +167,15 @@
Null
- - + frankdez93
Null
- + + terminaltrove @@ -612,13 +605,6 @@ Alessandro Del Prete - - - turnrye -
- Ryan Turner -
- sachahjkl @@ -639,15 +625,15 @@
Shawn Salat
- - + royshreyaa
Null
- + + Smexhy @@ -676,14 +662,28 @@ Steven Kast + + + twsouthwick +
+ Taylor Southwick +
+ + + + turnrye +
+ Ryan Turner +
+ + rubjo
Null
- - + PrynsTag @@ -718,15 +718,15 @@
Michael D
- + + miclav
Michael Lavaire
- - + imsakg @@ -734,13 +734,6 @@ Mert Sefa AKGUN - - - maximemoreillon -
- Maxime Moreillon -
- AmadeusGraves @@ -870,6 +863,13 @@ Xert + + + maximemoreillon +
+ Maxime Moreillon +
+ emiran-orange @@ -890,15 +890,15 @@
Dylan Bersans
- + + dyauss
Thandy Norberto
- - + dougaldhub @@ -933,15 +933,15 @@
David
- + + clsty
Celestial.y
- - + bskim45 @@ -976,15 +976,15 @@
Artyom
- + + alydemah
Aly Mohamed
- - + 5idereal @@ -1019,15 +1019,15 @@
Мирослав Асенов
- + + luispabon
Luis Pabon
- - + LeoColman @@ -1062,15 +1062,15 @@
Jemy SCHNEPP
- + + jjmung
JJ Munguia
- - + b1thunt3r @@ -1105,15 +1105,15 @@
Harald Töpfer
- + + gbrown09
Garrett Brown
- - + FormatToday From e151729cd87f553f47cc9c34de5025efece015c4 Mon Sep 17 00:00:00 2001 From: liss-bot Date: Sun, 12 May 2024 02:27:41 +0100 Subject: [PATCH 3/5] :yellow_heart: Updates sponsors table --- README.md | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a20d9e06..69d0be23 100644 --- a/README.md +++ b/README.md @@ -553,28 +553,21 @@ Huge thanks to the sponsors helping to support Dashy's development! Torgny Bjers - - - emlazzarin -
- Eddy Lazzarin -
- AnandChowdhary
Anand Chowdhary
- - + shrippen
Shrippen
- + + bile0026 @@ -609,15 +602,15 @@ Huge thanks to the sponsors helping to support Dashy's development!
Araguaci
- - + bmcgonag
Brian McGonagill
- + + vlad-timofeev @@ -652,15 +645,15 @@ Huge thanks to the sponsors helping to support Dashy's development!
Göksel Yeşiller
- - + allesauseinerhand
Allesauseinerhand
- + + lamtrinhdev @@ -695,8 +688,7 @@ Huge thanks to the sponsors helping to support Dashy's development!
Nixy
- - + nrvo From 5a88beaf64f28996552b8754655cc79b48e57cfb Mon Sep 17 00:00:00 2001 From: Alicia Bot <87835202+liss-bot@users.noreply.github.com> Date: Sun, 12 May 2024 02:27:43 +0100 Subject: [PATCH 4/5] :blue_heart: Updates contributor SVG --- docs/assets/CONTRIBUTORS.svg | 119 ++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/docs/assets/CONTRIBUTORS.svg b/docs/assets/CONTRIBUTORS.svg index c2218aec..655d5ca9 100644 --- a/docs/assets/CONTRIBUTORS.svg +++ b/docs/assets/CONTRIBUTORS.svg @@ -1,4 +1,4 @@ - + @@ -165,59 +165,59 @@ - - - - + - + - + - + - + - + - + - + + + + + + + - + - + - + - + - + - + - + - - - - + @@ -273,109 +273,112 @@ + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file From c3b199361ce2e528e452478a14712ed32a420c46 Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 13 May 2024 22:24:10 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A7=BE=20[docs](add)=20keycloak=20tro?= =?UTF-8?q?ubleshooting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/authentication.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/authentication.md b/docs/authentication.md index 016e905f..d7189b37 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -14,6 +14,7 @@ - [Deploying Keycloak](#1-deploy-keycloak) - [Setting up Keycloak](#2-setup-keycloak-users) - [Configuring Dashy for Keycloak](#3-enable-keycloak-in-dashy-config-file) + - [Toubleshooting Keycloak](#troubleshooting-keycloak) - [Alternative Authentication Methods](#alternative-authentication-methods) - [VPN](#vpn) - [IP-Based Access](#ip-based-access) @@ -251,6 +252,26 @@ Your app is now secured :) When you load Dashy, it will redirect to your Keycloa From within the Keycloak console, you can then configure things like time-outs, password policies, etc. You can also backup your full Keycloak config, and it is recommended to do this, along with your Dashy config. You can spin up both Dashy and Keycloak simultaneously and restore both applications configs using a `docker-compose.yml` file, and this is recommended. +--- + +### Troubleshooting Keycloak + +If you encounter issues with your Keycloak setup, follow these steps to troubleshoot and resolve common problems. + +1. Client Authentication Issue +Problem: Redirect loop, if client authentication is enabled. +Solution: Switch off "client authentication" in "TC clients" -> "Advanced" settings. + +2. Double URL +Problem: If you get redirected to "https://dashy.my.domain/#iss=https://keycloak.my.domain/realms/my-realm" +Solution: Make sure to turn on "Exclude Issuer From Authentication Response" in "TC clients" -> "Advanced" -> "OpenID Connect Compatibility Modes" + +3. Problems with mutiple Dashy Pages +Problem: Refreshing or logging out of dashy results in an "invalid_redirect_uri" error. +Solution: In "TC clients" -> "Access settings" -> "Root URL" https://dashy.my.domain/, valid redirect URIs must be /* + +--- + ## OIDC Dashy also supports using a general [OIDC compatible](https://openid.net/connect/) authentication server. In order to use it, the authentication section needs to be configured: