Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Add myusa provider #79

Merged
merged 2 commits into from
Mar 31, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ google_auth_proxy
=================


A reverse proxy that provides authentication using Google OAuth2 to validate
individual accounts, or a whole google apps domain.
A reverse proxy that provides authentication using Google and other OAuth2
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)

Expand Down Expand Up @@ -31,8 +31,10 @@ individual accounts, or a whole google apps domain.

## OAuth Configuration

You will need to register an OAuth application with google, and configure it with Redirect URI(s) for the domain you
intend to run `google_auth_proxy` on.
You will need to register an OAuth application with Google (or [another
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
2. Under "APIs & Auth", choose "Credentials"
Expand Down Expand Up @@ -73,9 +75,15 @@ Usage of google_auth_proxy:
-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
-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-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"
-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)
-upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path
-version=false: print version string
Expand Down Expand Up @@ -142,4 +150,19 @@ Google Auth Proxy logs requests to stdout in a format similar to Apache Combined

```
<REMOTE_ADDRESS> - <[email protected]> [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 changes: 55 additions & 0 deletions providers/myusa.go
Original file line number Diff line number Diff line change
@@ -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 changes: 134 additions & 0 deletions providers/myusa_test.go
Original file line number Diff line number Diff line change
@@ -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\": \"[email protected]\"}")
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, "[email protected]", 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)
}
2 changes: 2 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type Provider interface {

func New(provider string, p *ProviderData) Provider {
switch provider {
case "myusa":
return NewMyUsaProvider(p)
default:
return NewGoogleProvider(p)
}
Expand Down