git.iko.soy/typeable/oauth2-proxy
on v2.2 help sign in
309 lines 11 kB history raw
1package main
3import (
4 "crypto"
5 "crypto/tls"
6 "encoding/base64"
7 "fmt"
8 "net/http"
9 "net/url"
10 "os"
11 "regexp"
12 "strings"
13 "time"
15 "github.com/18F/hmacauth"
16 "github.com/bitly/oauth2_proxy/providers"
19// Configuration Options that can be set by Command Line Flag, or Config File
20type Options struct {
21 ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix"`
22 HttpAddress string `flag:"http-address" cfg:"http_address"`
23 HttpsAddress string `flag:"https-address" cfg:"https_address"`
24 RedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
25 ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
26 ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
27 TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file"`
28 TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"`
30 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
31 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
32 EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
33 GitHubOrg string `flag:"github-org" cfg:"github_org"`
34 GitHubTeam string `flag:"github-team" cfg:"github_team"`
35 GoogleGroups []string `flag:"google-group" cfg:"google_group"`
36 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
37 GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
38 HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"`
39 DisplayHtpasswdForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"`
40 CustomTemplatesDir string `flag:"custom-templates-dir" cfg:"custom_templates_dir"`
41 Footer string `flag:"footer" cfg:"footer"`
43 CookieName string `flag:"cookie-name" cfg:"cookie_name" env:"OAUTH2_PROXY_COOKIE_NAME"`
44 CookieSecret string `flag:"cookie-secret" cfg:"cookie_secret" env:"OAUTH2_PROXY_COOKIE_SECRET"`
45 CookieDomain string `flag:"cookie-domain" cfg:"cookie_domain" env:"OAUTH2_PROXY_COOKIE_DOMAIN"`
46 CookieExpire time.Duration `flag:"cookie-expire" cfg:"cookie_expire" env:"OAUTH2_PROXY_COOKIE_EXPIRE"`
47 CookieRefresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh" env:"OAUTH2_PROXY_COOKIE_REFRESH"`
48 CookieSecure bool `flag:"cookie-secure" cfg:"cookie_secure"`
49 CookieHttpOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"`
51 Upstreams []string `flag:"upstream" cfg:"upstreams"`
52 SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"`
53 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"`
54 BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"`
55 PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"`
56 PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
57 SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"`
58 PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers"`
59 SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
60 SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"`
61 SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
63 // These options allow for other providers besides Google, with
64 // potential overrides.
65 Provider string `flag:"provider" cfg:"provider"`
66 LoginURL string `flag:"login-url" cfg:"login_url"`
67 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
68 ProfileURL string `flag:"profile-url" cfg:"profile_url"`
69 ProtectedResource string `flag:"resource" cfg:"resource"`
70 ValidateURL string `flag:"validate-url" cfg:"validate_url"`
71 Scope string `flag:"scope" cfg:"scope"`
72 ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`
74 RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
76 SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`
78 // internal values that are set after config validation
79 redirectURL *url.URL
80 proxyURLs []*url.URL
81 CompiledRegex []*regexp.Regexp
82 provider providers.Provider
83 signatureData *SignatureData
86type SignatureData struct {
87 hash crypto.Hash
88 key string
91func NewOptions() *Options {
92 return &Options{
93 ProxyPrefix: "/oauth2",
94 HttpAddress: "127.0.0.1:4180",
95 HttpsAddress: ":443",
96 DisplayHtpasswdForm: true,
97 CookieName: "_oauth2_proxy",
98 CookieSecure: true,
99 CookieHttpOnly: true,
100 CookieExpire: time.Duration(168) * time.Hour,
101 CookieRefresh: time.Duration(0),
102 SetXAuthRequest: false,
103 SkipAuthPreflight: false,
104 PassBasicAuth: true,
105 PassUserHeaders: true,
106 PassAccessToken: false,
107 PassHostHeader: true,
108 ApprovalPrompt: "force",
109 RequestLogging: true,
110 }
113func parseURL(to_parse string, urltype string, msgs []string) (*url.URL, []string) {
114 parsed, err := url.Parse(to_parse)
115 if err != nil {
116 return nil, append(msgs, fmt.Sprintf(
117 "error parsing %s-url=%q %s", urltype, to_parse, err))
118 }
119 return parsed, msgs
122func (o *Options) Validate() error {
123 msgs := make([]string, 0)
124 if len(o.Upstreams) < 1 {
125 msgs = append(msgs, "missing setting: upstream")
126 }
127 if o.CookieSecret == "" {
128 msgs = append(msgs, "missing setting: cookie-secret")
129 }
130 if o.ClientID == "" {
131 msgs = append(msgs, "missing setting: client-id")
132 }
133 if o.ClientSecret == "" {
134 msgs = append(msgs, "missing setting: client-secret")
135 }
136 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
137 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required.\n use email-domain=* to authorize all email addresses")
138 }
140 o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
142 for _, u := range o.Upstreams {
143 upstreamURL, err := url.Parse(u)
144 if err != nil {
145 msgs = append(msgs, fmt.Sprintf(
146 "error parsing upstream=%q %s",
147 upstreamURL, err))
148 }
149 if upstreamURL.Path == "" {
150 upstreamURL.Path = "/"
151 }
152 o.proxyURLs = append(o.proxyURLs, upstreamURL)
153 }
155 for _, u := range o.SkipAuthRegex {
156 CompiledRegex, err := regexp.Compile(u)
157 if err != nil {
158 msgs = append(msgs, fmt.Sprintf(
159 "error compiling regex=%q %s", u, err))
160 }
161 o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
162 }
163 msgs = parseProviderInfo(o, msgs)
165 if o.PassAccessToken || (o.CookieRefresh != time.Duration(0)) {
166 valid_cookie_secret_size := false
167 for _, i := range []int{16, 24, 32} {
168 if len(secretBytes(o.CookieSecret)) == i {
169 valid_cookie_secret_size = true
170 }
171 }
172 var decoded bool
173 if string(secretBytes(o.CookieSecret)) != o.CookieSecret {
174 decoded = true
175 }
176 if valid_cookie_secret_size == false {
177 var suffix string
178 if decoded {
179 suffix = fmt.Sprintf(" note: cookie secret was base64 decoded from %q", o.CookieSecret)
180 }
181 msgs = append(msgs, fmt.Sprintf(
182 "cookie_secret must be 16, 24, or 32 bytes "+
183 "to create an AES cipher when "+
184 "pass_access_token == true or "+
185 "cookie_refresh != 0, but is %d bytes.%s",
186 len(secretBytes(o.CookieSecret)), suffix))
187 }
188 }
190 if o.CookieRefresh >= o.CookieExpire {
191 msgs = append(msgs, fmt.Sprintf(
192 "cookie_refresh (%s) must be less than "+
193 "cookie_expire (%s)",
194 o.CookieRefresh.String(),
195 o.CookieExpire.String()))
196 }
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 = validateCookieName(o, msgs)
213 if o.SSLInsecureSkipVerify {
214 insecureTransport := &http.Transport{
215 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
216 }
217 http.DefaultClient = &http.Client{Transport: insecureTransport}
218 }
220 if len(msgs) != 0 {
221 return fmt.Errorf("Invalid configuration:\n %s",
222 strings.Join(msgs, "\n "))
223 }
224 return nil
227func parseProviderInfo(o *Options, msgs []string) []string {
228 p := &providers.ProviderData{
229 Scope: o.Scope,
230 ClientID: o.ClientID,
231 ClientSecret: o.ClientSecret,
232 ApprovalPrompt: o.ApprovalPrompt,
233 }
234 p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
235 p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
236 p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
237 p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
238 p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
240 o.provider = providers.New(o.Provider, p)
241 switch p := o.provider.(type) {
242 case *providers.AzureProvider:
243 p.Configure(o.AzureTenant)
244 case *providers.GitHubProvider:
245 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
246 case *providers.GoogleProvider:
247 if o.GoogleServiceAccountJSON != "" {
248 file, err := os.Open(o.GoogleServiceAccountJSON)
249 if err != nil {
250 msgs = append(msgs, "invalid Google credentials file: "+o.GoogleServiceAccountJSON)
251 } else {
252 p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
253 }
254 }
255 }
256 return msgs
259func parseSignatureKey(o *Options, msgs []string) []string {
260 if o.SignatureKey == "" {
261 return msgs
262 }
264 components := strings.Split(o.SignatureKey, ":")
265 if len(components) != 2 {
266 return append(msgs, "invalid signature hash:key spec: "+
267 o.SignatureKey)
268 }
270 algorithm, secretKey := components[0], components[1]
271 if hash, err := hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
272 return append(msgs, "unsupported signature hash algorithm: "+
273 o.SignatureKey)
274 } else {
275 o.signatureData = &SignatureData{hash, secretKey}
276 }
277 return msgs
280func validateCookieName(o *Options, msgs []string) []string {
281 cookie := &http.Cookie{Name: o.CookieName}
282 if cookie.String() == "" {
283 return append(msgs, fmt.Sprintf("invalid cookie name: %q", o.CookieName))
284 }
285 return msgs
288func addPadding(secret string) string {
289 padding := len(secret) % 4
290 switch padding {
291 case 1:
292 return secret + "==="
293 case 2:
294 return secret + "=="
295 case 3:
296 return secret + "="
297 default:
298 return secret
299 }
302// secretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
303func secretBytes(secret string) []byte {
304 b, err := base64.URLEncoding.DecodeString(addPadding(secret))
305 if err == nil {
306 return []byte(addPadding(string(b)))
307 }
308 return []byte(secret)