git.iko.soy/typeable/oauth2-proxy
on ea261ca help sign in
409 lines 15 kB history raw
1package validation
3import (
4 "context"
5 "crypto/tls"
6 "fmt"
7 "io/ioutil"
8 "net/http"
9 "net/url"
10 "os"
11 "strings"
13 "github.com/coreos/go-oidc/v3/oidc"
14 "github.com/golang-jwt/jwt"
15 "github.com/mbland/hmacauth"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
18 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
19 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
20 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
24// Validate checks that required options are set and validates those that they
25// are of the correct format
26func Validate(o *options.Options) error {
27 msgs := validateCookie(o.Cookie)
28 msgs = append(msgs, validateSessionCookieMinimal(o)...)
29 msgs = append(msgs, validateRedisSessionStore(o)...)
30 msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
31 msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
32 msgs = append(msgs, validateProviders(o)...)
33 msgs = configureLogger(o.Logging, msgs)
34 msgs = parseSignatureKey(o, msgs)
36 if o.SSLInsecureSkipVerify {
37 // InsecureSkipVerify is a configurable option we allow
38 /* #nosec G402 */
39 insecureTransport := &http.Transport{
40 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
41 }
42 http.DefaultClient = &http.Client{Transport: insecureTransport}
43 } else if len(o.Providers[0].CAFiles) > 0 {
44 pool, err := util.GetCertPool(o.Providers[0].CAFiles)
45 if err == nil {
46 transport := http.DefaultTransport.(*http.Transport).Clone()
47 transport.TLSClientConfig = &tls.Config{
48 RootCAs: pool,
49 MinVersion: tls.VersionTLS12,
50 }
52 http.DefaultClient = &http.Client{Transport: transport}
53 } else {
54 msgs = append(msgs, fmt.Sprintf("unable to load provider CA file(s): %v", err))
55 }
56 }
58 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
59 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
60 "\n use email-domain=* to authorize all email addresses")
61 }
63 if o.Providers[0].OIDCConfig.IssuerURL != "" {
65 ctx := context.Background()
67 if o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification && !o.Providers[0].OIDCConfig.SkipDiscovery {
68 // go-oidc doesn't let us pass bypass the issuer check this in the oidc.NewProvider call
69 // (which uses discovery to get the URLs), so we'll do a quick check ourselves and if
70 // we get the URLs, we'll just use the non-discovery path.
72 logger.Printf("Performing OIDC Discovery...")
74 requestURL := strings.TrimSuffix(o.Providers[0].OIDCConfig.IssuerURL, "/") + "/.well-known/openid-configuration"
75 body, err := requests.New(requestURL).
76 WithContext(ctx).
77 Do().
78 UnmarshalJSON()
79 if err != nil {
80 logger.Errorf("error: failed to discover OIDC configuration: %v", err)
81 } else {
82 // Prefer manually configured URLs. It's a bit unclear
83 // why you'd be doing discovery and also providing the URLs
84 // explicitly though...
85 if o.Providers[0].LoginURL == "" {
86 o.Providers[0].LoginURL = body.Get("authorization_endpoint").MustString()
87 }
89 if o.Providers[0].RedeemURL == "" {
90 o.Providers[0].RedeemURL = body.Get("token_endpoint").MustString()
91 }
93 if o.Providers[0].OIDCConfig.JwksURL == "" {
94 o.Providers[0].OIDCConfig.JwksURL = body.Get("jwks_uri").MustString()
95 }
97 if o.Providers[0].ProfileURL == "" {
98 o.Providers[0].ProfileURL = body.Get("userinfo_endpoint").MustString()
99 }
101 o.Providers[0].OIDCConfig.SkipDiscovery = true
102 }
103 }
105 // Construct a manual IDTokenVerifier from issuer URL & JWKS URI
106 // instead of metadata discovery if we enable -skip-oidc-discovery.
107 // In this case we need to make sure the required endpoints for
108 // the provider are configured.
109 if o.Providers[0].OIDCConfig.SkipDiscovery {
110 if o.Providers[0].LoginURL == "" {
111 msgs = append(msgs, "missing setting: login-url")
112 }
113 if o.Providers[0].RedeemURL == "" {
114 msgs = append(msgs, "missing setting: redeem-url")
115 }
116 if o.Providers[0].OIDCConfig.JwksURL == "" {
117 msgs = append(msgs, "missing setting: oidc-jwks-url")
118 }
119 keySet := oidc.NewRemoteKeySet(ctx, o.Providers[0].OIDCConfig.JwksURL)
120 o.SetOIDCVerifier(oidc.NewVerifier(o.Providers[0].OIDCConfig.IssuerURL, keySet, &oidc.Config{
121 ClientID: o.Providers[0].ClientID,
122 SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
123 }))
124 } else {
125 // Configure discoverable provider data.
126 provider, err := oidc.NewProvider(ctx, o.Providers[0].OIDCConfig.IssuerURL)
127 if err != nil {
128 return err
129 }
130 o.SetOIDCVerifier(provider.Verifier(&oidc.Config{
131 ClientID: o.Providers[0].ClientID,
132 SkipIssuerCheck: o.Providers[0].OIDCConfig.InsecureSkipIssuerVerification,
133 }))
135 o.Providers[0].LoginURL = provider.Endpoint().AuthURL
136 o.Providers[0].RedeemURL = provider.Endpoint().TokenURL
137 }
138 if o.Providers[0].Scope == "" {
139 o.Providers[0].Scope = "openid email profile"
141 if len(o.Providers[0].AllowedGroups) > 0 {
142 o.Providers[0].Scope += " groups"
143 }
144 }
145 if o.Providers[0].OIDCConfig.UserIDClaim == "" {
146 o.Providers[0].OIDCConfig.UserIDClaim = "email"
147 }
148 }
150 if o.SkipJwtBearerTokens {
151 // Configure extra issuers
152 if len(o.ExtraJwtIssuers) > 0 {
153 var jwtIssuers []jwtIssuer
154 jwtIssuers, msgs = parseJwtIssuers(o.ExtraJwtIssuers, msgs)
155 for _, jwtIssuer := range jwtIssuers {
156 verifier, err := newVerifierFromJwtIssuer(jwtIssuer)
157 if err != nil {
158 msgs = append(msgs, fmt.Sprintf("error building verifiers: %s", err))
159 }
160 o.SetJWTBearerVerifiers(append(o.GetJWTBearerVerifiers(), verifier))
161 }
162 }
163 }
165 var redirectURL *url.URL
166 redirectURL, msgs = parseURL(o.RawRedirectURL, "redirect", msgs)
167 o.SetRedirectURL(redirectURL)
168 if o.RawRedirectURL == "" && !o.Cookie.Secure && !o.ReverseProxy {
169 logger.Print("WARNING: no explicit redirect URL: redirects will default to insecure HTTP")
170 }
172 msgs = append(msgs, validateUpstreams(o.UpstreamServers)...)
173 msgs = parseProviderInfo(o, msgs)
175 if o.ReverseProxy {
176 parser, err := ip.GetRealClientIPParser(o.RealClientIPHeader)
177 if err != nil {
178 msgs = append(msgs, fmt.Sprintf("real_client_ip_header (%s) not accepted parameter value: %v", o.RealClientIPHeader, err))
179 }
180 o.SetRealClientIPParser(parser)
182 // Allow the logger to get client IPs
183 logger.SetGetClientFunc(func(r *http.Request) string {
184 return ip.GetClientString(o.GetRealClientIPParser(), r, false)
185 })
186 }
188 // Do this after ReverseProxy validation for TrustedIP coordinated checks
189 msgs = append(msgs, validateAllowlists(o)...)
191 if len(msgs) != 0 {
192 return fmt.Errorf("invalid configuration:\n %s",
193 strings.Join(msgs, "\n "))
194 }
195 return nil
198func parseProviderInfo(o *options.Options, msgs []string) []string {
199 p := &providers.ProviderData{
200 Scope: o.Providers[0].Scope,
201 ClientID: o.Providers[0].ClientID,
202 ClientSecret: o.Providers[0].ClientSecret,
203 ClientSecretFile: o.Providers[0].ClientSecretFile,
204 Prompt: o.Providers[0].Prompt,
205 ApprovalPrompt: o.Providers[0].ApprovalPrompt,
206 AcrValues: o.Providers[0].AcrValues,
207 }
208 p.LoginURL, msgs = parseURL(o.Providers[0].LoginURL, "login", msgs)
209 p.RedeemURL, msgs = parseURL(o.Providers[0].RedeemURL, "redeem", msgs)
210 p.ProfileURL, msgs = parseURL(o.Providers[0].ProfileURL, "profile", msgs)
211 p.ValidateURL, msgs = parseURL(o.Providers[0].ValidateURL, "validate", msgs)
212 p.ProtectedResource, msgs = parseURL(o.Providers[0].ProtectedResource, "resource", msgs)
214 // Make the OIDC options available to all providers that support it
215 p.AllowUnverifiedEmail = o.Providers[0].OIDCConfig.InsecureAllowUnverifiedEmail
216 p.EmailClaim = o.Providers[0].OIDCConfig.EmailClaim
217 p.GroupsClaim = o.Providers[0].OIDCConfig.GroupsClaim
218 p.Verifier = o.GetOIDCVerifier()
220 // TODO (@NickMeves) - Remove This
221 // Backwards Compatibility for Deprecated UserIDClaim option
222 if o.Providers[0].OIDCConfig.EmailClaim == providers.OIDCEmailClaim &&
223 o.Providers[0].OIDCConfig.UserIDClaim != providers.OIDCEmailClaim {
224 p.EmailClaim = o.Providers[0].OIDCConfig.UserIDClaim
225 }
227 p.SetAllowedGroups(o.Providers[0].AllowedGroups)
229 provider := providers.New(o.Providers[0].Type, p)
230 if provider == nil {
231 msgs = append(msgs, fmt.Sprintf("invalid setting: provider '%s' is not available", o.Providers[0].Type))
232 return msgs
233 }
234 o.SetProvider(provider)
236 switch p := o.GetProvider().(type) {
237 case *providers.AzureProvider:
238 p.Configure(o.Providers[0].AzureConfig.Tenant)
239 case *providers.ADFSProvider:
240 p.Configure(o.Providers[0].ADFSConfig.SkipScope)
241 case *providers.GitHubProvider:
242 p.SetOrgTeam(o.Providers[0].GitHubConfig.Org, o.Providers[0].GitHubConfig.Team)
243 p.SetRepo(o.Providers[0].GitHubConfig.Repo, o.Providers[0].GitHubConfig.Token)
244 p.SetUsers(o.Providers[0].GitHubConfig.Users)
245 case *providers.KeycloakProvider:
246 // Backwards compatibility with `--keycloak-group` option
247 if len(o.Providers[0].KeycloakConfig.Groups) > 0 {
248 p.SetAllowedGroups(o.Providers[0].KeycloakConfig.Groups)
249 }
250 case *providers.KeycloakOIDCProvider:
251 if p.Verifier == nil {
252 msgs = append(msgs, "keycloak-oidc provider requires an oidc issuer URL")
253 }
254 p.AddAllowedRoles(o.Providers[0].KeycloakConfig.Roles)
255 case *providers.GoogleProvider:
256 if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" {
257 file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON)
258 if err != nil {
259 msgs = append(msgs, "invalid Google credentials file: "+o.Providers[0].GoogleConfig.ServiceAccountJSON)
260 } else {
261 groups := o.Providers[0].AllowedGroups
262 // Backwards compatibility with `--google-group` option
263 if len(o.Providers[0].GoogleConfig.Groups) > 0 {
264 groups = o.Providers[0].GoogleConfig.Groups
265 p.SetAllowedGroups(groups)
266 }
267 p.SetGroupRestriction(groups, o.Providers[0].GoogleConfig.AdminEmail, file)
268 }
269 }
270 case *providers.BitbucketProvider:
271 p.SetTeam(o.Providers[0].BitbucketConfig.Team)
272 p.SetRepository(o.Providers[0].BitbucketConfig.Repository)
273 case *providers.OIDCProvider:
274 p.SkipNonce = o.Providers[0].OIDCConfig.InsecureSkipNonce
275 if p.Verifier == nil {
276 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
277 }
278 case *providers.GitLabProvider:
279 p.SetAllowedGroups(o.Providers[0].GitLabConfig.Group)
280 err := p.SetAllowedProjects(o.Providers[0].GitLabConfig.Projects)
281 if err != nil {
282 msgs = append(msgs, "failed to setup gitlab project access level")
283 }
285 if p.Verifier == nil {
286 // Initialize with default verifier for gitlab.com
287 ctx := context.Background()
289 provider, err := oidc.NewProvider(ctx, "https://gitlab.com")
290 if err != nil {
291 msgs = append(msgs, "failed to initialize oidc provider for gitlab.com")
292 } else {
293 p.Verifier = provider.Verifier(&oidc.Config{
294 ClientID: o.Providers[0].ClientID,
295 })
297 p.LoginURL, msgs = parseURL(provider.Endpoint().AuthURL, "login", msgs)
298 p.RedeemURL, msgs = parseURL(provider.Endpoint().TokenURL, "redeem", msgs)
299 }
300 }
301 case *providers.LoginGovProvider:
302 p.PubJWKURL, msgs = parseURL(o.Providers[0].LoginGovConfig.PubJWKURL, "pubjwk", msgs)
304 // JWT key can be supplied via env variable or file in the filesystem, but not both.
305 switch {
306 case o.Providers[0].LoginGovConfig.JWTKey != "" && o.Providers[0].LoginGovConfig.JWTKeyFile != "":
307 msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options")
308 case o.Providers[0].LoginGovConfig.JWTKey == "" && o.Providers[0].LoginGovConfig.JWTKeyFile == "":
309 msgs = append(msgs, "login.gov provider requires a private key for signing JWTs")
310 case o.Providers[0].LoginGovConfig.JWTKey != "":
311 // The JWT Key is in the commandline argument
312 signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.Providers[0].LoginGovConfig.JWTKey))
313 if err != nil {
314 msgs = append(msgs, "could not parse RSA Private Key PEM")
315 } else {
316 p.JWTKey = signKey
317 }
318 case o.Providers[0].LoginGovConfig.JWTKeyFile != "":
319 // The JWT key is in the filesystem
320 keyData, err := ioutil.ReadFile(o.Providers[0].LoginGovConfig.JWTKeyFile)
321 if err != nil {
322 msgs = append(msgs, "could not read key file: "+o.Providers[0].LoginGovConfig.JWTKeyFile)
323 }
324 signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
325 if err != nil {
326 msgs = append(msgs, "could not parse private key from PEM file:"+o.Providers[0].LoginGovConfig.JWTKeyFile)
327 } else {
328 p.JWTKey = signKey
329 }
330 }
331 }
332 return msgs
335func parseSignatureKey(o *options.Options, msgs []string) []string {
336 if o.SignatureKey == "" {
337 return msgs
338 }
340 logger.Print("WARNING: `--signature-key` is deprecated. It will be removed in a future release")
342 components := strings.Split(o.SignatureKey, ":")
343 if len(components) != 2 {
344 return append(msgs, "invalid signature hash:key spec: "+
345 o.SignatureKey)
346 }
348 algorithm, secretKey := components[0], components[1]
349 hash, err := hmacauth.DigestNameToCryptoHash(algorithm)
350 if err != nil {
351 return append(msgs, "unsupported signature hash algorithm: "+o.SignatureKey)
352 }
353 o.SetSignatureData(&options.SignatureData{Hash: hash, Key: secretKey})
354 return msgs
357// parseJwtIssuers takes in an array of strings in the form of issuer=audience
358// and parses to an array of jwtIssuer structs.
359func parseJwtIssuers(issuers []string, msgs []string) ([]jwtIssuer, []string) {
360 parsedIssuers := make([]jwtIssuer, 0, len(issuers))
361 for _, jwtVerifier := range issuers {
362 components := strings.Split(jwtVerifier, "=")
363 if len(components) < 2 {
364 msgs = append(msgs, fmt.Sprintf("invalid jwt verifier uri=audience spec: %s", jwtVerifier))
365 continue
366 }
367 uri, audience := components[0], strings.Join(components[1:], "=")
368 parsedIssuers = append(parsedIssuers, jwtIssuer{issuerURI: uri, audience: audience})
369 }
370 return parsedIssuers, msgs
373// newVerifierFromJwtIssuer takes in issuer information in jwtIssuer info and returns
374// a verifier for that issuer.
375func newVerifierFromJwtIssuer(jwtIssuer jwtIssuer) (*oidc.IDTokenVerifier, error) {
376 config := &oidc.Config{
377 ClientID: jwtIssuer.audience,
378 }
379 // Try as an OpenID Connect Provider first
380 var verifier *oidc.IDTokenVerifier
381 provider, err := oidc.NewProvider(context.Background(), jwtIssuer.issuerURI)
382 if err != nil {
383 // Try as JWKS URI
384 jwksURI := strings.TrimSuffix(jwtIssuer.issuerURI, "/") + "/.well-known/jwks.json"
385 if err := requests.New(jwksURI).Do().Error(); err != nil {
386 return nil, err
387 }
389 verifier = oidc.NewVerifier(jwtIssuer.issuerURI, oidc.NewRemoteKeySet(context.Background(), jwksURI), config)
390 } else {
391 verifier = provider.Verifier(config)
392 }
393 return verifier, nil
396// jwtIssuer hold parsed JWT issuer info that's used to construct a verifier.
397type jwtIssuer struct {
398 issuerURI string
399 audience string
402func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
403 parsed, err := url.Parse(toParse)
404 if err != nil {
405 return nil, append(msgs, fmt.Sprintf(
406 "error parsing %s-url=%q %s", urltype, toParse, err))
407 }
408 return parsed, msgs