forked from hashicorp/cap
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
323 lines (287 loc) · 10 KB
/
config.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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package oidc
import (
"bytes"
"crypto/x509"
"encoding/binary"
"encoding/json"
"encoding/pem"
"fmt"
"hash"
"hash/fnv"
"net/url"
"reflect"
"runtime"
"sort"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/hashicorp/cap/oidc/internal/strutils"
)
// ClientSecret is an oauth client Secret.
type ClientSecret string
// RedactedClientSecret is the redacted string or json for an oauth client secret.
const RedactedClientSecret = "[REDACTED: client secret]"
// String will redact the client secret.
func (t ClientSecret) String() string {
return RedactedClientSecret
}
// MarshalJSON will redact the client secret.
func (t ClientSecret) MarshalJSON() ([]byte, error) {
return json.Marshal(RedactedClientSecret)
}
// Config represents the configuration for an OIDC provider used by a relying
// party.
type Config struct {
// ClientID is the relying party ID.
ClientID string
// ClientSecret is the relying party secret. This may be empty if you only
// intend to use the provider with the authorization Code with PKCE or the
// implicit flows.
ClientSecret ClientSecret
// Scopes is a list of default oidc scopes to request of the provider. The
// required "oidc" scope is requested by default, and does not need to be
// part of this optional list. If a Request has scopes, they will override
// this configured list for a specific authentication attempt.
Scopes []string
// Issuer is a case-sensitive URL string using the https scheme that
// contains scheme, host, and optionally, port number and path components
// and no query or fragment components.
// See the Issuer Identifier spec: https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier
// See the OIDC connect discovery spec: https://openid.net/specs/openid-connect-discovery-1_0.html#IdentifierNormalization
// See the id_token spec: https://tools.ietf.org/html/rfc7519#section-4.1.1
Issuer string
// SupportedSigningAlgs is a list of supported signing algorithms. List of
// currently supported algs: RS256, RS384, RS512, ES256, ES384, ES512,
// PS256, PS384, PS512
//
// The list can be used to limit the supported algorithms when verifying
// id_token signatures, an id_token's at_hash claim against an
// access_token, etc.
SupportedSigningAlgs []Alg
// AllowedRedirectURLs is a list of allowed URLs for the provider to
// redirect to after a user authenticates. If AllowedRedirects is empty,
// the package will not check the Request.RedirectURL() to see if it's
// allowed, and the check will be left to the OIDC provider's /authorize
// endpoint.
AllowedRedirectURLs []string
// Audiences is an optional default list of case-sensitive strings to use when
// verifying an id_token's "aud" claim (which is also a list) If provided,
// the audiences of an id_token must match one of the configured audiences.
// If a Request has audiences, they will override this configured list for a
// specific authentication attempt.
Audiences []string
// ProviderCA is an optional CA certs (PEM encoded) to use when sending
// requests to the provider. If you have a list of *x509.Certificates, then
// see EncodeCertificates(...) to PEM encode them.
ProviderCA string
// NowFunc is a time func that returns the current time.
NowFunc func() time.Time
}
// NewConfig composes a new config for a provider.
//
// The "oidc" scope will always be added to the new configuration's Scopes,
// regardless of what additional scopes are requested via the WithScopes option
// and duplicate scopes are allowed.
//
// Supported options: WithProviderCA, WithScopes, WithAudiences, WithNow
func NewConfig(issuer string, clientID string, clientSecret ClientSecret, supported []Alg, allowedRedirectURLs []string, opt ...Option) (*Config, error) {
const op = "NewConfig"
opts := getConfigOpts(opt...)
c := &Config{
Issuer: issuer,
ClientID: clientID,
ClientSecret: clientSecret,
SupportedSigningAlgs: supported,
Scopes: opts.withScopes,
ProviderCA: opts.withProviderCA,
Audiences: opts.withAudiences,
NowFunc: opts.withNowFunc,
AllowedRedirectURLs: allowedRedirectURLs,
}
if err := c.Validate(); err != nil {
return nil, fmt.Errorf("%s: invalid provider config: %w", op, err)
}
return c, nil
}
// Hash will produce a hash value for the Config, which is suitable to use for
// comparing two configurations for equality.
func (c *Config) Hash() (uint64, error) {
var h uint64
var err error
algs := make([]string, 0, len(c.SupportedSigningAlgs))
for _, a := range c.SupportedSigningAlgs {
algs = append(algs, string(a))
}
scopes := make([]string, 0, len(c.Scopes))
scopes = append(scopes, c.Scopes...)
audiences := make([]string, 0, len(c.Audiences))
audiences = append(audiences, c.Audiences...)
redirects := make([]string, 0, len(c.AllowedRedirectURLs))
redirects = append(redirects, c.AllowedRedirectURLs...)
sort.Strings(algs)
sort.Strings(scopes)
sort.Strings(audiences)
sort.Strings(redirects)
args := make([]string, 0, len(algs)+len(scopes)+len(audiences)+len(redirects)+5)
args = append(
args,
c.Issuer,
c.ClientID,
string(c.ClientSecret),
c.ProviderCA,
runtime.FuncForPC(reflect.ValueOf(c.NowFunc).Pointer()).Name(),
)
args = append(args, algs...)
args = append(args, scopes...)
args = append(args, audiences...)
args = append(args, redirects...)
if h, err = hashStrings(args...); err != nil {
return 0, fmt.Errorf("hashing error: %w", err)
}
return h, nil
}
func hashStrings(s ...string) (uint64, error) {
hasher := fnv.New64()
var h uint64
var err error
for _, current := range s {
hasher.Reset()
if _, err = hasher.Write([]byte(current)); err != nil {
return 0, err
}
if h, err = hashUpdateOrdered(hasher, h, hasher.Sum64()); err != nil {
return 0, err
}
}
return h, nil
}
// hashUpdateOrdered is taken directly from
// https://github.com/mitchellh/hashstructure
func hashUpdateOrdered(h hash.Hash64, a, b uint64) (uint64, error) {
// For ordered updates, use a real hash function
h.Reset()
e1 := binary.Write(h, binary.LittleEndian, a)
e2 := binary.Write(h, binary.LittleEndian, b)
if e1 != nil {
return 0, e1
}
if e2 != nil {
return 0, e2
}
return h.Sum64(), nil
}
// Validate the provider configuration. Among other validations, it verifies
// the issuer is not empty, but it doesn't verify the Issuer is discoverable via
// an http request. SupportedSigningAlgs are validated against the list of
// currently supported algs: RS256, RS384, RS512, ES256, ES384, ES512, PS256,
// PS384, PS512
func (c *Config) Validate() error {
const op = "Config.Validate"
// Note: c.ClientSecret is intentionally not checked for empty, in order to
// support providers that only use the implicit flow or PKCE.
if c == nil {
return fmt.Errorf("%s: provider config is nil: %w", op, ErrNilParameter)
}
if c.ClientID == "" {
return fmt.Errorf("%s: client ID is empty: %w", op, ErrInvalidParameter)
}
if c.Issuer == "" {
return fmt.Errorf("%s: discovery URL is empty: %w", op, ErrInvalidParameter)
}
if len(c.AllowedRedirectURLs) > 0 {
var invalidURLs []string
for _, allowed := range c.AllowedRedirectURLs {
if _, err := url.Parse(allowed); err != nil {
invalidURLs = append(invalidURLs, allowed)
}
}
if len(invalidURLs) > 0 {
return fmt.Errorf("%s: Invalid AllowedRedirectURLs provided %s: %w", op, strings.Join(invalidURLs, ", "), ErrInvalidParameter)
}
}
u, err := url.Parse(c.Issuer)
if err != nil {
return fmt.Errorf("%s: issuer %s is invalid (%s): %w", op, c.Issuer, err, ErrInvalidIssuer)
}
if !strutils.StrListContains([]string{"https", "http"}, u.Scheme) {
return fmt.Errorf("%s: issuer %s schema is not http or https: %w", op, c.Issuer, ErrInvalidIssuer)
}
if len(c.SupportedSigningAlgs) == 0 {
return fmt.Errorf("%s: supported algorithms is empty: %w", op, ErrInvalidParameter)
}
for _, a := range c.SupportedSigningAlgs {
if !supportedAlgorithms[a] {
return fmt.Errorf("%s: unsupported algorithm %s: %w", op, a, ErrInvalidParameter)
}
}
if c.ProviderCA != "" {
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM([]byte(c.ProviderCA)); !ok {
return fmt.Errorf("%s: %w", op, ErrInvalidCACert)
}
}
return nil
}
// Now will return the current time which can be overridden by the NowFunc
func (c *Config) Now() time.Time {
if c.NowFunc != nil {
return c.NowFunc()
}
return time.Now() // fallback to this default
}
// configOptions is the set of available options
type configOptions struct {
withScopes []string
withAudiences []string
withProviderCA string
withNowFunc func() time.Time
}
// configDefaults is a handy way to get the defaults at runtime and
// during unit tests.
func configDefaults() configOptions {
return configOptions{
withScopes: []string{oidc.ScopeOpenID},
}
}
// getConfigOpts gets the defaults and applies the opt overrides passed
// in.
func getConfigOpts(opt ...Option) configOptions {
opts := configDefaults()
ApplyOpts(&opts, opt...)
return opts
}
// WithProviderCA provides optional CA certs (PEM encoded) for the provider's
// config. These certs will can be used when making http requests to the
// provider.
//
// Valid for: Config
//
// See EncodeCertificates(...) to PEM encode a number of certs.
func WithProviderCA(cert string) Option {
return func(o interface{}) {
if o, ok := o.(*configOptions); ok {
o.withProviderCA = cert
}
}
}
// EncodeCertificates will encode a number of x509 certificates to PEM. It will
// help encode certs for use with the WithProviderCA(...) option.
func EncodeCertificates(certs ...*x509.Certificate) (string, error) {
const op = "EncodeCert"
var buffer bytes.Buffer
if len(certs) == 0 {
return "", fmt.Errorf("%s: no certs provided: %w", op, ErrInvalidParameter)
}
for _, cert := range certs {
if cert == nil {
return "", fmt.Errorf("%s: empty cert: %w", op, ErrNilParameter)
}
if err := pem.Encode(&buffer, &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}); err != nil {
return "", fmt.Errorf("%s: unable to encode cert: %w", op, err)
}
}
return buffer.String(), nil
}