-
Notifications
You must be signed in to change notification settings - Fork 0
/
plugin.go
311 lines (274 loc) · 12.2 KB
/
plugin.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
package traefik_plugin_oidc
import (
"context"
"fmt"
"net/http"
"regexp"
"text/template"
"time"
guuid "github.com/google/uuid"
"github.com/taliesins/traefik-plugin-oidc/jwks"
"github.com/taliesins/traefik-plugin-oidc/jwt_flow"
"github.com/taliesins/traefik-plugin-oidc/jwt_flow/validator"
"github.com/taliesins/traefik-plugin-oidc/log"
"github.com/taliesins/traefik-plugin-oidc/log/encoder"
"github.com/taliesins/traefik-plugin-oidc/sso_redirector"
)
type Config struct {
SsoRedirectUrlAddressTemplate string `json:"SsoRedirectUrlAddressTemplate,omitempty"`
SsoRedirectUrlMacClientSecret string `json:"ssoRedirectUrlMacClientSecret,omitempty"`
SsoRedirectUrlMacPrivateKey string `json:"ssoRedirectUrlMacPrivateKey,omitempty"`
SsoRedirectUrlMacStrength sso_redirector.HmacStrength `json:"ssoRedirectUrlMacStrength,omitempty"`
SsoRedirectUrlMacAllowedClockSkew time.Duration `json:"ssoRedirectUrlMacAllowedClockSkew,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
PublicKey string `json:"publicKey,omitempty"`
Issuer string `json:"issuer,omitempty"`
Audience string `json:"audience,omitempty"`
JwksAddress string `json:"jwksAddress,omitempty"`
OidcDiscoveryAddress string `json:"oidcDiscoveryAddress,omitempty"`
UseDynamicValidation bool `json:"useDynamicValidation,omitempty"`
AlgorithmValidationRegex string `json:"algorithmValidationRegex,omitempty"`
AudienceValidationRegex string `json:"audienceValidationRegex,omitempty"`
IssuerValidationRegex string `json:"issuerValidationRegex,omitempty"`
SubjectValidationRegex string `json:"subjectValidationRegex,omitempty"`
IdValidationRegex string `json:"idValidationRegex,omitempty"`
TokenAllowedClockSkew time.Duration `json:"tokenAllowedClockSkew,omitempty"`
IgnorePathRegex string `json:"ignorePathRegex,omitempty"`
CredentialsOptional bool `json:"credentialsOptional,omitempty"`
ValidateOnOptions bool `json:"validateOnOptions,omitempty"`
}
// CreateConfig creates the default plugin configuration.
func CreateConfig() *Config {
return &Config{
SsoRedirectUrlAddressTemplate: "",
SsoRedirectUrlMacStrength: sso_redirector.HmacStrength_256,
SsoRedirectUrlMacClientSecret: "",
SsoRedirectUrlMacPrivateKey: "",
SsoRedirectUrlMacAllowedClockSkew: time.Minute * 30,
ClientSecret: "",
PublicKey: "",
Issuer: "",
Audience: "",
JwksAddress: "",
OidcDiscoveryAddress: "",
UseDynamicValidation: false,
AlgorithmValidationRegex: "",
AudienceValidationRegex: "",
IssuerValidationRegex: "",
SubjectValidationRegex: "",
IdValidationRegex: "",
TokenAllowedClockSkew: time.Minute * 5,
IgnorePathRegex: "",
CredentialsOptional: false,
ValidateOnOptions: true,
}
}
type Plugin struct {
logger *log.Logger
ctx context.Context
flow jwt_flow.Flow
config *Config
name string
}
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
var err error
logger, err := log.NewLogger()
if err != nil {
err = fmt.Errorf("unable to intialize logger: %v", err)
return nil, err
}
/*
logger.Debug("Starting the traefik-plugin-oidc")
logger.Sync()
*/
// Parse config
// switch config.SsoRedirectUrlMacStrength {
// case sso_redirector.HmacStrength_256: {}
// case sso_redirector.HmacStrength_384: {}
// case sso_redirector.HmacStrength_512: {}
// default: {
// config.SsoRedirectUrlMacStrength = sso_redirector.HmacStrength_256
// }
config.SsoRedirectUrlMacStrength = sso_redirector.HmacStrength_256
if config.UseDynamicValidation == true && (config.Issuer == "" && config.IssuerValidationRegex == "") {
logger.Warn("DANGER DANGER DANGER when 'UseDynamicValidation' = 'true' and 'Issuer' = '' and 'IssuerValidationRegex' = '', then tokens from any IdP will be trusted - this is an insecure configuration DANGER DANGER DANGER", nil)
}
if config.SsoRedirectUrlMacClientSecret == "" && config.SsoRedirectUrlMacPrivateKey == "" && config.SsoRedirectUrlAddressTemplate != "" {
err = fmt.Errorf("config value 'SsoRedirectUrlMacPrivateKey' or 'SsoRedirectUrlMacClientSecret' must be set if config value 'SsoRedirectUrlAddressTemplate' is specified")
logger.Fatal("Unable to parse config", []encoder.Field{encoder.Error(err)})
return nil, err
}
if config.Issuer == "" && config.Audience == "" && config.JwksAddress == "" && config.OidcDiscoveryAddress == "" && config.UseDynamicValidation == false && config.ClientSecret == "" && config.SsoRedirectUrlMacPrivateKey == "" && config.SsoRedirectUrlMacClientSecret == "" && config.PublicKey == "" {
err = fmt.Errorf("configuration must be set")
logger.Fatal("Unable to parse config", []encoder.Field{encoder.Error(err)})
return nil, err
}
if !(config.ClientSecret != "" || config.PublicKey != "" || config.OidcDiscoveryAddress != "" || config.JwksAddress != "" || (config.Issuer != "" && config.UseDynamicValidation == true)) && config.Audience != "" {
err = fmt.Errorf("config value 'ClientSecret' or 'PublicKey' or 'OidcDiscoveryAddress' or 'JwksAddress' or 'Issuer' with UseDynamicValidation enabled, if config value 'Audience' is specified")
logger.Fatal("Unable to parse config", []encoder.Field{encoder.Error(err)})
return nil, err
}
if (config.ClientSecret == "" && config.PublicKey == "") && !(config.OidcDiscoveryAddress != "" || config.JwksAddress != "" || (config.Issuer != "" && config.UseDynamicValidation == true)) && config.SsoRedirectUrlAddressTemplate != "" {
err = fmt.Errorf("'OidcDiscoveryAddress' or 'JwksAddress' or 'Issuer' with UseDynamicValidation enabled, if config value 'SsoRedirectUrlAddressTemplate' is specified and 'ClientSecret' and 'PublicKey' have not been configured")
logger.Fatal("Unable to parse config", []encoder.Field{encoder.Error(err)})
return nil, err
}
//Redirect url for SSO
var ssoRedirectUrlAddressTemplate *template.Template
if config.SsoRedirectUrlAddressTemplate != "" {
ssoRedirectUrlAddressTemplate, err = sso_redirector.GetSsoRedirectUrlTemplate(config.SsoRedirectUrlAddressTemplate)
if err != nil {
logger.Fatal("Unable to parse config SsoRedirectUrlAddressTemplate", []encoder.Field{encoder.Error(err), encoder.String("SsoRedirectUrlAddressTemplate", config.SsoRedirectUrlAddressTemplate)})
return nil, err
}
} else {
ssoRedirectUrlAddressTemplate = nil
}
//Url hash using secret
var urlHashClientSecret []byte
if config.SsoRedirectUrlMacClientSecret != "" {
urlHashClientSecret = []byte(config.SsoRedirectUrlMacClientSecret)
} else {
urlHashClientSecret = nil
}
//Url hash using private key
var urlHashPrivateKey interface{}
if config.SsoRedirectUrlMacPrivateKey != "" {
urlHashPrivateKey, _, err = jwks.GetPrivateKeyFromFileOrContent(config.SsoRedirectUrlMacPrivateKey)
if err != nil {
logger.Fatal("Unable to parse config SsoRedirectUrlMacPrivateKey", []encoder.Field{encoder.Error(err)})
return nil, err
}
} else {
urlHashPrivateKey = nil
}
var ssoRedirectUrlMacSigningKey interface{}
if urlHashPrivateKey != nil {
ssoRedirectUrlMacSigningKey = urlHashPrivateKey
} else if urlHashClientSecret != nil {
ssoRedirectUrlMacSigningKey = urlHashClientSecret
} else {
ssoRedirectUrlMacSigningKey = nil
}
//Standard client secret Jwt validation
var clientSecret []byte
if config.ClientSecret != "" {
clientSecret = []byte(config.ClientSecret)
} else {
clientSecret = nil
}
//Standard certificate Jwt validation
var publicKey interface{}
if config.PublicKey != "" {
publicKey, _, err = jwks.GetPublicKeyFromFileOrContent(config.PublicKey)
if err != nil {
logger.Fatal("Unable to parse config PublicKey", []encoder.Field{encoder.Error(err), encoder.String("PublicKey", config.PublicKey)})
return nil, err
}
} else {
publicKey = nil
}
//Validations
var algorithmValidationRegex *regexp.Regexp
if config.AlgorithmValidationRegex != "" {
algorithmValidationRegex, err = regexp.Compile(config.AlgorithmValidationRegex)
if err != nil {
logger.Fatal("Unable to parse config AlgorithmValidationRegex", []encoder.Field{encoder.Error(err), encoder.String("AlgorithmValidationRegex", config.AlgorithmValidationRegex)})
return nil, err
}
} else {
algorithmValidationRegex = nil
}
var issuerValidationRegex *regexp.Regexp
if config.IssuerValidationRegex != "" {
issuerValidationRegex, err = regexp.Compile(config.IssuerValidationRegex)
if err != nil {
logger.Fatal("Unable to parse config IssuerValidationRegex", []encoder.Field{encoder.Error(err), encoder.String("IssuerValidationRegex", config.IssuerValidationRegex)})
return nil, err
}
} else {
issuerValidationRegex = nil
}
var audienceValidationRegex *regexp.Regexp
if config.AudienceValidationRegex != "" {
audienceValidationRegex, err = regexp.Compile(config.AudienceValidationRegex)
if err != nil {
logger.Fatal("Unable to parse config AudienceValidationRegex", []encoder.Field{encoder.Error(err), encoder.String("AudienceValidationRegex", config.AudienceValidationRegex)})
return nil, err
}
} else {
audienceValidationRegex = nil
}
var subjectValidationRegex *regexp.Regexp
if config.SubjectValidationRegex != "" {
subjectValidationRegex, err = regexp.Compile(config.SubjectValidationRegex)
if err != nil {
logger.Fatal("Unable to parse config SubjectValidationRegex", []encoder.Field{encoder.Error(err), encoder.String("SubjectValidationRegex", config.SubjectValidationRegex)})
return nil, err
}
} else {
subjectValidationRegex = nil
}
var idValidationRegex *regexp.Regexp
if config.IdValidationRegex != "" {
idValidationRegex, err = regexp.Compile(config.IdValidationRegex)
if err != nil {
logger.Fatal("Unable to parse config IdValidationRegex", []encoder.Field{encoder.Error(err), encoder.String("IdValidationRegex", config.IdValidationRegex)})
return nil, err
}
} else {
idValidationRegex = nil
}
//Paths to skip OIDC on
var ignorePathRegex *regexp.Regexp
if config.IgnorePathRegex != "" {
ignorePathRegex, err = regexp.Compile(config.IgnorePathRegex)
if err != nil {
logger.Fatal("Unable to parse config IgnorePathRegex", []encoder.Field{encoder.Error(err), encoder.String("IgnorePathRegex", config.IgnorePathRegex)})
return nil, err
}
} else {
ignorePathRegex = nil
}
errorHandler := jwt_flow.OidcErrorHandler(ssoRedirectUrlAddressTemplate, ssoRedirectUrlMacSigningKey, config.SsoRedirectUrlMacStrength)
successHandler := jwt_flow.OidcSuccessHandler(ssoRedirectUrlMacSigningKey, config.SsoRedirectUrlMacStrength, config.SsoRedirectUrlMacAllowedClockSkew)
tokenExtractor := jwt_flow.OidcTokenExtractor()
tokenValidator := validator.OidcTokenValidator(
algorithmValidationRegex,
issuerValidationRegex,
audienceValidationRegex,
subjectValidationRegex,
idValidationRegex,
config.TokenAllowedClockSkew,
clientSecret,
publicKey,
config.Issuer,
config.JwksAddress,
config.OidcDiscoveryAddress,
config.UseDynamicValidation,
)
oidcMiddleware := jwt_flow.New(
tokenValidator,
jwt_flow.WithCredentialsOptional(config.CredentialsOptional),
jwt_flow.WithValidateOnOptions(config.ValidateOnOptions),
jwt_flow.WithIgnorePathOptions(ignorePathRegex),
jwt_flow.WithTokenExtractor(tokenExtractor),
jwt_flow.WithErrorHandler(errorHandler),
jwt_flow.WithSuccessHandler(successHandler),
)
flow := oidcMiddleware.DefaultFlow(next)
return &Plugin{
logger: logger,
ctx: ctx,
name: name,
config: config,
flow: flow,
}, nil
}
const LogFieldRequestID = "requestID"
func (a *Plugin) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
requestId := guuid.NewString() //TODO: see if we can plugin to tracing plugins like jaeger/zipkin to get the tracing id. See https://doc.traefik.io/traefik/observability/tracing/overview/
loggerForRequest := a.logger.With([]encoder.Field{encoder.String(LogFieldRequestID, requestId)}) // it is a clone of the parent logger with the same properties but any additional changes will not be propagated to parent
a.flow(loggerForRequest, rw, req)
loggerForRequest.Sync()
}