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 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 diff --git a/docs/authentication.md b/docs/authentication.md index 4430d9b0..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) @@ -253,6 +254,67 @@ From within the Keycloak console, you can then configure things like time-outs, --- +### 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: + +```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 If you are self-hosting Dashy, and require secure authentication to prevent unauthorized access, then you can either use Keycloak, or one of the following options: 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/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 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: {