-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuserinfo.go
More file actions
99 lines (85 loc) · 2.67 KB
/
userinfo.go
File metadata and controls
99 lines (85 loc) · 2.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
retry "github.com/appleboy/go-httpretry"
)
// UserInfo holds the claims returned by GET /oauth/userinfo.
//
// The server always emits "sub" and "iss".
// Profile claims (name, preferred_username, picture, updated_at) are included
// when the token carries the "profile" scope.
// Email claims (email, email_verified) are included with the "email" scope.
type UserInfo struct {
Sub string `json:"sub"`
Issuer string `json:"iss"`
// profile scope
Name string `json:"name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Picture string `json:"picture,omitempty"`
// UpdatedAt is seconds since Unix epoch per OIDC Core §5.1.
UpdatedAt int64 `json:"updated_at,omitempty"`
// email scope
Email string `json:"email,omitempty"`
EmailVerified *bool `json:"email_verified,omitempty"`
}
// fetchUserInfo calls GET /oauth/userinfo with a Bearer token and returns the
// parsed claims per OIDC Core §5.3.
func fetchUserInfo(ctx context.Context, cfg *AppConfig, accessToken string) (*UserInfo, error) {
ctx, cancel := context.WithTimeout(ctx, cfg.UserInfoTimeout)
defer cancel()
resp, err := cfg.RetryClient.Get(ctx, cfg.Endpoints.UserinfoURL,
retry.WithHeader("Authorization", "Bearer "+accessToken),
retry.WithHeader("Accept", "application/json"),
)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := readResponseBody(resp, cfg.MaxResponseBodySize)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, formatHTTPError(body, resp.StatusCode)
}
var info UserInfo
if err := json.Unmarshal(body, &info); err != nil {
return nil, fmt.Errorf("failed to parse UserInfo response: %w", err)
}
if info.Sub == "" {
return nil, errors.New("UserInfo response missing required 'sub' claim")
}
return &info, nil
}
// formatUserInfo returns a human-readable summary of UserInfo claims.
func formatUserInfo(info *UserInfo) string {
var sb strings.Builder
write := func(label, value string) {
if value != "" {
fmt.Fprintf(&sb, " %-22s %s\n", label+":", value)
}
}
write("Subject (sub)", info.Sub)
write("Issuer (iss)", info.Issuer)
write("Name", info.Name)
write("Preferred username", info.PreferredUsername)
write("Picture", info.Picture)
if info.UpdatedAt != 0 {
write("Updated at", time.Unix(info.UpdatedAt, 0).UTC().Format(time.RFC3339))
}
write("Email", info.Email)
if info.EmailVerified != nil {
v := "false"
if *info.EmailVerified {
v = "true"
}
write("Email verified", v)
}
return strings.TrimRight(sb.String(), "\n")
}