Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC configure expiry time #1176

Closed
charlez-700 opened this issue Jan 27, 2023 · 13 comments · Fixed by #1191
Closed

OIDC configure expiry time #1176

charlez-700 opened this issue Jan 27, 2023 · 13 comments · Fixed by #1191
Labels
bug Something isn't working

Comments

@charlez-700
Copy link

Hi,
I run Headscale 0.17 and planning to upgrade to 0.18. on my test server running 0.18 I see that the oidc users have a default expiration time of 60 minutes.

how do I configure the expiration time for oidc users to make it longer?

thanks.

@charlez-700 charlez-700 added the bug Something isn't working label Jan 27, 2023
@evenh
Copy link
Contributor

evenh commented Jan 27, 2023

The implementation uses whatever your OIDC provider puts in the exp-field of the issued token.

It may indicate that your provider has a default token lifetime of 1hr.

@ported-pw
Copy link

ported-pw commented Jan 28, 2023

I just came across this while wondering why my node was getting such a short expiry time or expiring at all for that matter.
I take it that OIDC token refresh is not implemented yet? Are there any plans for doing that?
For example Keycloak uses very short lived tokens (5 minutes by default) and prefers to let users refresh often to ensure that they have up to date tokens/credentials. This way I'd have to make it somewhat less secure and configure the access token lifetime to something usable like an entire week or month which doesn't sound great.

@charlez-700
Copy link
Author

Hhhmmmm that may be a problem then because all my OIDC clients will disconnect once an hour.

I take it that OIDC token refresh is not implemented yet?

I think the same yes.

@ported-pw
Copy link

ported-pw commented Jan 28, 2023

Maybe it could be made possible to disable expiring nodes on token expiry for now until the OIDC implementation is complete? See #1067

@reynico
Copy link
Contributor

reynico commented Jan 28, 2023

Maybe it could be made possible to disable expiring nodes on token expiry for now until the OIDC implementation is complete? See #1067

maybe passing a default expiry time by configuration?

@evenh
Copy link
Contributor

evenh commented Jan 28, 2023

I just came across this while wondering why my node was getting such a short expiry time or expiring at all for that matter. I take it that OIDC token refresh is not implemented yet? Are there any plans for doing that? For example Keycloak uses very short lived tokens (5 minutes by default) and prefers to let users refresh often to ensure that they have up to date tokens/credentials. This way I'd have to make it somewhat less secure and configure the access token lifetime to something usable like an entire week or month which doesn't sound great.

Only the ID token from OIDC is used per right now. Related code and discussions:

Looking forward, there is a couple of obvious things that could be done:

  • Add a configuration option/environment variable that overrides whatever expiry comes from OIDC (or possibly when running without OIDC). HS_OVERRIDE_DEFAULT_EXPIRY_TIME where 0=never otherwise parse the given duration.
  • Determine what the behaviour is for the commercial Tailscale control plane and implement a similar flow. Are they using access/refresh tokens?

What would be the preferred approach to handling this @kradalby?

@jsiebens
Copy link
Contributor

