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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ The new policy can be used by setting the environment variable
- It is now possible to inspect running goroutines and take profiles
- View of config, policy, filter, ssh policy per node, connected nodes and
DERPmap
- OIDC: Fetch UserInfo to get EmailVerified if necessary
[#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.

## 0.25.1 (2025-02-25)

Expand Down
33 changes: 28 additions & 5 deletions hscontrol/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,14 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
return
}

idToken, err := a.extractIDToken(req.Context(), code, state)
oauth2Token, err := a.getOauth2Token(req.Context(), code, state)

if err != nil {
httpError(writer, err)
return
}

idToken, err := a.extractIDToken(req.Context(), oauth2Token)
if err != nil {
httpError(writer, err)
return
Expand Down Expand Up @@ -273,6 +280,16 @@ 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")
}
claims.EmailVerified = types.FlexibleBoolean(userinfo.EmailVerified)
}

user, err := a.createOrUpdateUserFromClaim(&claims)
if err != nil {
httpError(writer, err)
Expand Down Expand Up @@ -333,13 +350,12 @@ func extractCodeAndStateParamFromRequest(
return code, state, nil
}

// extractIDToken takes the code parameter from the callback
// and extracts the ID token from the oauth2 token.
func (a *AuthProviderOIDC) extractIDToken(
// getOauth2Token exchanges the code from the callback for an oauth2 token.
func (a *AuthProviderOIDC) getOauth2Token(
ctx context.Context,
code string,
state string,
) (*oidc.IDToken, error) {
) (*oauth2.Token, error) {
var exchangeOpts []oauth2.AuthCodeOption

if a.cfg.PKCE.Enabled {
Expand All @@ -356,7 +372,14 @@ func (a *AuthProviderOIDC) extractIDToken(
if err != nil {
return nil, NewHTTPError(http.StatusForbidden, "invalid code", fmt.Errorf("could not exchange code for token: %w", err))
}
return oauth2Token, err
}

// extractIDToken extracts the ID token from the oauth2 token.
func (a *AuthProviderOIDC) extractIDToken(
ctx context.Context,
oauth2Token *oauth2.Token,
) (*oidc.IDToken, error) {
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return nil, NewHTTPError(http.StatusBadRequest, "no id_token", errNoOIDCIDToken)
Expand Down
Loading