Merge pull request #79 from 18F/add-myusa-provider

Add myusa provider
This commit is contained in:
Jehiah Czebotar 2015-03-31 15:59:11 -04:00
commit 66d4d72d2e
4 changed files with 219 additions and 5 deletions

View File

@ -2,8 +2,8 @@ google_auth_proxy
================= =================
A reverse proxy that provides authentication using Google OAuth2 to validate A reverse proxy that provides authentication using Google and other OAuth2
individual accounts, or a whole google apps domain. providers to validate individual accounts, or a whole google apps domain.
[![Build Status](https://secure.travis-ci.org/bitly/google_auth_proxy.png?branch=master)](http://travis-ci.org/bitly/google_auth_proxy) [![Build Status](https://secure.travis-ci.org/bitly/google_auth_proxy.png?branch=master)](http://travis-ci.org/bitly/google_auth_proxy)
@ -31,8 +31,10 @@ individual accounts, or a whole google apps domain.
## OAuth Configuration ## OAuth Configuration
You will need to register an OAuth application with google, and configure it with Redirect URI(s) for the domain you You will need to register an OAuth application with Google (or [another
intend to run `google_auth_proxy` on. provider](#providers)), and configure it with Redirect URI(s) for the domain
you intend to run `google_auth_proxy` on. For Google, the registration steps
are:
1. Create a new project: https://console.developers.google.com/project 1. Create a new project: https://console.developers.google.com/project
2. Under "APIs & Auth", choose "Credentials" 2. Under "APIs & Auth", choose "Credentials"
@ -73,9 +75,15 @@ Usage of google_auth_proxy:
-google-apps-domain=: authenticate against the given Google apps domain (may be given multiple times) -google-apps-domain=: authenticate against the given Google apps domain (may be given multiple times)
-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
-login-url="": Authentication endpoint
-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
-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
-provider="": Oauth provider (defaults to Google)
-redeem-url="": Token redemption endpoint
-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
-scope="": Oauth scope specification
-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)
-upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path -upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path
-version=false: print version string -version=false: print version string
@ -142,4 +150,19 @@ Google Auth Proxy logs requests to stdout in a format similar to Apache Combined
``` ```
<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>
```` ```
## <a name="providers"></a>Providers other than Google
Other providers besides Google can be specified by the `providers` flag/config
directive. Right now this includes:
* `myusa` - The [MyUSA](https://alpha.my.usa.gov) authentication service
([GitHub](https://github.com/18F/myusa))
## Adding a new Provider
Follow the examples in the [`providers` package](providers/) to define a new
`Provider` instance. Add a new `case` to
[`providers.New()`](providers/providers.go) to allow the auth proxy to use the
new `Provider`.

55
providers/myusa.go Normal file
View File

@ -0,0 +1,55 @@
package providers
import (
"log"
"net/http"
"net/url"
"github.com/bitly/go-simplejson"
"github.com/bitly/google_auth_proxy/api"
)
type MyUsaProvider struct {
*ProviderData
}
func NewMyUsaProvider(p *ProviderData) *MyUsaProvider {
const myUsaHost string = "alpha.my.usa.gov"
p.ProviderName = "MyUSA"
if p.LoginUrl.String() == "" {
p.LoginUrl = &url.URL{Scheme: "https",
Host: myUsaHost,
Path: "/oauth/authorize"}
}
if p.RedeemUrl.String() == "" {
p.RedeemUrl = &url.URL{Scheme: "https",
Host: myUsaHost,
Path: "/oauth/token"}
}
if p.ProfileUrl.String() == "" {
p.ProfileUrl = &url.URL{Scheme: "https",
Host: myUsaHost,
Path: "/api/v1/profile"}
}
if p.Scope == "" {
p.Scope = "profile.email"
}
return &MyUsaProvider{ProviderData: p}
}
func (p *MyUsaProvider) GetEmailAddress(auth_response *simplejson.Json,
access_token string) (string, error) {
req, err := http.NewRequest("GET",
p.ProfileUrl.String()+"?access_token="+access_token, nil)
if err != nil {
log.Printf("failed building request %s", err)
return "", err
}
json, err := api.Request(req)
if err != nil {
log.Printf("failed making request %s", err)
return "", err
}
return json.Get("email").String()
}

134
providers/myusa_test.go Normal file
View File

@ -0,0 +1,134 @@
package providers
import (
"github.com/bitly/go-simplejson"
"github.com/bmizerany/assert"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func updateUrl(url *url.URL, hostname string) {
url.Scheme = "http"
url.Host = hostname
}
func testMyUsaProvider(hostname string) *MyUsaProvider {
p := NewMyUsaProvider(
&ProviderData{
ProviderName: "",
LoginUrl: &url.URL{},
RedeemUrl: &url.URL{},
ProfileUrl: &url.URL{},
Scope: ""})
if hostname != "" {
updateUrl(p.Data().LoginUrl, hostname)
updateUrl(p.Data().RedeemUrl, hostname)
updateUrl(p.Data().ProfileUrl, hostname)
}
return p
}
func testMyUsaBackend(payload string) *httptest.Server {
path := "/api/v1/profile"
query := "access_token=imaginary_access_token"
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
url := r.URL
if url.Path != path || url.RawQuery != query {
w.WriteHeader(404)
} else {
w.WriteHeader(200)
w.Write([]byte(payload))
}
}))
}
func TestMyUsaProviderDefaults(t *testing.T) {
p := testMyUsaProvider("")
assert.NotEqual(t, nil, p)
assert.Equal(t, "MyUSA", p.Data().ProviderName)
assert.Equal(t, "https://alpha.my.usa.gov/oauth/authorize",
p.Data().LoginUrl.String())
assert.Equal(t, "https://alpha.my.usa.gov/oauth/token",
p.Data().RedeemUrl.String())
assert.Equal(t, "https://alpha.my.usa.gov/api/v1/profile",
p.Data().ProfileUrl.String())
assert.Equal(t, "profile.email", p.Data().Scope)
}
func TestMyUsaProviderOverrides(t *testing.T) {
p := NewMyUsaProvider(
&ProviderData{
LoginUrl: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
RedeemUrl: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ProfileUrl: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/profile"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "MyUSA", p.Data().ProviderName)
assert.Equal(t, "https://example.com/oauth/auth",
p.Data().LoginUrl.String())
assert.Equal(t, "https://example.com/oauth/token",
p.Data().RedeemUrl.String())
assert.Equal(t, "https://example.com/oauth/profile",
p.Data().ProfileUrl.String())
assert.Equal(t, "profile", p.Data().Scope)
}
func TestMyUsaProviderGetEmailAddress(t *testing.T) {
b := testMyUsaBackend("{\"email\": \"michael.bland@gsa.gov\"}")
defer b.Close()
b_url, _ := url.Parse(b.URL)
p := testMyUsaProvider(b_url.Host)
unused_auth_response := simplejson.New()
email, err := p.GetEmailAddress(unused_auth_response,
"imaginary_access_token")
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}
// Note that trying to trigger the "failed building request" case is not
// practical, since the only way it can fail is if the URL fails to parse.
func TestMyUsaProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testMyUsaBackend("unused payload")
defer b.Close()
b_url, _ := url.Parse(b.URL)
p := testMyUsaProvider(b_url.Host)
unused_auth_response := simplejson.New()
// We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as
// JSON to fail.
email, err := p.GetEmailAddress(unused_auth_response,
"unexpected_access_token")
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}
func TestMyUsaProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testMyUsaBackend("{\"foo\": \"bar\"}")
defer b.Close()
b_url, _ := url.Parse(b.URL)
p := testMyUsaProvider(b_url.Host)
unused_auth_response := simplejson.New()
email, err := p.GetEmailAddress(unused_auth_response,
"imaginary_access_token")
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}

View File

@ -12,6 +12,8 @@ type Provider interface {
func New(provider string, p *ProviderData) Provider { func New(provider string, p *ProviderData) Provider {
switch provider { switch provider {
case "myusa":
return NewMyUsaProvider(p)
default: default:
return NewGoogleProvider(p) return NewGoogleProvider(p)
} }