mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	oidc: try to get username from userinfo (#2545)
* oidc: try to get username from userinfo Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * changelog Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
		
							parent
							
								
									8f9fbf16f1
								
							
						
					
					
						commit
						cfe9bbf829
					
				@ -97,6 +97,8 @@ working in v1 and not tested might be broken in v2 (and vice versa).
 | 
			
		||||
  [#2493](https://github.com/juanfont/headscale/pull/2493)
 | 
			
		||||
  - If a OIDC provider doesn't include the `email_verified` claim in its ID
 | 
			
		||||
    tokens, Headscale will attempt to get it from the UserInfo endpoint.
 | 
			
		||||
- OIDC: Try to populate name, email and username from UserInfo
 | 
			
		||||
  [#2545](https://github.com/juanfont/headscale/pull/2545)
 | 
			
		||||
- Improve performance by only querying relevant nodes from the database for node
 | 
			
		||||
  updates [#2509](https://github.com/juanfont/headscale/pull/2509)
 | 
			
		||||
- node FQDNs in the netmap will now contain a dot (".") at the end. This aligns
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package hscontrol
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"context"
 | 
			
		||||
	_ "embed"
 | 
			
		||||
	"errors"
 | 
			
		||||
@ -280,14 +281,28 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If EmailVerified is missing, we can try to get it from UserInfo
 | 
			
		||||
	if !claims.EmailVerified {
 | 
			
		||||
		var userinfo *oidc.UserInfo
 | 
			
		||||
		userinfo, err = a.oidcProvider.UserInfo(req.Context(), oauth2.StaticTokenSource(oauth2Token))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			util.LogErr(err, "could not get userinfo; email cannot be verified")
 | 
			
		||||
	var userinfo *oidc.UserInfo
 | 
			
		||||
	userinfo, err = a.oidcProvider.UserInfo(req.Context(), oauth2.StaticTokenSource(oauth2Token))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		util.LogErr(err, "could not get userinfo; only checking claim")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If the userinfo is available, we can check if the subject matches the
 | 
			
		||||
	// claims, then use some of the userinfo fields to update the user.
 | 
			
		||||
	// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
 | 
			
		||||
	if userinfo != nil && userinfo.Subject == claims.Sub {
 | 
			
		||||
		claims.Email = cmp.Or(claims.Email, userinfo.Email)
 | 
			
		||||
		claims.EmailVerified = cmp.Or(claims.EmailVerified, types.FlexibleBoolean(userinfo.EmailVerified))
 | 
			
		||||
 | 
			
		||||
		// The userinfo has some extra fields that we can use to update the user but they are only
 | 
			
		||||
		// available in the underlying claims struct.
 | 
			
		||||
		// TODO(kradalby): there might be more interesting fields here that we have not found yet.
 | 
			
		||||
		var userinfo2 types.OIDCUserInfo
 | 
			
		||||
		if err := userinfo.Claims(&userinfo2); err == nil {
 | 
			
		||||
			claims.Username = cmp.Or(claims.Username, userinfo2.PreferredUsername)
 | 
			
		||||
			claims.Name = cmp.Or(claims.Name, userinfo2.Name)
 | 
			
		||||
			claims.ProfilePictureURL = cmp.Or(claims.ProfilePictureURL, userinfo2.Picture)
 | 
			
		||||
		}
 | 
			
		||||
		claims.EmailVerified = types.FlexibleBoolean(userinfo.EmailVerified)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := a.createOrUpdateUserFromClaim(&claims)
 | 
			
		||||
 | 
			
		||||
@ -157,7 +157,7 @@ func (u *User) Proto() *v1.User {
 | 
			
		||||
type FlexibleBoolean bool
 | 
			
		||||
 | 
			
		||||
func (bit *FlexibleBoolean) UnmarshalJSON(data []byte) error {
 | 
			
		||||
	var val interface{}
 | 
			
		||||
	var val any
 | 
			
		||||
	err := json.Unmarshal(data, &val)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("could not unmarshal data: %w", err)
 | 
			
		||||
@ -203,6 +203,17 @@ func (c *OIDCClaims) Identifier() string {
 | 
			
		||||
	return c.Iss + "/" + c.Sub
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OIDCUserInfo struct {
 | 
			
		||||
	Sub               string          `json:"sub"`
 | 
			
		||||
	Name              string          `json:"name"`
 | 
			
		||||
	GivenName         string          `json:"given_name"`
 | 
			
		||||
	FamilyName        string          `json:"family_name"`
 | 
			
		||||
	PreferredUsername string          `json:"preferred_username"`
 | 
			
		||||
	Email             string          `json:"email"`
 | 
			
		||||
	EmailVerified     FlexibleBoolean `json:"email_verified,omitempty"`
 | 
			
		||||
	Picture           string          `json:"picture"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FromClaim overrides a User from OIDC claims.
 | 
			
		||||
// All fields will be updated, except for the ID.
 | 
			
		||||
func (u *User) FromClaim(claims *OIDCClaims) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user