git.iko.soy/typeable/oauth2-proxy
on 8cbf921 help sign in
424 lines 14 kB history raw
1package validation
3import (
4 "context"
5 "crypto"
6 "crypto/tls"
7 "fmt"
8 "io/ioutil"
9 "net/http"
10 "net/url"
11 "os"
12 "regexp"
13 "strings"
15 "github.com/coreos/go-oidc"
16 "github.com/dgrijalva/jwt-go"
17 "github.com/mbland/hmacauth"
18 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
19 "github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
20 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
21 "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
22 "github.com/oauth2-proxy/oauth2-proxy/pkg/util"
23 "github.com/oauth2-proxy/oauth2-proxy/providers"
26// Validate checks that required options are set and validates those that they
27// are of the correct format
28func Validate(o *options.Options) error {
29 msgs := validateCookie(o.Cookie)
30 msgs = append(msgs, validateSessionCookieMinimal(o)...)
31 msgs = append(msgs, validateRedisSessionStore(o)...)
33 if o.SSLInsecureSkipVerify {
34 // InsecureSkipVerify is a configurable option we allow
35 /* #nosec G402 */
36 insecureTransport := &http.Transport{
37 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
38 }
39 http.DefaultClient = &http.Client{Transport: insecureTransport}
40 } else if len(o.ProviderCAFiles) > 0 {
41 pool, err := util.GetCertPool(o.ProviderCAFiles)
42 if err == nil {
43 transport := &http.Transport{
44 TLSClientConfig: &tls.Config{
45 RootCAs: pool,
46 },
47 }
49 http.DefaultClient = &http.Client{Transport: transport}
50 } else {
51 msgs = append(msgs, fmt.Sprintf("unable to load provider CA file(s): %v", err))
52 }
53 }
55 if o.ClientID == "" {
56 msgs = append(msgs, "missing setting: client-id")
57 }
58 // login.gov uses a signed JWT to authenticate, not a client-secret
59 if o.ProviderType != "login.gov" {
60 if o.ClientSecret == "" && o.ClientSecretFile == "" {
61 msgs = append(msgs, "missing setting: client-secret or client-secret-file")
62 }
63 if o.ClientSecret == "" && o.ClientSecretFile != "" {
64 _, err := ioutil.ReadFile(o.ClientSecretFile)
65 if err != nil {
66 msgs = append(msgs, "could not read client secret file: "+o.ClientSecretFile)
67 }
68 }
69 }
70 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
71 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
72 "\n use email-domain=* to authorize all email addresses")
73 }
75 if o.SetBasicAuth && o.SetAuthorization {
76 msgs = append(msgs, "mutually exclusive: set-basic-auth and set-authorization-header can not both be true")
77 }
79 if o.OIDCIssuerURL != "" {
81 ctx := context.Background()
83 if o.InsecureOIDCSkipIssuerVerification && !o.SkipOIDCDiscovery {
84 // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
85 // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
86 // we get the URLs, we'll just use the non-discovery path.
88 logger.Printf("Performing OIDC Discovery...")
90 requestURL := strings.TrimSuffix(o.OIDCIssuerURL, "/") + "/.well-known/openid-configuration"
91 body, err := requests.New(requestURL).
92 WithContext(ctx).
93 Do().
94 UnmarshalJSON()
95 if err != nil {
96 logger.Errorf("error: failed to discover OIDC configuration: %v", err)
97 } else {
98 // Prefer manually configured URLs. It's a bit unclear
99 // why you'd be doing discovery and also providing the URLs
100 // explicitly though...
101 if o.LoginURL == "" {
102 o.LoginURL = body.Get("authorization_endpoint").MustString()
103 }
105 if o.RedeemURL == "" {
106 o.RedeemURL = body.Get("token_endpoint").MustString()
107 }
109 if o.OIDCJwksURL == "" {
110 o.OIDCJwksURL = body.Get("jwks_uri").MustString()
111 }
113 if o.ProfileURL == "" {
114 o.ProfileURL = body.Get("userinfo_endpoint").MustString()
115 }
117 o.SkipOIDCDiscovery = true
118 }
119 }
121 // Construct a manual IDTokenVerifier from issuer URL & JWKS URI
122 // instead of metadata discovery if we enable -skip-oidc-discovery.
123 // In this case we need to make sure the required endpoints for
124 // the provider are configured.
125 if o.SkipOIDCDiscovery {
126 if o.LoginURL == "" {
127 msgs = append(msgs, "missing setting: login-url")
128 }
129 if o.RedeemURL == "" {
130 msgs = append(msgs, "missing setting: redeem-url")
131 }
132 if o.OIDCJwksURL == "" {
133 msgs = append(msgs, "missing setting: oidc-jwks-url")
134 }
135 keySet := oidc.NewRemoteKeySet(ctx, o.OIDCJwksURL)
136 o.SetOIDCVerifier(oidc.NewVerifier(o.OIDCIssuerURL, keySet, &oidc.Config{
137 ClientID: o.ClientID,
138 SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
139 }))
140 } else {
141 // Configure discoverable provider data.
142 provider, err := oidc.NewProvider(ctx, o.OIDCIssuerURL)
143 if err != nil {
144 return err
145 }
146 o.SetOIDCVerifier(provider.Verifier(&oidc.Config{
147 ClientID: o.ClientID,
148 SkipIssuerCheck: o.InsecureOIDCSkipIssuerVerification,
149 }))
151 o.LoginURL = provider.Endpoint().AuthURL
152 o.RedeemURL = provider.Endpoint().TokenURL
153 }
154 if o.Scope == "" {
155 o.Scope = "openid email profile"
157 if len(o.AllowedGroups) > 0 {
158 o.Scope += " groups"
159 }
160 }
161 }
163 if o.PreferEmailToUser && !o.PassBasicAuth && !o.PassUserHeaders {
164 msgs = append(msgs, "PreferEmailToUser should only be used with PassBasicAuth or PassUserHeaders")
165 }
167 if o.SkipJwtBearerTokens {
168 // Configure extra issuers
169 if len(o.ExtraJwtIssuers) > 0 {
170 var jwtIssuers []jwtIssuer
171 jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
172 for _, jwtIssuer := range jwtIssuers {
173 verifier, err := newVerifierFromJwtIssuer(jwtIssuer)
174 if err != nil {
175 msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
176 }
177 o.SetJWTBearerVerifiers(append(o.GetJWTBearerVerifiers(), verifier))
178 }
179 }
180 }
182 var redirectURL *url.URL
183 redirectURL, msgs = parseURL(o.RawRedirectURL, "redirect", msgs)
184 o.SetRedirectURL(redirectURL)
186 msgs = append(msgs, validateUpstreams(o.UpstreamServers)...)
188 for _, u := range o.SkipAuthRegex {
189 compiledRegex, err := regexp.Compile(u)
190 if err != nil {
191 msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
192 continue
193 }
194 o.SetCompiledRegex(append(o.GetCompiledRegex(), compiledRegex))
195 }
196 msgs = parseProviderInfo(o, msgs)
198 if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
199 if len(o.GoogleGroups) < 1 {
200 msgs = append(msgs, "missing setting: google-group")
201 }
202 if o.GoogleAdminEmail == "" {
203 msgs = append(msgs, "missing setting: google-admin-email")
204 }
205 if o.GoogleServiceAccountJSON == "" {
206 msgs = append(msgs, "missing setting: google-service-account-json")
207 }
208 }
210 msgs = parseSignatureKey(o, msgs)
211 msgs = configureLogger(o.Logging, msgs)
213 if o.ReverseProxy {
214 parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader)
215 if err != nil {
216 msgs = append(msgs, fmt.Sprintf("real_client_ip_header (%s) not accepted parameter value: %v", o.RealClientIPHeader, err))
217 }
218 o.SetRealClientIPParser(parser)
220 // Allow the logger to get client IPs
221 logger.SetGetClientFunc(func(r *http.Request) string {
222 return ip.GetClientString(o.GetRealClientIPParser(), r, false)
223 })
224 }
226 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
227 _, err := fmt.Fprintln(os.Stderr, "WARNING: trusting of IPs with --reverse-proxy poses risks if a header spoofing attack is possible.")
228 if err != nil {
229 panic(err)
230 }
231 }
233 for i, ipStr := range o.TrustedIPs {
234 if nil == ip.ParseIPNet(ipStr) {
235 msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr))
236 }
237 }
239 if len(msgs) != 0 {
240 return fmt.Errorf("invalid configuration:\n %s",
241 strings.Join(msgs, "\n "))
242 }
243 return nil
246func parseProviderInfo(o *options.Options, msgs []string) []string {
247 p := &providers.ProviderData{
248 Scope: o.Scope,
249 ClientID: o.ClientID,
250 ClientSecret: o.ClientSecret,
251 ClientSecretFile: o.ClientSecretFile,
252 Prompt: o.Prompt,
253 ApprovalPrompt: o.ApprovalPrompt,
254 AcrValues: o.AcrValues,
255 }
256 p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
257 p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
258 p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
259 p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
260 p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
262 o.SetProvider(providers.New(o.ProviderType, p))
263 switch p := o.GetProvider().(type) {
264 case *providers.AzureProvider:
265 p.Configure(o.AzureTenant)
266 case *providers.GitHubProvider:
267 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
268 p.SetRepo(o.GitHubRepo, o.GitHubToken)
269 p.SetUsers(o.GitHubUsers)
270 case *providers.KeycloakProvider:
271 p.SetGroup(o.KeycloakGroup)
272 case *providers.GoogleProvider:
273 if o.GoogleServiceAccountJSON != "" {
274 file, err := os.Open(o.GoogleServiceAccountJSON)
275 if err != nil {
276 msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON)
277 } else {
278 p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
279 }
280 }
281 case *providers.BitbucketProvider:
282 p.SetTeam(o.BitbucketTeam)
283 p.SetRepository(o.BitbucketRepository)
284 case *providers.OIDCProvider:
285 p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
286 p.UserIDClaim = o.UserIDClaim
287 p.GroupsClaim = o.OIDCGroupsClaim
288 if o.GetOIDCVerifier() == nil {
289 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
290 } else {
291 p.Verifier = o.GetOIDCVerifier()
292 }
293 case *providers.GitLabProvider:
294 p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
295 p.Groups = o.GitLabGroup
296 p.EmailDomains = o.EmailDomains
298 if o.GetOIDCVerifier() != nil {
299 p.Verifier = o.GetOIDCVerifier()
300 } else {
301 // Initialize with default verifier for gitlab.com
302 ctx := context.Background()
304 provider, err := oidc.NewProvider(ctx, "https://gitlab.com")
305 if err != nil {
306 msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
307 } else {
308 p.Verifier = provider.Verifier(&oidc.Config{
309 ClientID: o.ClientID,
310 })
312 p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
313 p.RedeemURL, msgs = parseURL(provider.Endpoint().TokenURL, "redeem", msgs)
314 }
315 }
316 case *providers.LoginGovProvider:
317 p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs)
319 // JWT key can be supplied via env variable or file in the filesystem, but not both.
320 switch {
321 case o.JWTKey != "" && o.JWTKeyFile != "":
322 msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options")
323 case o.JWTKey == "" && o.JWTKeyFile == "":
324 msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
325 case o.JWTKey != "":
326 // The JWT Key is in the commandline argument
327 signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.JWTKey))
328 if err != nil {
329 msgs = append(msgs, "could not parse RSA Private Key PEM")
330 } else {
331 p.JWTKey = signKey
332 }
333 case o.JWTKeyFile != "":
334 // The JWT key is in the filesystem
335 keyData, err := ioutil.ReadFile(o.JWTKeyFile)
336 if err != nil {
337 msgs = append(msgs, "could not read key file: "+o.JWTKeyFile)
338 }
339 signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
340 if err != nil {
341 msgs = append(msgs, "could not parse private key from PEM file:"+o.JWTKeyFile)
342 } else {
343 p.JWTKey = signKey
344 }
345 }
346 }
347 return msgs
350func parseSignatureKey(o *options.Options, msgs []string) []string {
351 if o.SignatureKey == "" {
352 return msgs
353 }
355 components := strings.Split(o.SignatureKey, ":")
356 if len(components) != 2 {
357 return append(msgs, "invalid signature hash:key spec: "+
358 o.SignatureKey)
359 }
361 algorithm, secretKey := components[0], components[1]
362 var hash crypto.Hash
363 var err error
364 if hash, err = hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
365 return append(msgs, "unsupported signature hash algorithm: "+
366 o.SignatureKey)
367 }
368 o.SetSignatureData(&options.SignatureData{Hash: hash, Key: secretKey})
369 return msgs
372// parseJwtIssuers takes in an array of strings in the form of issuer=audience
373// and parses to an array of jwtIssuer structs.
374func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
375 parsedIssuers := make([]jwtIssuer, 0, len(issuers))
376 for _, jwtVerifier := range issuers {
377 components := strings.Split(jwtVerifier, "=")
378 if len(components) < 2 {
379 msgs = append(msgs, fmt.Sprintf("invalid jwt verifier uri=audience spec: %s", jwtVerifier))
380 continue
381 }
382 uri, audience := components[0], strings.Join(components[1:], "=")
383 parsedIssuers = append(parsedIssuers, jwtIssuer{issuerURI: uri, audience: audience})
384 }
385 return parsedIssuers, msgs
388// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
389// a verifier for that issuer.
390func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error) {
391 config := &oidc.Config{
392 ClientID: jwtIssuer.audience,
393 }
394 // Try as an OpenID Connect Provider first
395 var verifier *oidc.IDTokenVerifier
396 provider, err := oidc.NewProvider(context.Background(), jwtIssuer.issuerURI)
397 if err != nil {
398 // Try as JWKS URI
399 jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
400 if err := requests.New(jwksURI).Do().Error(); err != nil {
401 return nil, err
402 }
404 verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
405 } else {
406 verifier = provider.Verifier(config)
407 }
408 return verifier, nil
411// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
412type jwtIssuer struct {
413 issuerURI string
414 audience string
417func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
418 parsed, err := url.Parse(toParse)
419 if err != nil {
420 return nil, append(msgs, fmt.Sprintf(
421 "error parsing %s-url=%q %s", urltype, toParse, err))
422 }
423 return parsed, msgs