As OIDC tokens tend to be short-lived (with google it's 1h, with others perhaps even shorter), I wonder why machines should follow the same expiry.

On the commercial Tailscale server, newly registered machines get a fixed expiration period (default 180 days but can be configured on your tailnet using the admin console) unless they have ACL tags, then key expiry is disabled.

@ported-pw
Copy link

ported-pw commented Jan 29, 2023

As OIDC tokens tend to be short-lived (with google it's 1h, with others perhaps even shorter), I wonder why machines should follow the same expiry.

Exactly. As I mentioned, for Keycloak the default is 5 minutes.

It would be the best if

  1. Machines were expired once the OIDC session cannot be refreshed anymore (forces re-login at SSO provider)
    and
  2. Machines were expired if on token refresh the token does not match the required allowed groups/domains/etc. anymore (this would allow expiring machines within [access token expiry time] of removing a user's permissions for example)

But if the original Tailscale behaviour is 180 days or a different configurable value on registration via OIDC, that would be fine too and I'd assume much easier to implement. But right now due to security concerns with creating such long-lived access tokens (as they could be valid on other services by configuration error for example) the feature is not really usable for me.

@Hacksawfred3232
Copy link

Hacksawfred3232 commented Jan 30, 2023

The implementation uses whatever your OIDC provider puts in the exp-field of the issued token.

It may indicate that your provider has a default token lifetime of 1hr.

Following @evenh comment, trying to increase the token expiry time in Keycloak - which is what I use for my OIDC provider - it seems to only max out at 10 hours when Headscale updates a client. Which seems really random, so either a bug with Keycloak/OIDC implementation or built-in limitation. Though while the token time is fair in terms of short-lived tokens, some users may need it at max a month without making the OIDC provider insecure. Implementing HS_OVERRIDE_DEFAULT_EXPIRY_TIME sounds like a good idea.

@ported-pw
Copy link

ported-pw commented Jan 30, 2023

Implementing HS_OVERRIDE_DEFAULT_EXPIRY_TIME sounds like a good idea.

I agree that it's an easy and acceptable stopgap solution.

@evenh
Copy link
Contributor

evenh commented Jan 30, 2023

I won't have time to tackle the integration tests anytime soon, but could something like this work?

From 28d8dd3d32003b826febcc2699c0706f71258b2e Mon Sep 17 00:00:00 2001
From: Even Holthe <[email protected]>
Date: Mon, 30 Jan 2023 16:05:56 +0100
Subject: [PATCH] Add option for overriding OIDC ID Token expiry

Closes #1176
---
 config.go | 13 +++++++++++++
 oidc.go   | 16 ++++++++++++++--
 2 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/config.go b/config.go
index 6865b30..f23e827 100644
--- a/config.go
+++ b/config.go
@@ -101,6 +101,7 @@ type OIDCConfig struct {
 	AllowedUsers               []string
 	AllowedGroups              []string
 	StripEmaildomain           bool
+	OverrideExpiry             *time.Duration
 }
 
 type DERPConfig struct {
@@ -180,6 +181,7 @@ func LoadConfig(path string, isFile bool) error {
 	viper.SetDefault("oidc.scope", []string{oidc.ScopeOpenID, "profile", "email"})
 	viper.SetDefault("oidc.strip_email_domain", true)
 	viper.SetDefault("oidc.only_start_if_oidc_is_available", true)
+	viper.SetDefault("oidc.override_expiry", "")
 
 	viper.SetDefault("logtail.enabled", false)
 	viper.SetDefault("randomize_client_port", false)
@@ -603,6 +605,17 @@ func GetHeadscaleConfig() (*Config, error) {
 			AllowedUsers:     viper.GetStringSlice("oidc.allowed_users"),
 			AllowedGroups:    viper.GetStringSlice("oidc.allowed_groups"),
 			StripEmaildomain: viper.GetBool("oidc.strip_email_domain"),
+			OverrideExpiry: func() *time.Duration {
+				// if empty, no override set
+				if e := viper.GetString("oidc.override_expiry"); len(e) == 0 {
+					return nil
+				}
+
+				// return parsed override expiry
+				e := viper.GetDuration("oidc.override_expiry")
+
+				return &e
+			}(),
 		},
 
 		LogTail:             logConfig,
diff --git a/oidc.go b/oidc.go
index 4909ba9..4961041 100644
--- a/oidc.go
+++ b/oidc.go
@@ -68,6 +68,17 @@ func (h *Headscale) initOIDC() error {
 	return nil
 }
 
+func (h *Headscale) determineTokenExpiration(originalExpiration time.Time) time.Time {
+	overriddenExpirationDuration := h.cfg.OIDC.OverrideExpiry
+	// don't modify expiry
+	if overriddenExpirationDuration == nil {
+		return originalExpiration
+	}
+
+	// calculate a new expiry based on override
+	return time.Now().Add(*overriddenExpirationDuration)
+}
+
 // RegisterOIDC redirects to the OIDC provider for authentication
 // Puts NodeKey in cache so the callback can retrieve it using the oidc state param
 // Listens in /oidc/register/:nKey.
@@ -193,6 +204,7 @@ func (h *Headscale) OIDCCallback(
 	if err != nil {
 		return
 	}
+	idTokenExpiry := h.determineTokenExpiration(idToken.Expiry)
 
 	// TODO: we can use userinfo at some point to grab additional information about the user (groups membership, etc)
 	// userInfo, err := oidcProvider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token))
@@ -218,7 +230,7 @@ func (h *Headscale) OIDCCallback(
 		return
 	}
 
-	nodeKey, machineExists, err := h.validateMachineForOIDCCallback(writer, state, claims, idToken.Expiry)
+	nodeKey, machineExists, err := h.validateMachineForOIDCCallback(writer, state, claims, idTokenExpiry)
 	if err != nil || machineExists {
 		return
 	}
@@ -236,7 +248,7 @@ func (h *Headscale) OIDCCallback(
 		return
 	}
 
-	if err := h.registerMachineForOIDCCallback(writer, user, nodeKey, idToken.Expiry); err != nil {
+	if err := h.registerMachineForOIDCCallback(writer, user, nodeKey, idTokenExpiry); err != nil {
 		return
 	}
 
-- 
2.37.5

@Hacksawfred3232
Copy link

Hacksawfred3232 commented Jan 30, 2023

I won't have time to tackle the integration tests anytime soon, but could something like this work?

@evenh That patch seems to work! I'll roll with the patched version until it becomes added officially.

kradalby added a commit to kradalby/headscale that referenced this issue Jan 31, 2023
This commit adds a default OpenID Connect expiry to 180d to align with
Tailscale SaaS (previously infinite or based on token expiry).

In addition, it adds an option use the expiry time from the Token sent
by the OpenID provider. This will typically cause really short expiry
and you should only turn on this option if you know what you are
desiring.

This fixes juanfont#1176.

Signed-off-by: Kristoffer Dalby <[email protected]>
kradalby added a commit to kradalby/headscale that referenced this issue Jan 31, 2023
This commit adds a default OpenID Connect expiry to 180d to align with
Tailscale SaaS (previously infinite or based on token expiry).

In addition, it adds an option use the expiry time from the Token sent
by the OpenID provider. This will typically cause really short expiry
and you should only turn on this option if you know what you are
desiring.

This fixes juanfont#1176.

Co-authored-by: Even Holthe <[email protected]>
Signed-off-by: Kristoffer Dalby <[email protected]>
@kradalby
Copy link
Collaborator

I have created #1191 based on @evenh's patch, it takes a slightly different approach:

Instead of adding an option to override the new behaviour (using the token expiry) it makes that an optional behaviour for people who desire it.

Instead of reverting back to setting no expiry for OpenID clients, it will now align with Tailscale SaaS and set 180 days by default, and of course exposing the option to the user so it can be set longer or short as desired.

Please help testing it or voice concerns on the PR.

kradalby added a commit to kradalby/headscale that referenced this issue Jan 31, 2023
This commit adds a default OpenID Connect expiry to 180d to align with
Tailscale SaaS (previously infinite or based on token expiry).

In addition, it adds an option use the expiry time from the Token sent
by the OpenID provider. This will typically cause really short expiry
and you should only turn on this option if you know what you are
desiring.

This fixes juanfont#1176.

Co-authored-by: Even Holthe <[email protected]>
Signed-off-by: Kristoffer Dalby <[email protected]>
kradalby added a commit to kradalby/headscale that referenced this issue Jan 31, 2023
This commit adds a default OpenID Connect expiry to 180d to align with
Tailscale SaaS (previously infinite or based on token expiry).

In addition, it adds an option use the expiry time from the Token sent
by the OpenID provider. This will typically cause really short expiry
and you should only turn on this option if you know what you are
desiring.

This fixes juanfont#1176.

Co-authored-by: Even Holthe <[email protected]>
Signed-off-by: Kristoffer Dalby <[email protected]>
kradalby added a commit that referenced this issue Jan 31, 2023
This commit adds a default OpenID Connect expiry to 180d to align with
Tailscale SaaS (previously infinite or based on token expiry).

In addition, it adds an option use the expiry time from the Token sent
by the OpenID provider. This will typically cause really short expiry
and you should only turn on this option if you know what you are
desiring.

This fixes #1176.

Co-authored-by: Even Holthe <[email protected]>
Signed-off-by: Kristoffer Dalby <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants