UserInfo returns the parsed user information using the given OAuth2 token.
(token string)
| 88 | |
| 89 | // UserInfo returns the parsed user information using the given OAuth2 token. |
| 90 | func (p *IdentityProvider) UserInfo(token string) (*storepb.IdentityProviderUserInfo, map[string]any, error) { |
| 91 | req, err := http.NewRequest(http.MethodGet, p.config.UserInfoUrl, nil) |
| 92 | if err != nil { |
| 93 | return nil, nil, errors.Wrap(err, "failed to new http request") |
| 94 | } |
| 95 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) |
| 96 | resp, err := p.client.Do(req) |
| 97 | if err != nil { |
| 98 | slog.Error("Failed to get user information", slog.String("token", token), log.BBError(err)) |
| 99 | return nil, nil, errors.Wrap(err, "failed to get user information") |
| 100 | } |
| 101 | defer resp.Body.Close() |
| 102 | body, err := io.ReadAll(resp.Body) |
| 103 | if err != nil { |
| 104 | slog.Error("Failed to read response body", slog.String("token", token), log.BBError(err)) |
| 105 | return nil, nil, errors.Wrap(err, "failed to read response body") |
| 106 | } |
| 107 | |
| 108 | if resp.StatusCode != http.StatusOK { |
| 109 | slog.Error("Unexpected status code from user info endpoint", slog.Int("status", resp.StatusCode), slog.String("body", string(body))) |
| 110 | return nil, nil, errors.Errorf("user info request failed with status %d", resp.StatusCode) |
| 111 | } |
| 112 | |
| 113 | var claims map[string]any |
| 114 | if err := json.Unmarshal(body, &claims); err != nil { |
| 115 | slog.Error("Failed to unmarshal response body", slog.String("token", token), slog.String("body", string(body)), log.BBError(err)) |
| 116 | return nil, nil, errors.Wrap(err, "failed to unmarshal response body") |
| 117 | } |
| 118 | slog.Debug("User info", slog.Any("claims", claims)) |
| 119 | |
| 120 | userInfo := &storepb.IdentityProviderUserInfo{} |
| 121 | if v, ok := idp.GetValueWithKey(claims, p.config.FieldMapping.Identifier).(string); ok { |
| 122 | userInfo.Identifier = v |
| 123 | } |
| 124 | |
| 125 | // GitHub-specific: when the identifier field (typically "email") is missing because the |
| 126 | // user's email is private, fetch it from the /user/emails endpoint. |
| 127 | if userInfo.Identifier == "" && p.config.FieldMapping.Identifier == "email" && isGitHubUserInfoURL(p.config.UserInfoUrl) { |
| 128 | if email, err := p.fetchGitHubPrimaryEmail(token); err == nil && email != "" { |
| 129 | userInfo.Identifier = email |
| 130 | claims["email"] = email |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | if p.config.FieldMapping.DisplayName != "" { |
| 135 | if v, ok := idp.GetValueWithKey(claims, p.config.FieldMapping.DisplayName).(string); ok { |
| 136 | userInfo.DisplayName = v |
| 137 | } |
| 138 | } |
| 139 | if userInfo.DisplayName == "" { |
| 140 | userInfo.DisplayName = userInfo.Identifier |
| 141 | } |
| 142 | if p.config.FieldMapping.Phone != "" { |
| 143 | if v, ok := idp.GetValueWithKey(claims, p.config.FieldMapping.Phone).(string); ok { |
| 144 | // Only set phone if it's valid. |
| 145 | if err := common.ValidatePhone(v); err == nil { |
| 146 | userInfo.Phone = v |
| 147 | } |