Sign Upstream requests with HMAC. closes #147
This commit is contained in:
parent
7c241ec1fe
commit
e4626c1360
1
Godeps
1
Godeps
@ -1,3 +1,4 @@
|
|||||||
|
github.com/18F/hmacauth 1.0.1
|
||||||
github.com/BurntSushi/toml 3883ac1ce943878302255f538fce319d23226223
|
github.com/BurntSushi/toml 3883ac1ce943878302255f538fce319d23226223
|
||||||
github.com/bitly/go-simplejson 3378bdcb5cebedcbf8b5750edee28010f128fe24
|
github.com/bitly/go-simplejson 3378bdcb5cebedcbf8b5750edee28010f128fe24
|
||||||
github.com/mreiferson/go-options ee94b57f2fbf116075426f853e5abbcdfeca8b3d
|
github.com/mreiferson/go-options ee94b57f2fbf116075426f853e5abbcdfeca8b3d
|
||||||
|
29
README.md
29
README.md
@ -113,15 +113,16 @@ An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is i
|
|||||||
|
|
||||||
```
|
```
|
||||||
Usage of oauth2_proxy:
|
Usage of oauth2_proxy:
|
||||||
-approval_prompt="force": Oauth approval_prompt
|
-approval-prompt="force": Oauth approval_prompt
|
||||||
-authenticated-emails-file="": authenticate against emails via file (one per line)
|
-authenticated-emails-file="": authenticate against emails via file (one per line)
|
||||||
|
-basic-auth-password="": the password to set when passing the HTTP Basic Auth header
|
||||||
-client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
|
-client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
|
||||||
-client-secret="": the OAuth Client Secret
|
-client-secret="": the OAuth Client Secret
|
||||||
-config="": path to config file
|
-config="": path to config file
|
||||||
-cookie-domain="": an optional cookie domain to force cookies to (ie: .yourcompany.com)*
|
-cookie-domain="": an optional cookie domain to force cookies to (ie: .yourcompany.com)*
|
||||||
-cookie-expire=168h0m0s: expire timeframe for cookie
|
-cookie-expire=168h0m0s: expire timeframe for cookie
|
||||||
-cookie-httponly=true: set HttpOnly cookie flag
|
-cookie-httponly=true: set HttpOnly cookie flag
|
||||||
-cookie-key="_oauth2_proxy": the name of the cookie that the oauth_proxy creates
|
-cookie-name="_oauth2_proxy": the name of the cookie that the oauth_proxy creates
|
||||||
-cookie-refresh=0: refresh the cookie after this duration; 0 to disable
|
-cookie-refresh=0: refresh the cookie after this duration; 0 to disable
|
||||||
-cookie-secret="": the seed string for secure cookies
|
-cookie-secret="": the seed string for secure cookies
|
||||||
-cookie-secure=true: set secure (HTTPS) cookie flag
|
-cookie-secure=true: set secure (HTTPS) cookie flag
|
||||||
@ -130,17 +131,15 @@ Usage of oauth2_proxy:
|
|||||||
-email-domain=: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
|
-email-domain=: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
|
||||||
-github-org="": restrict logins to members of this organisation
|
-github-org="": restrict logins to members of this organisation
|
||||||
-github-team="": restrict logins to members of this team
|
-github-team="": restrict logins to members of this team
|
||||||
-google-group="": restrict logins to members of this google group
|
|
||||||
-google-admin-email="": the google admin to impersonate for api calls
|
-google-admin-email="": the google admin to impersonate for api calls
|
||||||
|
-google-group=: restrict logins to members of this google group (may be given multiple times).
|
||||||
-google-service-account-json="": the path to the service account json credentials
|
-google-service-account-json="": the path to the service account json credentials
|
||||||
|
|
||||||
-htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
|
-htpasswd-file="": additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption
|
||||||
-http-address="127.0.0.1:4180": [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients
|
-http-address="127.0.0.1:4180": [http://]<addr>:<port> or unix://<path> to listen on for HTTP clients
|
||||||
-https-address=":443": <addr>:<port> to listen on for HTTPS clients
|
-https-address=":443": <addr>:<port> to listen on for HTTPS clients
|
||||||
-login-url="": Authentication endpoint
|
-login-url="": Authentication endpoint
|
||||||
-pass-access-token=false: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
|
-pass-access-token=false: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
|
||||||
-pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
|
-pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
|
||||||
-basic-auth-password="": the password to set when passing the HTTP Basic Auth header
|
|
||||||
-pass-host-header=true: pass the request Host Header to upstream
|
-pass-host-header=true: pass the request Host Header to upstream
|
||||||
-profile-url="": Profile access endpoint
|
-profile-url="": Profile access endpoint
|
||||||
-provider="google": OAuth provider
|
-provider="google": OAuth provider
|
||||||
@ -149,6 +148,7 @@ Usage of oauth2_proxy:
|
|||||||
-redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
|
-redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
|
||||||
-request-logging=true: Log requests to stdout
|
-request-logging=true: Log requests to stdout
|
||||||
-scope="": Oauth scope specification
|
-scope="": Oauth scope specification
|
||||||
|
-signature-key="": GAP-Signature request signature key (algorithm:secretkey)
|
||||||
-skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times)
|
-skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times)
|
||||||
-tls-cert="": path to certificate file
|
-tls-cert="": path to certificate file
|
||||||
-tls-key="": path to private key file
|
-tls-key="": path to private key file
|
||||||
@ -250,6 +250,24 @@ OAuth2 Proxy responds directly to the following endpoints. All other endpoints w
|
|||||||
* /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.
|
* /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url.
|
||||||
* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request)
|
* /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](#nginx-auth-request)
|
||||||
|
|
||||||
|
## Request signatures
|
||||||
|
|
||||||
|
If `signature_key` is defined, proxied requests will be signed with the
|
||||||
|
`GAP-Signature` header, which is a [Hash-based Message Authentication Code
|
||||||
|
(HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code)
|
||||||
|
of selected request information and the request body [see `SIGNATURE_HEADERS`
|
||||||
|
in `oauthproxy.go`](./oauthproxy.go).
|
||||||
|
|
||||||
|
`signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`)
|
||||||
|
|
||||||
|
For more information about HMAC request signature validation, read the
|
||||||
|
following:
|
||||||
|
|
||||||
|
* [Amazon Web Services: Signing and Authenticating REST
|
||||||
|
Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)
|
||||||
|
* [rc3.org: Using HMAC to authenticate Web service
|
||||||
|
requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/)
|
||||||
|
|
||||||
## Logging Format
|
## Logging Format
|
||||||
|
|
||||||
OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log.
|
OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log.
|
||||||
@ -258,7 +276,6 @@ OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log.
|
|||||||
<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
|
<REMOTE_ADDRESS> - <user@domain.com> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Adding a new Provider
|
## Adding a new Provider
|
||||||
|
|
||||||
Follow the examples in the [`providers` package](providers/) to define a new
|
Follow the examples in the [`providers` package](providers/) to define a new
|
||||||
|
2
main.go
2
main.go
@ -69,6 +69,8 @@ func main() {
|
|||||||
flagSet.String("scope", "", "OAuth scope specification")
|
flagSet.String("scope", "", "OAuth scope specification")
|
||||||
flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
|
flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
|
||||||
|
|
||||||
|
flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)")
|
||||||
|
|
||||||
flagSet.Parse(os.Args[1:])
|
flagSet.Parse(os.Args[1:])
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
|
@ -14,10 +14,26 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/18F/hmacauth"
|
||||||
"github.com/bitly/oauth2_proxy/cookie"
|
"github.com/bitly/oauth2_proxy/cookie"
|
||||||
"github.com/bitly/oauth2_proxy/providers"
|
"github.com/bitly/oauth2_proxy/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const SignatureHeader = "GAP-Signature"
|
||||||
|
|
||||||
|
var SignatureHeaders []string = []string{
|
||||||
|
"Content-Length",
|
||||||
|
"Content-Md5",
|
||||||
|
"Content-Type",
|
||||||
|
"Date",
|
||||||
|
"Authorization",
|
||||||
|
"X-Forwarded-User",
|
||||||
|
"X-Forwarded-Email",
|
||||||
|
"X-Forwarded-Access-Token",
|
||||||
|
"Cookie",
|
||||||
|
"Gap-Auth",
|
||||||
|
}
|
||||||
|
|
||||||
type OAuthProxy struct {
|
type OAuthProxy struct {
|
||||||
CookieSeed string
|
CookieSeed string
|
||||||
CookieName string
|
CookieName string
|
||||||
@ -54,10 +70,15 @@ type OAuthProxy struct {
|
|||||||
type UpstreamProxy struct {
|
type UpstreamProxy struct {
|
||||||
upstream string
|
upstream string
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
|
auth hmacauth.HmacAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("GAP-Upstream-Address", u.upstream)
|
w.Header().Set("GAP-Upstream-Address", u.upstream)
|
||||||
|
if u.auth != nil {
|
||||||
|
r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
|
||||||
|
u.auth.SignRequest(r)
|
||||||
|
}
|
||||||
u.handler.ServeHTTP(w, r)
|
u.handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +110,11 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
|
|||||||
|
|
||||||
func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
||||||
serveMux := http.NewServeMux()
|
serveMux := http.NewServeMux()
|
||||||
|
var auth hmacauth.HmacAuth
|
||||||
|
if sigData := opts.signatureData; sigData != nil {
|
||||||
|
auth = hmacauth.NewHmacAuth(sigData.hash, []byte(sigData.key),
|
||||||
|
SignatureHeader, SignatureHeaders)
|
||||||
|
}
|
||||||
for _, u := range opts.proxyURLs {
|
for _, u := range opts.proxyURLs {
|
||||||
path := u.Path
|
path := u.Path
|
||||||
switch u.Scheme {
|
switch u.Scheme {
|
||||||
@ -101,14 +127,15 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
|
|||||||
} else {
|
} else {
|
||||||
setProxyDirector(proxy)
|
setProxyDirector(proxy)
|
||||||
}
|
}
|
||||||
serveMux.Handle(path, &UpstreamProxy{u.Host, proxy})
|
serveMux.Handle(path,
|
||||||
|
&UpstreamProxy{u.Host, proxy, auth})
|
||||||
case "file":
|
case "file":
|
||||||
if u.Fragment != "" {
|
if u.Fragment != "" {
|
||||||
path = u.Fragment
|
path = u.Fragment
|
||||||
}
|
}
|
||||||
log.Printf("mapping path %q => file system %q", path, u.Path)
|
log.Printf("mapping path %q => file system %q", path, u.Path)
|
||||||
proxy := NewFileServer(path, u.Path)
|
proxy := NewFileServer(path, u.Path)
|
||||||
serveMux.Handle(path, &UpstreamProxy{path, proxy})
|
serveMux.Handle(path, &UpstreamProxy{path, proxy, nil})
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
|
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"github.com/18F/hmacauth"
|
||||||
"github.com/bitly/oauth2_proxy/providers"
|
"github.com/bitly/oauth2_proxy/providers"
|
||||||
"github.com/bmizerany/assert"
|
"github.com/bmizerany/assert"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@ -88,6 +91,45 @@ func TestRobotsTxt(t *testing.T) {
|
|||||||
assert.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String())
|
assert.Equal(t, "User-agent: *\nDisallow: /", rw.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TestProvider struct {
|
||||||
|
*providers.ProviderData
|
||||||
|
EmailAddress string
|
||||||
|
ValidToken bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestProvider(provider_url *url.URL, email_address string) *TestProvider {
|
||||||
|
return &TestProvider{
|
||||||
|
ProviderData: &providers.ProviderData{
|
||||||
|
ProviderName: "Test Provider",
|
||||||
|
LoginURL: &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: provider_url.Host,
|
||||||
|
Path: "/oauth/authorize",
|
||||||
|
},
|
||||||
|
RedeemURL: &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: provider_url.Host,
|
||||||
|
Path: "/oauth/token",
|
||||||
|
},
|
||||||
|
ProfileURL: &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: provider_url.Host,
|
||||||
|
Path: "/api/v1/profile",
|
||||||
|
},
|
||||||
|
Scope: "profile.email",
|
||||||
|
},
|
||||||
|
EmailAddress: email_address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) {
|
||||||
|
return tp.EmailAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool {
|
||||||
|
return tp.ValidToken
|
||||||
|
}
|
||||||
|
|
||||||
func TestBasicAuthPassword(t *testing.T) {
|
func TestBasicAuthPassword(t *testing.T) {
|
||||||
provider_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
provider_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("%#v", r)
|
log.Printf("%#v", r)
|
||||||
@ -121,29 +163,7 @@ func TestBasicAuthPassword(t *testing.T) {
|
|||||||
const email_address = "michael.bland@gsa.gov"
|
const email_address = "michael.bland@gsa.gov"
|
||||||
const user_name = "michael.bland"
|
const user_name = "michael.bland"
|
||||||
|
|
||||||
opts.provider = &TestProvider{
|
opts.provider = NewTestProvider(provider_url, email_address)
|
||||||
ProviderData: &providers.ProviderData{
|
|
||||||
ProviderName: "Test Provider",
|
|
||||||
LoginURL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: provider_url.Host,
|
|
||||||
Path: "/oauth/authorize",
|
|
||||||
},
|
|
||||||
RedeemURL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: provider_url.Host,
|
|
||||||
Path: "/oauth/token",
|
|
||||||
},
|
|
||||||
ProfileURL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: provider_url.Host,
|
|
||||||
Path: "/api/v1/profile",
|
|
||||||
},
|
|
||||||
Scope: "profile.email",
|
|
||||||
},
|
|
||||||
EmailAddress: email_address,
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := NewOAuthProxy(opts, func(email string) bool {
|
proxy := NewOAuthProxy(opts, func(email string) bool {
|
||||||
return email == email_address
|
return email == email_address
|
||||||
})
|
})
|
||||||
@ -183,20 +203,6 @@ func TestBasicAuthPassword(t *testing.T) {
|
|||||||
provider_server.Close()
|
provider_server.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestProvider struct {
|
|
||||||
*providers.ProviderData
|
|
||||||
EmailAddress string
|
|
||||||
ValidToken bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TestProvider) GetEmailAddress(session *providers.SessionState) (string, error) {
|
|
||||||
return tp.EmailAddress, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TestProvider) ValidateSessionState(session *providers.SessionState) bool {
|
|
||||||
return tp.ValidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type PassAccessTokenTest struct {
|
type PassAccessTokenTest struct {
|
||||||
provider_server *httptest.Server
|
provider_server *httptest.Server
|
||||||
proxy *OAuthProxy
|
proxy *OAuthProxy
|
||||||
@ -242,29 +248,7 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) *PassAccessTokenTes
|
|||||||
provider_url, _ := url.Parse(t.provider_server.URL)
|
provider_url, _ := url.Parse(t.provider_server.URL)
|
||||||
const email_address = "michael.bland@gsa.gov"
|
const email_address = "michael.bland@gsa.gov"
|
||||||
|
|
||||||
t.opts.provider = &TestProvider{
|
t.opts.provider = NewTestProvider(provider_url, email_address)
|
||||||
ProviderData: &providers.ProviderData{
|
|
||||||
ProviderName: "Test Provider",
|
|
||||||
LoginURL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: provider_url.Host,
|
|
||||||
Path: "/oauth/authorize",
|
|
||||||
},
|
|
||||||
RedeemURL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: provider_url.Host,
|
|
||||||
Path: "/oauth/token",
|
|
||||||
},
|
|
||||||
ProfileURL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: provider_url.Host,
|
|
||||||
Path: "/api/v1/profile",
|
|
||||||
},
|
|
||||||
Scope: "profile.email",
|
|
||||||
},
|
|
||||||
EmailAddress: email_address,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.proxy = NewOAuthProxy(t.opts, func(email string) bool {
|
t.proxy = NewOAuthProxy(t.opts, func(email string) bool {
|
||||||
return email == email_address
|
return email == email_address
|
||||||
})
|
})
|
||||||
@ -559,7 +543,7 @@ func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) {
|
|||||||
func NewAuthOnlyEndpointTest() *ProcessCookieTest {
|
func NewAuthOnlyEndpointTest() *ProcessCookieTest {
|
||||||
pc_test := NewProcessCookieTestWithDefaults()
|
pc_test := NewProcessCookieTestWithDefaults()
|
||||||
pc_test.req, _ = http.NewRequest("GET",
|
pc_test.req, _ = http.NewRequest("GET",
|
||||||
pc_test.opts.ProxyPrefix + "/auth", nil)
|
pc_test.opts.ProxyPrefix+"/auth", nil)
|
||||||
return pc_test
|
return pc_test
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,3 +594,143 @@ func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) {
|
|||||||
bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
|
bodyBytes, _ := ioutil.ReadAll(test.rw.Body)
|
||||||
assert.Equal(t, "unauthorized request\n", string(bodyBytes))
|
assert.Equal(t, "unauthorized request\n", string(bodyBytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SignatureAuthenticator struct {
|
||||||
|
auth hmacauth.HmacAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SignatureAuthenticator) Authenticate(
|
||||||
|
w http.ResponseWriter, r *http.Request) {
|
||||||
|
result, headerSig, computedSig := v.auth.AuthenticateRequest(r)
|
||||||
|
if result == hmacauth.ResultNoSignature {
|
||||||
|
w.Write([]byte("no signature received"))
|
||||||
|
} else if result == hmacauth.ResultMatch {
|
||||||
|
w.Write([]byte("signatures match"))
|
||||||
|
} else if result == hmacauth.ResultMismatch {
|
||||||
|
w.Write([]byte("signatures do not match:" +
|
||||||
|
"\n received: " + headerSig +
|
||||||
|
"\n computed: " + computedSig))
|
||||||
|
} else {
|
||||||
|
panic("Unknown result value: " + result.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignatureTest struct {
|
||||||
|
opts *Options
|
||||||
|
upstream *httptest.Server
|
||||||
|
upstream_host string
|
||||||
|
provider *httptest.Server
|
||||||
|
header http.Header
|
||||||
|
rw *httptest.ResponseRecorder
|
||||||
|
authenticator *SignatureAuthenticator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignatureTest() *SignatureTest {
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.CookieSecret = "cookie secret"
|
||||||
|
opts.ClientID = "client ID"
|
||||||
|
opts.ClientSecret = "client secret"
|
||||||
|
opts.EmailDomains = []string{"acm.org"}
|
||||||
|
|
||||||
|
authenticator := &SignatureAuthenticator{}
|
||||||
|
upstream := httptest.NewServer(
|
||||||
|
http.HandlerFunc(authenticator.Authenticate))
|
||||||
|
upstream_url, _ := url.Parse(upstream.URL)
|
||||||
|
opts.Upstreams = append(opts.Upstreams, upstream.URL)
|
||||||
|
|
||||||
|
providerHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(`{"access_token": "my_auth_token"}`))
|
||||||
|
}
|
||||||
|
provider := httptest.NewServer(http.HandlerFunc(providerHandler))
|
||||||
|
provider_url, _ := url.Parse(provider.URL)
|
||||||
|
opts.provider = NewTestProvider(provider_url, "mbland@acm.org")
|
||||||
|
|
||||||
|
return &SignatureTest{
|
||||||
|
opts,
|
||||||
|
upstream,
|
||||||
|
upstream_url.Host,
|
||||||
|
provider,
|
||||||
|
make(http.Header),
|
||||||
|
httptest.NewRecorder(),
|
||||||
|
authenticator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *SignatureTest) Close() {
|
||||||
|
st.provider.Close()
|
||||||
|
st.upstream.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fakeNetConn simulates an http.Request.Body buffer that will be consumed
|
||||||
|
// when it is read by the hmacauth.HmacAuth if not handled properly. See:
|
||||||
|
// https://github.com/18F/hmacauth/pull/4
|
||||||
|
type fakeNetConn struct {
|
||||||
|
reqBody string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fnc *fakeNetConn) Read(p []byte) (n int, err error) {
|
||||||
|
if bodyLen := len(fnc.reqBody); bodyLen != 0 {
|
||||||
|
copy(p, fnc.reqBody)
|
||||||
|
fnc.reqBody = ""
|
||||||
|
return bodyLen, io.EOF
|
||||||
|
}
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) {
|
||||||
|
err := st.opts.Validate()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
proxy := NewOAuthProxy(st.opts, func(email string) bool { return true })
|
||||||
|
|
||||||
|
var bodyBuf io.ReadCloser
|
||||||
|
if body != "" {
|
||||||
|
bodyBuf = ioutil.NopCloser(&fakeNetConn{reqBody: body})
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, "/foo/bar", bodyBuf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
req.Header = st.header
|
||||||
|
|
||||||
|
state := &providers.SessionState{
|
||||||
|
Email: "mbland@acm.org", AccessToken: "my_access_token"}
|
||||||
|
value, err := proxy.provider.CookieForSession(state, proxy.CookieCipher)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cookie := proxy.MakeCookie(req, value, proxy.CookieExpire, time.Now())
|
||||||
|
req.AddCookie(cookie)
|
||||||
|
// This is used by the upstream to validate the signature.
|
||||||
|
st.authenticator.auth = hmacauth.NewHmacAuth(
|
||||||
|
crypto.SHA1, []byte(key), SignatureHeader, SignatureHeaders)
|
||||||
|
proxy.ServeHTTP(st.rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoRequestSignature(t *testing.T) {
|
||||||
|
st := NewSignatureTest()
|
||||||
|
defer st.Close()
|
||||||
|
st.MakeRequestWithExpectedKey("GET", "", "")
|
||||||
|
assert.Equal(t, 200, st.rw.Code)
|
||||||
|
assert.Equal(t, st.rw.Body.String(), "no signature received")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestSignatureGetRequest(t *testing.T) {
|
||||||
|
st := NewSignatureTest()
|
||||||
|
defer st.Close()
|
||||||
|
st.opts.SignatureKey = "sha1:foobar"
|
||||||
|
st.MakeRequestWithExpectedKey("GET", "", "foobar")
|
||||||
|
assert.Equal(t, 200, st.rw.Code)
|
||||||
|
assert.Equal(t, st.rw.Body.String(), "signatures match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestSignaturePostRequest(t *testing.T) {
|
||||||
|
st := NewSignatureTest()
|
||||||
|
defer st.Close()
|
||||||
|
st.opts.SignatureKey = "sha1:foobar"
|
||||||
|
payload := `{ "hello": "world!" }`
|
||||||
|
st.MakeRequestWithExpectedKey("POST", payload, "foobar")
|
||||||
|
assert.Equal(t, 200, st.rw.Code)
|
||||||
|
assert.Equal(t, st.rw.Body.String(), "signatures match")
|
||||||
|
}
|
||||||
|
33
options.go
33
options.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -8,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/18F/hmacauth"
|
||||||
"github.com/bitly/oauth2_proxy/providers"
|
"github.com/bitly/oauth2_proxy/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,11 +62,19 @@ type Options struct {
|
|||||||
|
|
||||||
RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
|
RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
|
||||||
|
|
||||||
|
SignatureKey string `flag:"signature-key" cfg:"signature_key"`
|
||||||
|
|
||||||
// internal values that are set after config validation
|
// internal values that are set after config validation
|
||||||
redirectURL *url.URL
|
redirectURL *url.URL
|
||||||
proxyURLs []*url.URL
|
proxyURLs []*url.URL
|
||||||
CompiledRegex []*regexp.Regexp
|
CompiledRegex []*regexp.Regexp
|
||||||
provider providers.Provider
|
provider providers.Provider
|
||||||
|
signatureData *SignatureData
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignatureData struct {
|
||||||
|
hash crypto.Hash
|
||||||
|
key string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOptions() *Options {
|
func NewOptions() *Options {
|
||||||
@ -175,6 +185,8 @@ func (o *Options) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msgs = parseSignatureKey(o, msgs)
|
||||||
|
|
||||||
if len(msgs) != 0 {
|
if len(msgs) != 0 {
|
||||||
return fmt.Errorf("Invalid configuration:\n %s",
|
return fmt.Errorf("Invalid configuration:\n %s",
|
||||||
strings.Join(msgs, "\n "))
|
strings.Join(msgs, "\n "))
|
||||||
@ -210,3 +222,24 @@ func parseProviderInfo(o *Options, msgs []string) []string {
|
|||||||
}
|
}
|
||||||
return msgs
|
return msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSignatureKey(o *Options, msgs []string) []string {
|
||||||
|
if o.SignatureKey == "" {
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
components := strings.Split(o.SignatureKey, ":")
|
||||||
|
if len(components) != 2 {
|
||||||
|
return append(msgs, "invalid signature hash:key spec: "+
|
||||||
|
o.SignatureKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm, secretKey := components[0], components[1]
|
||||||
|
if hash, err := hmacauth.DigestNameToCryptoHash(algorithm); err != nil {
|
||||||
|
return append(msgs, "unsupported signature hash algorithm: "+
|
||||||
|
o.SignatureKey)
|
||||||
|
} else {
|
||||||
|
o.signatureData = &SignatureData{hash, secretKey}
|
||||||
|
}
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -166,3 +167,27 @@ func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) {
|
|||||||
o.CookieRefresh -= time.Duration(1)
|
o.CookieRefresh -= time.Duration(1)
|
||||||
assert.Equal(t, nil, o.Validate())
|
assert.Equal(t, nil, o.Validate())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateSignatureKey(t *testing.T) {
|
||||||
|
o := testOptions()
|
||||||
|
o.SignatureKey = "sha1:secret"
|
||||||
|
assert.Equal(t, nil, o.Validate())
|
||||||
|
assert.Equal(t, o.signatureData.hash, crypto.SHA1)
|
||||||
|
assert.Equal(t, o.signatureData.key, "secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSignatureKeyInvalidSpec(t *testing.T) {
|
||||||
|
o := testOptions()
|
||||||
|
o.SignatureKey = "invalid spec"
|
||||||
|
err := o.Validate()
|
||||||
|
assert.Equal(t, err.Error(), "Invalid configuration:\n"+
|
||||||
|
" invalid signature hash:key spec: "+o.SignatureKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSignatureKeyUnsupportedAlgorithm(t *testing.T) {
|
||||||
|
o := testOptions()
|
||||||
|
o.SignatureKey = "unsupported:default secret"
|
||||||
|
err := o.Validate()
|
||||||
|
assert.Equal(t, err.Error(), "Invalid configuration:\n"+
|
||||||
|
" unsupported signature hash algorithm: "+o.SignatureKey)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user