git.iko.soy/typeable/oauth2-proxy
on master help sign in
Support for client secret file. (#355)

* added ClientSecretFile in ProviderData * add documentation notes on client secret file * added Changelog entry for Client Secret File PR * fixing configuration.md * addressing PR issue of ClientSecret property naming * Update providers/provider_data.go Co-Authored-By: Joel Speed <Joel.speed@hotmail.co.uk> * corrected changelog entry * fixed typo in GetClientSecret Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commitf2661c47ba5367c9de34929fde4c6a84701c58f0 authorPavel Kirichenko <pasha-r@standalone.name> dateSat 15 Feb 2020, 14:44 (7 years ago) parent3a843f8
11 files changed+161−27
CHANGELOG.md+2 docs/configuration/configuration.md+4 options.go+28−18 options_test.go+51−1 providers/azure.go+6−1 providers/gitlab.go+12−2 providers/google.go+11−2 providers/google_test.go+12 providers/oidc.go+12−2 providers/provider_data.go+18 providers/provider_default.go+5−1
CHANGELOG.md+2
@@ -13,6 +13,7 @@
1313- [#335](https://github.com/pusher/oauth2_proxy/pull/335) OIDC Provider support for empty id_tokens in the access token refresh response (@howzat)
1414- [#363](https://github.com/pusher/oauth2_proxy/pull/363) Extension of Redis Session Store to Support Redis Cluster (@yan-dblinf)
1515- [#353](https://github.com/pusher/oauth2_proxy/pull/353) Fix login page fragment handling after soft reload on Firefox (@ffdybuster)
16+- [#355](https://github.com/pusher/oauth2_proxy/pull/355) Add Client Secret File support for providers that rotate client secret via file system (@pasha-r)
1617
1718# v5.0.0
1819
@@ -40,6 +41,7 @@
4041- [#179](https://github.com/pusher/oauth2_proxy/pull/179) Add Nextcloud provider (@Ramblurr)
4142- [#280](https://github.com/pusher/oauth2_proxy/pull/280) whitelisted redirect domains: add support for whitelisting specific ports or allowing wildcard ports (@kamaln7)
4243- [#351](https://github.com/pusher/oauth2_proxy/pull/351) Add DigitalOcean Auth provider (@kamaln7)
44+
4345# v4.1.0
4446
4547## Release Highlights
docs/configuration/configuration.md+4
@@ -29,6 +29,7 @@ An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example
2929| `-basic-auth-password` | string | the password to set when passing the HTTP Basic Auth header | |
3030| `-client-id` | string | the OAuth Client ID: ie: `"123456.apps.googleusercontent.com"` | |
3131| `-client-secret` | string | the OAuth Client Secret | |
32+| `-client-secret-file` | string | the file with OAuth Client Secret | |
3233| `-config` | string | path to config file | |
3334| `-cookie-domain` | string | an optional cookie domain to force cookies to (ie: `.yourcompany.com`) | |
3435| `-cookie-expire` | duration | expire timeframe for cookie | 168h0m0s |
@@ -329,3 +330,6 @@ nginx.ingress.kubernetes.io/configuration-snippet: |
329330```
330331
331332You have to substitute *name* with the actual cookie name you configured via --cookie-name parameter. If you don't set a custom cookie name the variable should be "$upstream_cookie__oauth2_proxy_1" instead of "$upstream_cookie_name_1" and the new cookie-name should be "_oauth2_proxy_1=" instead of "name_1=".
333+
334+### Note on rotated Client Secret
335+If you set up your OAuth2 provider to rotate your client secret, you can use the `client-secret-file` option to reload the secret when it is updated.
options.go+28−18
@@ -29,18 +29,19 @@ import (
2929// Options holds Configuration Options that can be set by Command Line Flag,
3030// or Config File
3131type Options struct {
32− ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
33− PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"`
34− ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"`
35− HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"`
36− HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"`
37− ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"`
38− ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"`
39− RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"`
40− ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
41− ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
42− TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"`
43− TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
32+ ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix" env:"OAUTH2_PROXY_PROXY_PREFIX"`
33+ PingPath string `flag:"ping-path" cfg:"ping_path" env:"OAUTH2_PROXY_PING_PATH"`
34+ ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets" env:"OAUTH2_PROXY_PROXY_WEBSOCKETS"`
35+ HTTPAddress string `flag:"http-address" cfg:"http_address" env:"OAUTH2_PROXY_HTTP_ADDRESS"`
36+ HTTPSAddress string `flag:"https-address" cfg:"https_address" env:"OAUTH2_PROXY_HTTPS_ADDRESS"`
37+ ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy" env:"OAUTH2_PROXY_REVERSE_PROXY"`
38+ ForceHTTPS bool `flag:"force-https" cfg:"force_https" env:"OAUTH2_PROXY_FORCE_HTTPS"`
39+ RedirectURL string `flag:"redirect-url" cfg:"redirect_url" env:"OAUTH2_PROXY_REDIRECT_URL"`
40+ ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
41+ ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
42+ ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file" env:"OAUTH2_PROXY_CLIENT_SECRET_FILE"`
43+ TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file" env:"OAUTH2_PROXY_TLS_CERT_FILE"`
44+ TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file" env:"OAUTH2_PROXY_TLS_KEY_FILE"`
4445
4546 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file" env:"OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE"`
4647 KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group" env:"OAUTH2_PROXY_KEYCLOAK_GROUP"`
@@ -222,8 +223,16 @@ func (o *Options) Validate() error {
222223 msgs = append(msgs, "missing setting: client-id")
223224 }
224225 // login.gov uses a signed JWT to authenticate, not a client-secret
225− if o.ClientSecret == "" && o.Provider != "login.gov" {
226− msgs = append(msgs, "missing setting: client-secret")
226+ if o.Provider != "login.gov" {
227+ if o.ClientSecret == "" && o.ClientSecretFile == "" {
228+ msgs = append(msgs, "missing setting: client-secret or client-secret-file")
229+ }
230+ if o.ClientSecret == "" && o.ClientSecretFile != "" {
231+ _, err := ioutil.ReadFile(o.ClientSecretFile)
232+ if err != nil {
233+ msgs = append(msgs, "could not read client secret file: "+o.ClientSecretFile)
234+ }
235+ }
227236 }
228237 if o.AuthenticatedEmailsFile == "" && len(o.EmailDomains) == 0 && o.HtpasswdFile == "" {
229238 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required."+
@@ -392,10 +401,11 @@ func (o *Options) Validate() error {
392401
393402func parseProviderInfo(o *Options, msgs []string) []string {
394403 p := &providers.ProviderData{
395− Scope: o.Scope,
396− ClientID: o.ClientID,
397− ClientSecret: o.ClientSecret,
398− ApprovalPrompt: o.ApprovalPrompt,
404+ Scope: o.Scope,
405+ ClientID: o.ClientID,
406+ ClientSecret: o.ClientSecret,
407+ ClientSecretFile: o.ClientSecretFile,
408+ ApprovalPrompt: o.ApprovalPrompt,
399409 }
400410 p.LoginURL, msgs = parseURL(o.LoginURL, "login", msgs)
401411 p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
options_test.go+51−1
@@ -3,7 +3,9 @@ package main
33import (
44 "crypto"
55 "fmt"
6+ "io/ioutil"
67 "net/url"
8+ "os"
79 "strings"
810 "testing"
911 "time"
@@ -37,10 +39,58 @@ func TestNewOptions(t *testing.T) {
3739 expected := errorMsg([]string{
3840 "missing setting: cookie-secret",
3941 "missing setting: client-id",
40− "missing setting: client-secret"})
42+ "missing setting: client-secret or client-secret-file"})
4143 assert.Equal(t, expected, err.Error())
4244}
4345
46+func TestClientSecretFileOptionFails(t *testing.T) {
47+ o := NewOptions()
48+ o.CookieSecret = "foobar"
49+ o.ClientID = "bazquux"
50+ o.ClientSecretFile = "xyzzyplugh"
51+ o.EmailDomains = []string{"*"}
52+ err := o.Validate()
53+ assert.NotEqual(t, nil, err)
54+
55+ p := o.provider.Data()
56+ assert.Equal(t, "xyzzyplugh", p.ClientSecretFile)
57+ assert.Equal(t, "", p.ClientSecret)
58+
59+ s, err := p.GetClientSecret()
60+ assert.NotEqual(t, nil, err)
61+ assert.Equal(t, "", s)
62+}
63+
64+func TestClientSecretFileOption(t *testing.T) {
65+ var err error
66+ f, err := ioutil.TempFile("", "client_secret_temp_file_")
67+ if err != nil {
68+ t.Fatalf("failed to create temp file: %v", err)
69+ }
70+ f.WriteString("testcase")
71+ if err := f.Close(); err != nil {
72+ t.Fatalf("failed to close temp file: %v", err)
73+ }
74+ clientSecretFileName := f.Name()
75+ defer os.Remove(clientSecretFileName)
76+
77+ o := NewOptions()
78+ o.CookieSecret = "foobar"
79+ o.ClientID = "bazquux"
80+ o.ClientSecretFile = clientSecretFileName
81+ o.EmailDomains = []string{"*"}
82+ err = o.Validate()
83+ assert.Equal(t, nil, err)
84+
85+ p := o.provider.Data()
86+ assert.Equal(t, clientSecretFileName, p.ClientSecretFile)
87+ assert.Equal(t, "", p.ClientSecret)
88+
89+ s, err := p.GetClientSecret()
90+ assert.Equal(t, nil, err)
91+ assert.Equal(t, "testcase", s)
92+}
93+
4494func TestGoogleGroupOptions(t *testing.T) {
4595 o := testOptions()
4696 o.GoogleGroups = []string{"googlegroup"}
providers/azure.go+6−1
@@ -74,11 +74,16 @@ func (p *AzureProvider) Redeem(redirectURL, code string) (s *sessions.SessionSta
7474 err = errors.New("missing code")
7575 return
7676 }
77+ clientSecret, err := p.GetClientSecret()
78+ if err != nil {
79+ return
80+ }
81+
7782
7883 params := url.Values{}
7984 params.Add("redirect_uri", redirectURL)
8085 params.Add("client_id", p.ClientID)
81− params.Add("client_secret", p.ClientSecret)
86+ params.Add("client_secret", clientSecret)
8287 params.Add("code", code)
8388 params.Add("grant_type", "authorization_code")
8489 if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
providers/gitlab.go+12−2
@@ -38,10 +38,15 @@ func NewGitLabProvider(p *ProviderData) *GitLabProvider {
3838
3939// Redeem exchanges the OAuth2 authentication token for an ID token
4040func (p *GitLabProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
41+ clientSecret, err := p.GetClientSecret()
42+ if err != nil {
43+ return
44+ }
45+
4146 ctx := context.Background()
4247 c := oauth2.Config{
4348 ClientID: p.ClientID,
44− ClientSecret: p.ClientSecret,
49+ ClientSecret: clientSecret,
4550 Endpoint: oauth2.Endpoint{
4651 TokenURL: p.RedeemURL.String(),
4752 },
@@ -77,9 +82,14 @@ func (p *GitLabProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool,
7782}
7883
7984func (p *GitLabProvider) redeemRefreshToken(s *sessions.SessionState) (err error) {
85+ clientSecret, err := p.GetClientSecret()
86+ if err != nil {
87+ return
88+ }
89+
8090 c := oauth2.Config{
8191 ClientID: p.ClientID,
82− ClientSecret: p.ClientSecret,
92+ ClientSecret: clientSecret,
8393 Endpoint: oauth2.Endpoint{
8494 TokenURL: p.RedeemURL.String(),
8595 },
providers/google.go+11−2
@@ -102,11 +102,15 @@ func (p *GoogleProvider) Redeem(redirectURL, code string) (s *sessions.SessionSt
102102 err = errors.New("missing code")
103103 return
104104 }
105+ clientSecret, err := p.GetClientSecret()
106+ if err != nil {
107+ return
108+ }
105109
106110 params := url.Values{}
107111 params.Add("redirect_uri", redirectURL)
108112 params.Add("client_id", p.ClientID)
109− params.Add("client_secret", p.ClientSecret)
113+ params.Add("client_secret", clientSecret)
110114 params.Add("code", code)
111115 params.Add("grant_type", "authorization_code")
112116 var req *http.Request
@@ -261,9 +265,14 @@ func (p *GoogleProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool,
261265
262266func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, idToken string, expires time.Duration, err error) {
263267 // https://developers.google.com/identity/protocols/OAuth2WebServer#refresh
268+ clientSecret, err := p.GetClientSecret()
269+ if err != nil {
270+ return
271+ }
272+
264273 params := url.Values{}
265274 params.Add("client_id", p.ClientID)
266− params.Add("client_secret", p.ClientSecret)
275+ params.Add("client_secret", clientSecret)
267276 params.Add("refresh_token", refreshToken)
268277 params.Add("grant_type", "refresh_token")
269278 var req *http.Request
providers/google_test.go+12
@@ -146,6 +146,18 @@ func TestGoogleProviderGetEmailAddressInvalidEncoding(t *testing.T) {
146146 }
147147}
148148
149+func TestGoogleProviderRedeemFailsNoCLientSecret(t *testing.T) {
150+ p := newGoogleProvider()
151+ p.ProviderData.ClientSecretFile = "srvnoerre"
152+
153+ session, err := p.Redeem("http://redirect/", "code1234")
154+ assert.NotEqual(t, nil, err)
155+ if session != nil {
156+ t.Errorf("expect nill session %#v", session)
157+ }
158+ assert.Equal(t, "could not read client secret file", err.Error())
159+}
160+
149161func TestGoogleProviderGetEmailAddressInvalidJson(t *testing.T) {
150162 p := newGoogleProvider()
151163
providers/oidc.go+12−2
@@ -30,10 +30,15 @@ func NewOIDCProvider(p *ProviderData) *OIDCProvider {
3030
3131// Redeem exchanges the OAuth2 authentication token for an ID token
3232func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
33+ clientSecret, err := p.GetClientSecret()
34+ if err != nil {
35+ return
36+ }
37+
3338 ctx := context.Background()
3439 c := oauth2.Config{
3540 ClientID: p.ClientID,
36− ClientSecret: p.ClientSecret,
41+ ClientSecret: clientSecret,
3742 Endpoint: oauth2.Endpoint{
3843 TokenURL: p.RedeemURL.String(),
3944 },
@@ -77,9 +82,14 @@ func (p *OIDCProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, e
7782}
7883
7984func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) {
85+ clientSecret, err := p.GetClientSecret()
86+ if err != nil {
87+ return
88+ }
89+
8090 c := oauth2.Config{
8191 ClientID: p.ClientID,
82− ClientSecret: p.ClientSecret,
92+ ClientSecret: clientSecret,
8393 Endpoint: oauth2.Endpoint{
8494 TokenURL: p.RedeemURL.String(),
8595 },
providers/provider_data.go+18
@@ -1,6 +1,9 @@
11package providers
22
33import (
4+ "errors"
5+ "github.com/pusher/oauth2_proxy/pkg/logger"
6+ "io/ioutil"
47 "net/url"
58)
69
@@ -10,6 +13,7 @@ type ProviderData struct {
1013 ProviderName string
1114 ClientID string
1215 ClientSecret string
16+ ClientSecretFile string
1317 LoginURL *url.URL
1418 RedeemURL *url.URL
1519 ProfileURL *url.URL
@@ -21,3 +25,17 @@ type ProviderData struct {
2125
2226// Data returns the ProviderData
2327func (p *ProviderData) Data() *ProviderData { return p }
28+
29+func (p *ProviderData) GetClientSecret() (ClientSecret string, err error) {
30+ if p.ClientSecret != "" || p.ClientSecretFile == "" {
31+ return p.ClientSecret, nil
32+ }
33+
34+ // Getting ClientSecret can fail in runtime so we need to report it without returning the file name to the user
35+ fileClientSecret, err := ioutil.ReadFile(p.ClientSecretFile)
36+ if err != nil {
37+ logger.Printf("error reading client secret file %s: %s", p.ClientSecretFile, err)
38+ return "", errors.New("could not read client secret file")
39+ }
40+ return string(fileClientSecret), nil
41+}
providers/provider_default.go+5−1
@@ -20,11 +20,15 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *sessions.SessionStat
2020 err = errors.New("missing code")
2121 return
2222 }
23+ clientSecret, err := p.GetClientSecret()
24+ if err != nil {
25+ return
26+ }
2327
2428 params := url.Values{}
2529 params.Add("redirect_uri", redirectURL)
2630 params.Add("client_id", p.ClientID)
27− params.Add("client_secret", p.ClientSecret)
31+ params.Add("client_secret", clientSecret)
2832 params.Add("code", code)
2933 params.Add("grant_type", "authorization_code")
3034 if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {