Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ The new policy can be used by setting the environment variable
[#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
Expand Down
29 changes: 22 additions & 7 deletions hscontrol/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package hscontrol

import (
"bytes"
"cmp"
"context"
_ "embed"
"errors"
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion hscontrol/types/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
Loading