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