/
authenticator.go
150 lines (119 loc) 路 3.09 KB
/
authenticator.go
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
"net/http"
"net/url"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type Option func(*authenticator) error
type Options struct {
// OAuth2 client ID of this application.
ClientID string
// OAuth2 client secret of this application.
ClientSecret string
// URL of the OpenID Connect issuer.
IssuerURL string
// Callback URL for OAuth2 responses.
RedirectURI string
}
func WithOptions(opts Options) Option {
return func(a *authenticator) error {
if opts.ClientID != "" {
a.clientID = opts.ClientID
}
if opts.ClientSecret != "" {
a.clientSecret = opts.ClientSecret
}
if opts.IssuerURL != "" {
u, err := url.Parse(opts.IssuerURL)
if err != nil {
return fmt.Errorf("issue url: %w", err)
}
a.issuerURL = *u
}
if opts.RedirectURI != "" {
u, err := url.Parse(opts.RedirectURI)
if err != nil {
return fmt.Errorf("redirect uri: %w", err)
}
a.redirectURI = *u
}
return nil
}
}
type authenticator struct {
clientID string
clientSecret string
redirectURI url.URL
verifier *oidc.IDTokenVerifier
provider *oidc.Provider
issuerURL url.URL
// Does the provider use "offline_access" scope to request a refresh token
// or does it use "access_type=offline" (e.g. Google)?
offlineAsScope bool
client *http.Client
SpaRedirectURI url.URL
}
func New(options ...Option) (*authenticator, error) {
a := &authenticator{
client: http.DefaultClient,
}
for _, opt := range options {
if err := opt(a); err != nil {
return nil, err
}
}
ctx := oidc.ClientContext(context.Background(), a.client)
provider, err := oidc.NewProvider(ctx, a.issuerURL.String())
if err != nil {
return nil, fmt.Errorf("creating oidc provider: %w", err)
}
var s struct {
// What scopes does a provider support?
//
// See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
ScopesSupported []string `json:"scopes_supported"`
}
if err := provider.Claims(&s); err != nil {
return nil, fmt.Errorf("failed to parse provider scopes_supported: %v", err)
}
if len(s.ScopesSupported) == 0 {
// scopes_supported is a "RECOMMENDED" discovery claim, not a required
// one. If missing, assume that the provider follows the spec and has
// an "offline_access" scope.
a.offlineAsScope = true
} else {
// See if scopes_supported has the "offline_access" scope.
a.offlineAsScope = func() bool {
for _, scope := range s.ScopesSupported {
if scope == oidc.ScopeOfflineAccess {
return true
}
}
return false
}()
}
a.provider = provider
a.verifier = provider.Verifier(&oidc.Config{ClientID: a.clientID})
return a, nil
}
func (a *authenticator) oauth2Config(scopes []string) *oauth2.Config {
return &oauth2.Config{
ClientID: a.clientID,
ClientSecret: a.clientSecret,
Endpoint: a.provider.Endpoint(),
Scopes: scopes,
RedirectURL: a.redirectURI.String(),
}
}
func (a *authenticator) newState() string {
buf := make([]byte, 1024)
rand.Read(buf)
h := sha256.New()
h.Write(buf)
return fmt.Sprintf("%x", h.Sum(nil))
}