Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const (
type clientConfig struct {
Clusters []Cluster `json:"clusters"`
IsDynamicClusterEnabled bool `json:"isDynamicClusterEnabled"`
OidcAutoLogin bool `json:"oidcAutoLogin"`
}

type OauthConfig struct {
Expand Down Expand Up @@ -1749,7 +1750,11 @@ func parseClusterFromKubeConfig(kubeConfigs []string) ([]Cluster, []error) {
func (c *HeadlampConfig) getConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

clientConfig := clientConfig{c.getClusters(), c.EnableDynamicClusters}
clientConfig := clientConfig{
Clusters: c.getClusters(),
IsDynamicClusterEnabled: c.EnableDynamicClusters,
OidcAutoLogin: c.OidcAutoLogin,
}

if err := json.NewEncoder(w).Encode(&clientConfig); err != nil {
logger.Log(logger.LevelError, nil, err, "encoding config")
Expand Down
1 change: 1 addition & 0 deletions backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig {

cfg := &headlampconfig.HeadlampConfig{
HeadlampCFG: buildHeadlampCFG(conf, kubeConfigStore),
OidcAutoLogin: conf.OidcAutoLogin,
OidcClientID: conf.OidcClientID,
OidcValidatorClientID: conf.OidcValidatorClientID,
OidcClientSecret: conf.OidcClientSecret,
Expand Down
2 changes: 1 addition & 1 deletion backend/cmd/stateless.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (c *HeadlampConfig) parseKubeConfig(w http.ResponseWriter, r *http.Request)
return
}

clientConfig := clientConfig{contexts, c.EnableDynamicClusters}
clientConfig := clientConfig{contexts, c.EnableDynamicClusters, c.OidcAutoLogin}

if err := json.NewEncoder(w).Encode(&clientConfig); err != nil {
logger.Log(logger.LevelError, nil, err, "encoding config")
Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type Config struct {
UserPluginsDir string `koanf:"user-plugins-dir"`
BaseURL string `koanf:"base-url"`
ProxyURLs string `koanf:"proxy-urls"`
OidcAutoLogin bool `koanf:"oidc-auto-login"`
OidcClientID string `koanf:"oidc-client-id"`
OidcValidatorClientID string `koanf:"oidc-validator-client-id"`
OidcClientSecret string `koanf:"oidc-client-secret"`
Expand Down Expand Up @@ -437,6 +438,7 @@ func addGeneralFlags(f *flag.FlagSet) {
}

func addOIDCFlags(f *flag.FlagSet) {
f.Bool("oidc-auto-login", false, "Automatic Redirect to OIDC provider")
f.String("oidc-client-id", "", "ClientID for OIDC")
f.String("oidc-client-secret", "", "ClientSecret for OIDC")
f.String("oidc-validator-client-id", "", "Override ClientID for OIDC during validation")
Expand Down
1 change: 1 addition & 0 deletions backend/pkg/headlampconfig/headlampConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type WebSocketMultiplexer interface {
// HeadlampConfig holds full server config. Lives here so packages (e.g. k8cache) can import without cmd.
type HeadlampConfig struct {
*HeadlampCFG
OidcAutoLogin bool
OidcClientID string
OidcValidatorClientID string
OidcClientSecret string
Expand Down
2 changes: 2 additions & 0 deletions charts/headlamp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ $ helm install my-headlamp headlamp/headlamp \
| config.oidc.issuerURL | string | `""` | OIDC issuer URL |
| config.oidc.scopes | string | `""` | OIDC scopes to be used |
| config.oidc.usePKCE | bool | `false` | Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow |
| confgi.oidc.autoLogin | bool | `false` | Enable Automatic redirect to OIDC provider |
| config.oidc.secret.create | bool | `true` | Create OIDC secret using provided values |
| config.oidc.secret.name | string | `"oidc"` | Name of the OIDC secret |
| config.oidc.externalSecret.enabled | bool | `false` | Enable using external secret for OIDC |
Expand All @@ -102,6 +103,7 @@ config:
clientSecret: "your-client-secret"
issuerURL: "https://your-issuer"
scopes: "openid profile email"
autoLogin: true
meUserInfoURL: "https://headlamp.example.com/oauth2/userinfo"
```

Expand Down
25 changes: 23 additions & 2 deletions charts/headlamp/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
{{- $usePKCE := "" }}
{{- $useAccessToken := "" }}
{{- $meUserInfoURL := "" }}
{{- $oidcAutoLogin := "" }}

# This block of code is used to extract the values from the env.
# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml.
Expand Down Expand Up @@ -45,6 +46,9 @@
{{- if eq .name "ME_USER_INFO_URL" }}
{{- $meUserInfoURL = .value | toString }}
{{- end }}
{{- if eq .name "OIDC_AUTO_LOGIN" }}
{{- $oidcAutoLogin = .value | toString }}
{{- end }}
{{- end }}

apiVersion: apps/v1
Expand Down Expand Up @@ -182,6 +186,13 @@ spec:
name: {{ $oidc.secret.name }}
key: meUserInfoURL
{{- end }}
{{- if $oidc.autoLogin }}
- name: OIDC_AUTO_LOGIN
valueFrom:
secretKeyRef:
name: {{ $oidc.auto.login }}
key: autoLogin
{{- end }}
{{- else }}
{{- if $oidc.clientID }}
- name: OIDC_CLIENT_ID
Expand Down Expand Up @@ -223,10 +234,14 @@ spec:
- name: ME_USER_INFO_URL
value: {{ $oidc.meUserInfoURL }}
{{- end }}
{{- if $oidc.autoLogin }}
- name: OIDC_AUTO_LOGIN
value: {{ $oidc.autoLogin | quote }}
{{- end }}
{{- if .Values.env }}
{{- end }}
{{- if .Values.env }}
{{- toYaml .Values.env | nindent 12 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
args:
Expand Down Expand Up @@ -285,6 +300,9 @@ spec:
{{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }}
- "-me-user-info-url=$(ME_USER_INFO_URL)"
{{- end }}
{{- if or (eq ($oidc.autoLogin | toString) "true") (eq $oidcAutoLogin "true") }}
- "-oidc-auto-login=$(OIDC_AUTO_LOGIN)"
{{- end }}
{{- else }}
- "-oidc-client-id=$(OIDC_CLIENT_ID)"
- "-oidc-client-secret=$(OIDC_CLIENT_SECRET)"
Expand Down Expand Up @@ -312,6 +330,9 @@ spec:
{{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }}
- "-me-user-info-url=$(ME_USER_INFO_URL)"
{{- end }}
{{- if or (eq ($oidc.autoLogin | toString) "true") (eq $oidcAutoLogin "true") }}
- "-oidc-auto-login=$(OIDC_AUTO_LOGIN)"
{{- end }}
{{- end }}
{{- with .Values.config.baseURL }}
- "-base-url={{ . }}"
Expand Down
4 changes: 4 additions & 0 deletions charts/headlamp/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@
"type": "boolean",
"description": "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow"
},
"autoLogin": {
"type": "boolean",
"description": "Enable automatic redirect to the OIDC provider"
},
"externalSecret": {
"type": "object",
"description": "External secret to use for OIDC configuration",
Expand Down
2 changes: 2 additions & 0 deletions charts/headlamp/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ config:
useAccessToken: false
# -- Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow
usePKCE: false
# -- Enable automatic redirect to the OIDC provider
autoLogin: false

# Option 3:
# @param config.oidc - External OIDC secret configuration
Expand Down
10 changes: 10 additions & 0 deletions docs/installation/in-cluster/oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ then add them all to the option:
used by Dex and other services, but since it's not part of the default spec,
it was removed in the mentioned version.


### Auto-login

By default, Headlamp shows a "Sign in" button for OIDC clusters. To bypass this screen and redirect users to your Identity Provider (IDP) you can use the auto-login flag.

- `-oidc-auto-login=true` OR env var `HEADLAMP_CONFIG_OIDC_AUTO_LOGIN`

> **ℹ️ Note:** This will only cause a redirect if the user is not currently authenticated and the selected cluster is configured in OIDC.

### Token Validation Overrides

In the event your OIDC Provider issues `access_tokens` from a different Issuer URL or clientID audience than its `id_tokens` (i.e. Azure Entra ID) you may have need of the following parameters to configure what is used in validation of tokens.
Expand Down Expand Up @@ -100,6 +109,7 @@ For quick reference if you are already familiar with setting up Entra ID,
- Set `--oidc-validator-idp-issuer-url` to `https://sts.windows.net/<Your Directory (tenant) ID>/`
- Set `-oidc-validator-client-id` to `6dae42f8-4368-4678-94ff-3960e28e3630`
- Set `-oidc-use-access-token=true`
- Set `-oidc-auto-login=true` (optional to skip the "Sign in" screen)


### Example: OIDC with Dex
Expand Down
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<!--
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Expand Down
44 changes: 43 additions & 1 deletion frontend/src/components/App/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router';
import { getAppUrl } from '../../helpers/getAppUrl';
import { getCluster } from '../../lib/cluster';
import { getSelectedClusters } from '../../lib/cluster';
import { useCluster, useClustersConf } from '../../lib/k8s';
Expand Down Expand Up @@ -146,7 +148,11 @@ const fetchConfig = (dispatch: Dispatch<UnknownAction>) => {
clustersToConfig[cluster.name] = cluster;
});

const configToStore = { ...config, clusters: clustersToConfig };
const configToStore = {
...config,
clusters: clustersToConfig,
oidcAutoLogin: config.oidcAutoLogin,
};

if (clusters === null) {
dispatch(setConfig(configToStore));
Expand Down Expand Up @@ -191,6 +197,7 @@ export default function Layout({}: LayoutProps) {
const isFullWidth = useTypedSelector(state => state.ui.isFullWidth);
const { t } = useTranslation();
const allClusters = useClustersConf();
const location = useLocation();

/** This fetches the cluster config from the backend and updates the redux store on an interval.
* When stateless clusters are enabled, it also fetches the stateless cluster config from the
Expand Down Expand Up @@ -235,6 +242,41 @@ export default function Layout({}: LayoutProps) {

const panels = useUIPanelsGroupedBySide();

const oidcAutoLogin = useTypedSelector(state => state.config.oidcAutoLogin);

useEffect(() => {
if (!oidcAutoLogin || !clusters) {
return;
}
const urlParams = new URLSearchParams(location.search);
const isLoggingOut = urlParams.get('logout') === 'true';
if (isLoggingOut || !!error) {
return;
}
const isCallbackPath =
location.pathname.includes('oidc-callback') ||
urlParams.has('code') ||
urlParams.has('state');
if (isCallbackPath) {
return;
}
const currentClusterName = getCluster();
if (!currentClusterName) {
return;
}
const currentCluster = clusters[currentClusterName];
const isOIDC = currentCluster?.auth_type === 'oidc';
if (!isOIDC) {
return;
}
if (currentCluster.useToken === undefined) {
const oauthUrl = `${getAppUrl()}oidc?dt=${Date.now()}&cluster=${encodeURIComponent(
currentClusterName
)}`;
window.location.href = oauthUrl;
}
}, [oidcAutoLogin, clusters, error, location.pathname, location.search]);

if (!disableBackendLoader) {
if (error && !config) {
return <ErrorPage message={<Trans>Failed to connect to the backend</Trans>} error={error} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const makeStore = () => {
'cluster-a': { name: 'cluster-a' },
'cluster-b': { name: 'cluster-b' },
} as any,
oidcAutoLogin: null,
settings: {
tableRowsPerPageOptions: [15, 25, 50],
timezone: 'UTC',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const makeStore = () => {
'cluster-a': { name: 'cluster-a' },
'cluster-b': { name: 'cluster-b' },
} as any,
oidcAutoLogin: null,
settings: {
tableRowsPerPageOptions: [15, 25, 50],
timezone: 'UTC',
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/redux/configSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export interface ConfigState {
useEvict: boolean;
[key: string]: any;
};
/**
* Whether OIDC auto-login is enabled. Null indicates the value hasn't been loaded from the backend yet.
*/
oidcAutoLogin: boolean | null;
}

export const defaultTableRowsPerPageOptions = [15, 25, 50];
Expand All @@ -71,6 +75,7 @@ export const initialState: ConfigState = {
clusters: null,
statelessClusters: null,
allClusters: null,
oidcAutoLogin: null,
settings: {
tableRowsPerPageOptions:
storedSettings.tableRowsPerPageOptions || defaultTableRowsPerPageOptions,
Expand All @@ -89,8 +94,15 @@ const configSlice = createSlice({
* @param state - The current state.
* @param action - The payload action containing the config.
*/
setConfig(state, action: PayloadAction<{ clusters: ConfigState['clusters'] }>) {
setConfig(
state,
action: PayloadAction<{ clusters: ConfigState['clusters']; oidcAutoLogin?: boolean }>
) {
state.clusters = action.payload.clusters;

if (action.payload.oidcAutoLogin !== undefined) {
state.oidcAutoLogin = action.payload.oidcAutoLogin;
}
},
/**
* Save the config. To both the store, and localStorage.
Expand Down
Loading