git.iko.soy/typeable/oauth2-proxy
on v3.0.0 help sign in
341 lines 12 kB history raw
1package main
3import (
4 "context"
5 "crypto"
6 "crypto/tls"
7 "encoding/base64"
8 "fmt"
9 "net/http"
10 "net/url"
11 "os"
12 "regexp"
13 "strings"
14 "time"
16 oidc "github.com/coreos/go-oidc"
17 "github.com/mbland/hmacauth"
18 "github.com/pusher/oauth2_proxy/providers"
21// Options holds Configuration Options that can be set by Command Line Flag,
22// or Config File
23type Options struct {
24 ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix"`
25 HTTPAddress string `flag:"http-address" cfg:"http_address"`
26 HTTPSAddress string `flag:"https-address" cfg:"https_address"`
27 RedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
28 ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
29 ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
30 TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file"`
31 TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"`
33 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
34 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
35 EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
36 GitHubOrg string `flag:"github-org" cfg:"github_org"`
37 GitHubTeam string `flag:"github-team" cfg:"github_team"`
38 GoogleGroups []string `flag:"google-group" cfg:"google_group"`
39 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
40 GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
41 HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
42 DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
43 CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
44 Footer string `flag:"footer" cfg:"footer"`
46 CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
47 CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
48 CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
49 CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
50 CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
51 CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure"`
52 CookieHTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
54 Upstreams []string `flag:"upstream" cfg:"upstreams"`
55 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
56 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
57 BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"`
58 PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"`
59 PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
60 SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"`
61 PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers"`
62 SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
63 SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"`
64 SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
66 // These options allow for other providers besides Google, with
67 // potential overrides.
68 Provider string `flag:"provider" cfg:"provider"`
69 OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
70 LoginURL string `flag:"login-url" cfg:"login_url"`
71 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
72 ProfileURL string `flag:"profile-url" cfg:"profile_url"`
73 ProtectedResource string `flag:"resource" cfg:"resource"`
74 ValidateURL string `flag:"validate-url" cfg:"validate_url"`
75 Scope string `flag:"scope" cfg:"scope"`
76 ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`
78 RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
79 RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format"`
81 SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`
83 // internal values that are set after config validation
84 redirectURL *url.URL
85 proxyURLs []*url.URL
86 CompiledRegex []*regexp.Regexp
87 provider providers.Provider
88 signatureData *SignatureData
89 oidcVerifier *oidc.IDTokenVerifier
92// SignatureData holds hmacauth signature hash and key
93type SignatureData struct {
94 hash crypto.Hash
95 key string
98// NewOptions constructs a new Options with defaulted values
99func NewOptions() *Options {
100 return &Options{
101 ProxyPrefix: "/oauth2",
102 HTTPAddress: "127.0.0.1:4180",
103 HTTPSAddress: ":443",
104 DisplayHtpasswdForm: true,
105 CookieName: "_oauth2_proxy",
106 CookieSecure: true,
107 CookieHTTPOnly: true,
108 CookieExpire: time.Duration(168) * time.Hour,
109 CookieRefresh: time.Duration(0),
110 SetXAuthRequest: false,
111 SkipAuthPreflight: false,
112 PassBasicAuth: true,
113 PassUserHeaders: true,
114 PassAccessToken: false,
115 PassHostHeader: true,
116 ApprovalPrompt: "force",
117 RequestLogging: true,
118 RequestLoggingFormat: defaultRequestLoggingFormat,
119 }
122func parseURL(toParse string, urltype string, msgs []string) (*url.URL, []string) {
123 parsed, err := url.Parse(toParse)
124 if err != nil {
125 return nil, append(msgs, fmt.Sprintf(
126 "error parsing %s-url=%q %s", urltype, toParse, err))
127 }
128 return parsed, msgs
131// Validate checks that required options are set and validates those that they
132// are of the correct format
133func (o *Options) Validate() error {
134 if o.SSLInsecureSkipVerify {
135 // TODO: Accept a certificate bundle.
136 insecureTransport := &http.Transport{
137 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
138 }
139 http.DefaultClient = &http.Client{Transport: insecureTransport}
140 }
142 msgs := make([]string, 0)
143 if o.CookieSecret == "" {
144 msgs = append(msgs, "missing setting: cookie-secret")
145 }
146 if o.ClientID == "" {
147 msgs = append(msgs, "missing setting: client-id")
148 }
149 if o.ClientSecret == "" {
150 msgs = append(msgs, "missing setting: client-secret")
151 }
152 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
153 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
154 "\n use email-domain=* to authorize all email addresses")
155 }
157 if o.OIDCIssuerURL != "" {
158 // Configure discoverable provider data.
159 provider, err := oidc.NewProvider(context.Background(), o.OIDCIssuerURL)
160 if err != nil {
161 return err
162 }
163 o.oidcVerifier = provider.Verifier(&oidc.Config{
164 ClientID: o.ClientID,
165 })
166 o.LoginURL = provider.Endpoint().AuthURL
167 o.RedeemURL = provider.Endpoint().TokenURL
168 if o.Scope == "" {
169 o.Scope = "openid email profile"
170 }
171 }
173 o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
175 for _, u := range o.Upstreams {
176 upstreamURL, err := url.Parse(u)
177 if err != nil {
178 msgs = append(msgs, fmt.Sprintf("error parsing upstream: %s", err))
179 } else {
180 if upstreamURL.Path == "" {
181 upstreamURL.Path = "/"
182 }
183 o.proxyURLs = append(o.proxyURLs, upstreamURL)
184 }
185 }
187 for _, u := range o.SkipAuthRegex {
188 CompiledRegex, err := regexp.Compile(u)
189 if err != nil {
190 msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
191 continue
192 }
193 o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
194 }
195 msgs = parseProviderInfo(o, msgs)
197 if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
198 validCookieSecretSize := false
199 for _, i := range []int{16, 24, 32} {
200 if len(secretBytes(o.CookieSecret)) == i {
201 validCookieSecretSize = true
202 }
203 }
204 var decoded bool
205 if string(secretBytes(o.CookieSecret)) != o.CookieSecret {
206 decoded = true
207 }
208 if validCookieSecretSize == false {
209 var suffix string
210 if decoded {
211 suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret)
212 }
213 msgs = append(msgs, fmt.Sprintf(
214 "cookie_secret must be 16, 24, or 32 bytes "+
215 "to create an AES cipher when "+
216 "pass_access_token == true or "+
217 "cookie_refresh != 0, but is %d bytes.%s",
218 len(secretBytes(o.CookieSecret)), suffix))
219 }
220 }
222 if o.CookieRefresh >= o.CookieExpire {
223 msgs = append(msgs, fmt.Sprintf(
224 "cookie_refresh (%s) must be less than "+
225 "cookie_expire (%s)",
226 o.CookieRefresh.String(),
227 o.CookieExpire.String()))
228 }
230 if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
231 if len(o.GoogleGroups) < 1 {
232 msgs = append(msgs, "missing setting: google-group")
233 }
234 if o.GoogleAdminEmail == "" {
235 msgs = append(msgs, "missing setting: google-admin-email")
236 }
237 if o.GoogleServiceAccountJSON == "" {
238 msgs = append(msgs, "missing setting: google-service-account-json")
239 }
240 }
242 msgs = parseSignatureKey(o, msgs)
243 msgs = validateCookieName(o, msgs)
245 if len(msgs) != 0 {
246 return fmt.Errorf("Invalid configuration:\n %s",
247 strings.Join(msgs, "\n "))
248 }
249 return nil
252func parseProviderInfo(o *Options, msgs []string) []string {
253 p := &providers.ProviderData{
254 Scope: o.Scope,
255 ClientID: o.ClientID,
256 ClientSecret: o.ClientSecret,
257 ApprovalPrompt: o.ApprovalPrompt,
258 }
259 p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
260 p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
261 p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
262 p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
263 p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
265 o.provider = providers.New(o.Provider, p)
266 switch p := o.provider.(type) {
267 case *providers.AzureProvider:
268 p.Configure(o.AzureTenant)
269 case *providers.GitHubProvider:
270 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
271 case *providers.GoogleProvider:
272 if o.GoogleServiceAccountJSON != "" {
273 file, err := os.Open(o.GoogleServiceAccountJSON)
274 if err != nil {
275 msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON)
276 } else {
277 p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
278 }
279 }
280 case *providers.OIDCProvider:
281 if o.oidcVerifier == nil {
282 msgs = append(msgs, "oidc provider requires an oidc issuer URL")
283 } else {
284 p.Verifier = o.oidcVerifier
285 }
286 }
287 return msgs
290func parseSignatureKey(o *Options, msgs []string) []string {
291 if o.SignatureKey == "" {
292 return msgs
293 }
295 components := strings.Split(o.SignatureKey, ":")
296 if len(components) != 2 {
297 return append(msgs, "invalid signature hash:key spec: "+
298 o.SignatureKey)
299 }
301 algorithm, secretKey := components[0], components[1]
302 var hash crypto.Hash
303 var err error
304 if hash, err = hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
305 return append(msgs, "unsupported signature hash algorithm: "+
306 o.SignatureKey)
307 }
308 o.signatureData = &SignatureData{hash, secretKey}
309 return msgs
312func validateCookieName(o *Options, msgs []string) []string {
313 cookie := &http.Cookie{Name: o.CookieName}
314 if cookie.String() == "" {
315 return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName))
316 }
317 return msgs
320func addPadding(secret string) string {
321 padding := len(secret) % 4
322 switch padding {
323 case 1:
324 return secret + "==="
325 case 2:
326 return secret + "=="
327 case 3:
328 return secret + "="
329 default:
330 return secret
331 }
334// secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
335func secretBytes(secret string) []byte {
336 b, err := base64.URLEncoding.DecodeString(addPadding(secret))
337 if err == nil {
338 return []byte(addPadding(string(b)))
339 }
340 return []byte(secret)