Merge branch 'master' of https://github.com/bitly/oauth2_proxy into work

This commit is contained in:
Pavel Sorokin 2017-12-11 07:37:05 +00:00
commit 0a334b7cf4
39 changed files with 676 additions and 282 deletions

11
.gitignore vendored
View File

@ -1,5 +1,10 @@
google_auth_proxy
oauth2_proxy
vendor
dist
.godeps
*.exe
# Go.gitignore
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
@ -23,9 +28,5 @@ _cgo_export.*
_testmain.go
*.exe
dist
.godeps
# Editor swap/temp files
.*.swp

View File

@ -1,11 +1,11 @@
language: go
go:
- 1.7.5
- 1.8.1
- 1.8.x
- 1.9.x
script:
- curl -s https://raw.githubusercontent.com/pote/gpm/v1.4.0/bin/gpm > gpm
- chmod +x gpm
- ./gpm install
- wget -O dep https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64
- chmod +x dep
- ./dep ensure
- ./test.sh
sudo: false
notifications:

10
Godeps
View File

@ -1,10 +0,0 @@
github.com/18F/hmacauth 1.0.1
github.com/BurntSushi/toml d94612f9fc140360834f9742158c70b5c5b5535b
github.com/bitly/go-simplejson da1a8928f709389522c8023062a3739f3b4af419
github.com/mreiferson/go-options 77551d20752b54535462404ad9d877ebdb26e53d
github.com/bmizerany/assert e17e99893cb6509f428e1728281c2ad60a6b31e3
gopkg.in/fsnotify.v1 v1.2.0
golang.org/x/oauth2 7fdf09982454086d5570c7db3e11f360194830ca
golang.org/x/net/context 242b6b35177ec3909636b6cf6a47e8c2c6324b5d
google.golang.org/api/admin/directory/v1 650535c7d6201e8304c92f38c922a9a3a36c6877
cloud.google.com/go/compute/metadata v0.7.0

117
Gopkg.lock generated Normal file
View File

@ -0,0 +1,117 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "cloud.google.com/go"
packages = ["compute/metadata"]
revision = "2d3a6656c17a60b0815b7e06ab0be04eacb6e613"
version = "v0.16.0"
[[projects]]
name = "github.com/BurntSushi/toml"
packages = ["."]
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
[[projects]]
name = "github.com/bitly/go-simplejson"
packages = ["."]
revision = "aabad6e819789e569bd6aabf444c935aa9ba1e44"
version = "v0.5.0"
[[projects]]
branch = "v2"
name = "github.com/coreos/go-oidc"
packages = ["."]
revision = "77e7f2010a464ade7338597afe650dfcffbe2ca8"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
[[projects]]
name = "github.com/mbland/hmacauth"
packages = ["."]
revision = "107c17adcc5eccc9935cd67d9bc2feaf5255d2cb"
version = "1.0.2"
[[projects]]
branch = "master"
name = "github.com/mreiferson/go-options"
packages = ["."]
revision = "77551d20752b54535462404ad9d877ebdb26e53d"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/pquerna/cachecontrol"
packages = [".","cacheobject"]
revision = "0dec1b30a0215bb68605dfc568e8855066c9202d"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ed25519","ed25519/internal/edwards25519"]
revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","context/ctxhttp"]
revision = "9dfe39835686865bff950a07b394c12a98ddc811"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [".","google","internal","jws","jwt"]
revision = "9ff8ebcc8e241d46f52ecc5bff0e5a2f2dbef402"
[[projects]]
branch = "master"
name = "google.golang.org/api"
packages = ["admin/directory/v1","gensupport","googleapi","googleapi/internal/uritemplates"]
revision = "8791354e7ab150705ede13637a18c1fcc16b62e8"
[[projects]]
name = "google.golang.org/appengine"
packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
name = "gopkg.in/fsnotify.v1"
packages = ["."]
revision = "836bfd95fecc0f1511dd66bdbf2b5b61ab8b00b6"
version = "v1.2.11"
[[projects]]
name = "gopkg.in/square/go-jose.v2"
packages = [".","cipher","json"]
revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1"
version = "v2.1.3"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "efab48a0e196c2a849bfbe9aa02d2ae28d281ce1bfe9f23720d648858eefc8e6"
solver-name = "gps-cdcl"
solver-version = 1

40
Gopkg.toml Normal file
View File

@ -0,0 +1,40 @@
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
[[constraint]]
name = "github.com/18F/hmacauth"
version = "~1.0.1"
[[constraint]]
name = "github.com/BurntSushi/toml"
version = "~0.3.0"
[[constraint]]
name = "github.com/bitly/go-simplejson"
version = "~0.5.0"
[[constraint]]
branch = "v2"
name = "github.com/coreos/go-oidc"
[[constraint]]
branch = "master"
name = "github.com/mreiferson/go-options"
[[constraint]]
name = "github.com/stretchr/testify"
version = "~1.1.4"
[[constraint]]
branch = "master"
name = "golang.org/x/oauth2"
[[constraint]]
branch = "master"
name = "google.golang.org/api"
[[constraint]]
name = "gopkg.in/fsnotify.v1"
version = "~1.2.0"

View File

@ -32,7 +32,6 @@ Valid providers are :
* [GitHub](#github-auth-provider)
* [GitLab](#gitlab-auth-provider)
* [LinkedIn](#linkedin-auth-provider)
* [MyUSA](#myusa-auth-provider)
The provider can be selected using the `provider` configuration value.
@ -99,7 +98,7 @@ The Azure AD auth provider uses `openid` as it default scope. It uses `https://g
The GitHub auth provider supports two additional parameters to restrict authentication to Organization or Team level access. Restricting by org and team is normally accompanied with `--email-domain=*`
-github-org="": restrict logins to members of this organisation
-github-team="": restrict logins to members of any of these teams, separated by a comma
-github-team="": restrict logins to members of any of these teams (slug), separated by a comma
If you are using GitHub enterprise, make sure you set the following to the appropriate url:
@ -115,7 +114,7 @@ If you are using self-hosted GitLab, make sure you set the following to the appr
-login-url="<your gitlab url>/oauth/authorize"
-redeem-url="<your gitlab url>/oauth/token"
-validate-url="<your gitlab url>/api/v3/user"
-validate-url="<your gitlab url>/api/v4/user"
### LinkedIn Auth Provider
@ -129,10 +128,6 @@ For LinkedIn, the registration steps are:
3. Fill in the remaining required fields and Save.
4. Take note of the **Consumer Key / API Key** and **Consumer Secret / Secret Key**
### MyUSA Auth Provider
The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa))
### Microsoft Azure AD Provider
1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant.
@ -153,6 +148,24 @@ The `filter-groups` flag enables a simple filter that will elide any groups that
The `permit-groups` flag requires that a user belong to a group that contains the specified string (or one of the specified strings). The X-Forwarded-Group header is checked for a `strings.Contains` match for each item in the list.
The 'group-delimiter' flag reflects how multiple group names will be represented in X-Forwarded-Groups header. By default group names will be delimited with '|' symbol
### OpenID Connect Provider
OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects. This provider was originally built against CoreOS Dex and we will use it as an example.
1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md).
2. Setup oauth2_proxy with the correct provider and using the default ports and callbacks.
3. Login with the fixture use in the dex guide and run the oauth2_proxy with the following args:
-provider oidc
-client-id oauth2_proxy
-client-secret proxy
-redirect-url http://127.0.0.1:4180/oauth2/callback
-oidc-issuer-url http://127.0.0.1:5556
-cookie-secure=false
-email-domain example.com
## Email Authentication
To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`.
@ -178,7 +191,7 @@ Usage of oauth2_proxy:
-client-id string: the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
-client-secret string: the OAuth Client Secret
-config string: path to config file
-cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com)*
-cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com)
-cookie-expire duration: expire timeframe for cookie (default 168h0m0s)
-cookie-httponly: set HttpOnly cookie flag (default true)
-cookie-name string: the name of the cookie that the oauth_proxy creates (default "_oauth2_proxy")
@ -189,9 +202,10 @@ Usage of oauth2_proxy:
-display-htpasswd-form: display username / password login form if an htpasswd file is provided (default true)
-email-domain value: authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email
-filter-groups string: only pass groups in the X-Forwarded-Groups header that contain this string
-groups-delimiter string: delimiter that will be used between group names in X-Forwarded-Groups header
-footer string: custom footer string. Use "-" to disable default footer.
-github-org string: restrict logins to members of this organisation
-github-team string: restrict logins to members of this team
-github-team string: restrict logins to members of any of these teams (slug), separated by a comma
-google-admin-email string: the google admin to impersonate for api calls
-google-group value: restrict logins to members of this google group (may be given multiple times).
-google-service-account-json string: the path to the service account json credentials

View File

@ -2,12 +2,13 @@ package api
import (
"github.com/bitly/go-simplejson"
"github.com/bmizerany/assert"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func testBackend(response_code int, payload string) *httptest.Server {

View File

@ -4,7 +4,7 @@ import (
"encoding/base64"
"testing"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
func TestEncodeAndDecodeAccessToken(t *testing.T) {

View File

@ -4,7 +4,7 @@ import (
"os"
"testing"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
type envTest struct {

View File

@ -2,7 +2,7 @@ package main
import (
"bytes"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
"testing"
)

View File

@ -37,6 +37,7 @@ func main() {
flagSet.Bool("pass-groups", false, "pass user group information in the X-Forwarded-Groups header to upstream (Azure only)")
flagSet.String("filter-groups", "", "exclude groups that do not contain this value in its 'displayName' (Azure only)")
flagSet.Var(&permittedGroups, "permit-groups", "restrict logins to members of this group (may be given multiple times; Azure and Google only).")
flagSet.String("groups-delimiter", "|", "delimiter between group names if more than one found. By default it is '|' symbol")
flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header")
flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header")
flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream")
@ -71,6 +72,7 @@ func main() {
flagSet.Bool("request-logging", true, "Log requests to stdout")
flagSet.String("provider", "google", "OAuth provider")
flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
flagSet.String("login-url", "", "Authentication endpoint")
flagSet.String("redeem-url", "", "Token redemption endpoint")
flagSet.String("profile-url", "", "Profile access endpoint")

View File

@ -14,9 +14,9 @@ import (
"strings"
"time"
"github.com/18F/hmacauth"
"github.com/bitly/oauth2_proxy/cookie"
"github.com/bitly/oauth2_proxy/providers"
"github.com/mbland/hmacauth"
)
const SignatureHeader = "GAP-Signature"
@ -64,6 +64,7 @@ type OAuthProxy struct {
SetXAuthRequest bool
PassBasicAuth bool
PassGroups bool
GroupsDelimiter string
FilterGroups string
SkipProviderButton bool
PassUserHeaders bool
@ -161,16 +162,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
log.Printf("OAuthProxy configured for %s Client ID: %s", opts.provider.Data().ProviderName, opts.ClientID)
domain := opts.CookieDomain
if domain == "" {
domain = "<default>"
}
refresh := "disabled"
if opts.CookieRefresh != time.Duration(0) {
refresh = fmt.Sprintf("after %s", opts.CookieRefresh)
}
log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain, refresh)
log.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domain:%s refresh:%s", opts.CookieName, opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, opts.CookieDomain, refresh)
var cipher *cookie.Cipher
if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
@ -211,6 +208,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
PassBasicAuth: opts.PassBasicAuth,
PassUserHeaders: opts.PassUserHeaders,
PassGroups: opts.PassGroups,
GroupsDelimiter: opts.GroupsDelimiter,
FilterGroups: opts.FilterGroups,
BasicAuthPassword: opts.BasicAuthPassword,
PassAccessToken: opts.PassAccessToken,
@ -257,6 +255,13 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, e
if s.Email == "" {
s.Email, err = p.provider.GetEmailAddress(s)
}
if s.User == "" {
s.User, err = p.provider.GetUserName(s)
if err != nil && err.Error() == "not implemented" {
err = nil
}
}
return
}
@ -276,22 +281,21 @@ func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration
}
func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
if p.CookieDomain != "" {
domain := req.Host
if h, _, err := net.SplitHostPort(domain); err == nil {
domain = h
}
if p.CookieDomain != "" {
if !strings.HasSuffix(domain, p.CookieDomain) {
log.Printf("Warning: request host is %q but using configured cookie domain of %q", domain, p.CookieDomain)
}
domain = p.CookieDomain
}
return &http.Cookie{
Name: name,
Value: value,
Path: "/",
Domain: domain,
Domain: p.CookieDomain,
HttpOnly: p.CookieHttpOnly,
Secure: p.CookieSecure,
Expires: now.Add(expiration),
@ -537,7 +541,6 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
errorString := req.Form.Get("error")
if errorString != "" {
log.Printf("[OAuthCallback] error in parsed form (REQ) : %s", req)
log.Printf("[OAuthCallback] error in parsed form (REQ.Form) : %s", req.Form)
log.Printf("[OAuthCallback] error in parsed form (REQ.error string) : %s", errorString)
p.ErrorPage(rw, 403, "Permission Denied", errorString)
@ -572,11 +575,12 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
session.IDToken = req.Form.Get("id_token")
if p.PassGroups && session.IDToken != "" {
session.Groups, err = p.provider.GetGroups(session, p.FilterGroups)
groups, err := p.provider.GetGroups(session, p.FilterGroups)
if err != nil {
p.ErrorPage(rw, 500, "Internal Error", "Internal Error")
return
}
session.Groups = strings.Join(groups, p.GroupsDelimiter)
}
redirect = req.Form.Get("state")

View File

@ -15,9 +15,9 @@ import (
"testing"
"time"
"github.com/18F/hmacauth"
"github.com/bitly/oauth2_proxy/providers"
"github.com/bmizerany/assert"
"github.com/mbland/hmacauth"
"github.com/stretchr/testify/assert"
)
func init() {

View File

@ -1,6 +1,7 @@
package main
import (
"context"
"crypto"
"crypto/tls"
"encoding/base64"
@ -12,8 +13,9 @@ import (
"strings"
"time"
"github.com/18F/hmacauth"
"github.com/bitly/oauth2_proxy/providers"
oidc "github.com/coreos/go-oidc"
"github.com/mbland/hmacauth"
)
// Configuration Options that can be set by Command Line Flag, or Config File
@ -53,6 +55,7 @@ type Options struct {
PassGroups bool `flag:"pass-groups" cfg:"pass_groups"`
FilterGroups string `flag:"filter-groups" cfg:"filter_groups"`
PermitGroups []string `flag:"permit-groups" cfg:"permit_groups"`
GroupsDelimiter string `flag:"groups-delimiter" cfg:"groups_delimiter"`
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"`
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"`
PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
@ -65,6 +68,7 @@ type Options struct {
// These options allow for other providers besides Google, with
// potential overrides.
Provider string `flag:"provider" cfg:"provider"`
OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
LoginURL string `flag:"login-url" cfg:"login_url"`
RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
ProfileURL string `flag:"profile-url" cfg:"profile_url"`
@ -83,6 +87,7 @@ type Options struct {
CompiledRegex []*regexp.Regexp
provider providers.Provider
signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier
}
type SignatureData struct {
@ -107,6 +112,7 @@ func NewOptions() *Options {
PassUserHeaders: true,
PassGroups: false,
FilterGroups: "",
GroupsDelimiter: "|",
PassAccessToken: false,
PassHostHeader: true,
ApprovalPrompt: "",
@ -125,6 +131,14 @@ func parseURL(to_parse string, urltype string, msgs []string) (*url.URL, []strin
}
func (o *Options) Validate() error {
if o.SSLInsecureSkipVerify {
// TODO: Accept a certificate bundle.
insecureTransport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
http.DefaultClient = &http.Client{Transport: insecureTransport}
}
msgs := make([]string, 0)
if o.CookieSecret == "" {
msgs = append(msgs, "missing setting: cookie-secret")
@ -140,6 +154,22 @@ func (o *Options) Validate() error {
"\n use email-domain=* to authorize all email addresses")
}
if o.OIDCIssuerURL != "" {
// Configure discoverable provider data.
provider, err := oidc.NewProvider(context.Background(), o.OIDCIssuerURL)
if err != nil {
return err
}
o.oidcVerifier = provider.Verifier(&oidc.Config{
ClientID: o.ClientID,
})
o.LoginURL = provider.Endpoint().AuthURL
o.RedeemURL = provider.Endpoint().TokenURL
if o.Scope == "" {
o.Scope = "openid email profile"
}
}
o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
for _, u := range o.Upstreams {
@ -157,8 +187,8 @@ func (o *Options) Validate() error {
for _, u := range o.SkipAuthRegex {
CompiledRegex, err := regexp.Compile(u)
if err != nil {
msgs = append(msgs, fmt.Sprintf(
"error compiling regex=%q %s", u, err))
msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
continue
}
o.CompiledRegex = append(o.CompiledRegex, CompiledRegex)
}
@ -214,13 +244,6 @@ func (o *Options) Validate() error {
msgs = parseSignatureKey(o, msgs)
msgs = validateCookieName(o, msgs)
if o.SSLInsecureSkipVerify {
insecureTransport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
http.DefaultClient = &http.Client{Transport: insecureTransport}
}
if len(msgs) != 0 {
return fmt.Errorf("Invalid configuration:\n %s",
strings.Join(msgs, "\n "))
@ -263,6 +286,12 @@ func parseProviderInfo(o *Options, msgs []string) []string {
p.SetGroupRestriction(o.PermitGroups, o.GoogleAdminEmail, file)
}
}
case *providers.OIDCProvider:
if o.oidcVerifier == nil {
msgs = append(msgs, "oidc provider requires an oidc issuer URL")
} else {
p.Verifier = o.oidcVerifier
}
}
return msgs
}

View File

@ -8,7 +8,7 @@ import (
"testing"
"time"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
func testOptions() *Options {
@ -103,6 +103,18 @@ func TestProxyURLs(t *testing.T) {
assert.Equal(t, expected, o.proxyURLs)
}
func TestProxyURLsError(t *testing.T) {
o := testOptions()
o.Upstreams = append(o.Upstreams, "127.0.0.1:8081")
err := o.Validate()
assert.NotEqual(t, nil, err)
expected := errorMsg([]string{
"error parsing upstream: parse 127.0.0.1:8081: " +
"first path segment in URL cannot contain colon"})
assert.Equal(t, expected, err.Error())
}
func TestCompiledRegex(t *testing.T) {
o := testOptions()
regexps := []string{"/foo/.*", "/ba[rz]/quux"}
@ -127,6 +139,15 @@ func TestCompiledRegexError(t *testing.T) {
"error compiling regex=\"barquux)\" error parsing regexp: " +
"unexpected ): `barquux)`"})
assert.Equal(t, expected, err.Error())
o.SkipAuthRegex = []string{"foobaz", "barquux)"}
err = o.Validate()
assert.NotEqual(t, nil, err)
expected = errorMsg([]string{
"error compiling regex=\"barquux)\" error parsing regexp: " +
"unexpected ): `barquux)`"})
assert.Equal(t, expected, err.Error())
}
func TestDefaultProviderApiSettings(t *testing.T) {

View File

@ -130,13 +130,13 @@ func (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) {
}
// Get list of groups user belong to. Filter the desired names of groups (in case of huge group set)
func (p *AzureProvider) GetGroups(s *SessionState, f string) (string, error) {
func (p *AzureProvider) GetGroups(s *SessionState, f string) ([]string, error) {
if s.AccessToken == "" {
return "", errors.New("missing access token")
return []string{}, errors.New("missing access token")
}
if s.IDToken == "" {
return "", errors.New("missing id token")
return []string{}, errors.New("missing id token")
}
// For future use. Right now microsoft graph don't support filter
@ -164,7 +164,7 @@ func (p *AzureProvider) GetGroups(s *SessionState, f string) (string, error) {
// err = errors.New("fake error")
if err != nil {
return "", err
return []string{}, err
}
req.Header = getAzureHeader(s.AccessToken)
req.Header.Add("Content-Type", "application/json")
@ -174,7 +174,7 @@ func (p *AzureProvider) GetGroups(s *SessionState, f string) (string, error) {
// If workaround already tried, just fail the execution
if workaround_set {
log.Printf("[GetGroups] We tried hard, but still receive error: '%s'", err)
return "", err
return []string{}, err
}
// It might be that it is a Graph bug, try to workaround it by accessing another URL
@ -204,7 +204,7 @@ func (p *AzureProvider) GetGroups(s *SessionState, f string) (string, error) {
}
}
return strings.Join(groups, "|"), nil
return groups, nil
}
func (p *AzureProvider) GetLoginURL(redirectURI, state string) string {

View File

@ -2,7 +2,6 @@ package providers
import (
"fmt"
"github.com/bmizerany/assert"
"io/ioutil"
"log"
"net/http"
@ -10,6 +9,8 @@ import (
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
var (
@ -273,7 +274,8 @@ func TestAzureProviderNoGroups(t *testing.T) {
http.DefaultClient.Transport = nil
assert.Equal(t, nil, err)
assert.Equal(t, "", groups)
// assert.Equal(t, "", groups)
assert.Equal(t, []string{}, groups)
}
func TestAzureProviderWrongRequestGroups(t *testing.T) {
@ -292,7 +294,7 @@ func TestAzureProviderWrongRequestGroups(t *testing.T) {
http.DefaultClient.Transport = nil
assert.NotEqual(t, nil, err)
assert.Equal(t, "", groups)
assert.Equal(t, []string{}, groups)
}
func TestAzureProviderMultiRequestsGroups(t *testing.T) {
@ -311,7 +313,7 @@ func TestAzureProviderMultiRequestsGroups(t *testing.T) {
http.DefaultClient.Transport = nil
assert.Equal(t, nil, err)
assert.Equal(t, "test-group-1|test-group-2|test-group-3", groups)
assert.Equal(t, []string{"test-group-1", "test-group-2", "test-group-3"}, groups)
}
func TestAzureEmptyPermittedGroups(t *testing.T) {

View File

@ -218,10 +218,10 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
if resp.StatusCode != 200 {
return "", fmt.Errorf("got %d from %q %s",
resp.StatusCode, endpoint.String(), body)
} else {
log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
}
log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
if err := json.Unmarshal(body, &emails); err != nil {
return "", fmt.Errorf("%s unmarshaling %s", err, body)
}
@ -234,3 +234,46 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
return "", nil
}
func (p *GitHubProvider) GetUserName(s *SessionState) (string, error) {
var user struct {
Login string `json:"login"`
Email string `json:"email"`
}
endpoint := &url.URL{
Scheme: p.ValidateURL.Scheme,
Host: p.ValidateURL.Host,
Path: path.Join(p.ValidateURL.Path, "/user"),
}
req, err := http.NewRequest("GET", endpoint.String(), nil)
if err != nil {
return "", fmt.Errorf("could not create new GET request: %v", err)
}
req.Header.Set("Authorization", fmt.Sprintf("token %s", s.AccessToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("got %d from %q %s",
resp.StatusCode, endpoint.String(), body)
}
log.Printf("got %d from %q %s", resp.StatusCode, endpoint.String(), body)
if err := json.Unmarshal(body, &user); err != nil {
return "", fmt.Errorf("%s unmarshaling %s", err, body)
}
return user.Login, nil
}

View File

@ -6,16 +6,11 @@ import (
"net/url"
"testing"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
func updateURL(url *url.URL, hostname string) {
url.Scheme = "http"
url.Host = hostname
}
func testMyUsaProvider(hostname string) *MyUsaProvider {
p := NewMyUsaProvider(
func testGitHubProvider(hostname string) *GitHubProvider {
p := NewGitHubProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
@ -32,14 +27,19 @@ func testMyUsaProvider(hostname string) *MyUsaProvider {
return p
}
func testMyUsaBackend(payload string) *httptest.Server {
path := "/api/v1/profile"
query := "access_token=imaginary_access_token"
func testGitHubBackend(payload string) *httptest.Server {
pathToQueryMap := map[string]string{
"/user": "",
"/user/emails": "",
}
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
url := r.URL
if url.Path != path || url.RawQuery != query {
query, ok := pathToQueryMap[url.Path]
if !ok {
w.WriteHeader(404)
} else if url.RawQuery != query {
w.WriteHeader(404)
} else {
w.WriteHeader(200)
@ -48,60 +48,52 @@ func testMyUsaBackend(payload string) *httptest.Server {
}))
}
func TestMyUsaProviderDefaults(t *testing.T) {
p := testMyUsaProvider("")
func TestGitHubProviderDefaults(t *testing.T) {
p := testGitHubProvider("")
assert.NotEqual(t, nil, p)
assert.Equal(t, "MyUSA", p.Data().ProviderName)
assert.Equal(t, "https://alpha.my.usa.gov/oauth/authorize",
assert.Equal(t, "GitHub", p.Data().ProviderName)
assert.Equal(t, "https://github.com/login/oauth/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://alpha.my.usa.gov/oauth/token",
assert.Equal(t, "https://github.com/login/oauth/access_token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://alpha.my.usa.gov/api/v1/profile",
p.Data().ProfileURL.String())
assert.Equal(t, "https://alpha.my.usa.gov/api/v1/tokeninfo",
assert.Equal(t, "https://api.github.com/",
p.Data().ValidateURL.String())
assert.Equal(t, "profile.email", p.Data().Scope)
assert.Equal(t, "user:email", p.Data().Scope)
}
func TestMyUsaProviderOverrides(t *testing.T) {
p := NewMyUsaProvider(
func TestGitHubProviderOverrides(t *testing.T) {
p := NewGitHubProvider(
&ProviderData{
LoginURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
Path: "/login/oauth/authorize"},
RedeemURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ProfileURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/profile"},
Path: "/login/oauth/access_token"},
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/tokeninfo"},
Host: "api.example.com",
Path: "/"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "MyUSA", p.Data().ProviderName)
assert.Equal(t, "https://example.com/oauth/auth",
assert.Equal(t, "GitHub", p.Data().ProviderName)
assert.Equal(t, "https://example.com/login/oauth/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://example.com/oauth/token",
assert.Equal(t, "https://example.com/login/oauth/access_token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://example.com/oauth/profile",
p.Data().ProfileURL.String())
assert.Equal(t, "https://example.com/oauth/tokeninfo",
assert.Equal(t, "https://api.example.com/",
p.Data().ValidateURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}
func TestMyUsaProviderGetEmailAddress(t *testing.T) {
b := testMyUsaBackend("{\"email\": \"michael.bland@gsa.gov\"}")
func TestGitHubProviderGetEmailAddress(t *testing.T) {
b := testGitHubBackend(`[ {"email": "michael.bland@gsa.gov", "primary": true} ]`)
defer b.Close()
b_url, _ := url.Parse(b.URL)
p := testMyUsaProvider(b_url.Host)
bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host)
session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
@ -111,12 +103,12 @@ func TestMyUsaProviderGetEmailAddress(t *testing.T) {
// 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")
func TestGitHubProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testGitHubBackend("unused payload")
defer b.Close()
b_url, _ := url.Parse(b.URL)
p := testMyUsaProvider(b_url.Host)
bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host)
// We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as
@ -127,15 +119,28 @@ func TestMyUsaProviderGetEmailAddressFailedRequest(t *testing.T) {
assert.Equal(t, "", email)
}
func TestMyUsaProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testMyUsaBackend("{\"foo\": \"bar\"}")
func TestGitHubProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testGitHubBackend("{\"foo\": \"bar\"}")
defer b.Close()
b_url, _ := url.Parse(b.URL)
p := testMyUsaProvider(b_url.Host)
bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host)
session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}
func TestGitHubProviderGetUserName(t *testing.T) {
b := testGitHubBackend(`{"email": "michael.bland@gsa.gov", "login": "mbland"}`)
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testGitHubProvider(bURL.Host)
session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetUserName(session)
assert.Equal(t, nil, err)
assert.Equal(t, "mbland", email)
}

View File

@ -32,11 +32,11 @@ func NewGitLabProvider(p *ProviderData) *GitLabProvider {
p.ValidateURL = &url.URL{
Scheme: "https",
Host: "gitlab.com",
Path: "/api/v3/user",
Path: "/api/v4/user",
}
}
if p.Scope == "" {
p.Scope = "api"
p.Scope = "read_user"
}
return &GitLabProvider{ProviderData: p}
}

View File

@ -6,7 +6,7 @@ import (
"net/url"
"testing"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
func testGitLabProvider(hostname string) *GitLabProvider {
@ -28,7 +28,7 @@ func testGitLabProvider(hostname string) *GitLabProvider {
}
func testGitLabBackend(payload string) *httptest.Server {
path := "/api/v3/user"
path := "/api/v4/user"
query := "access_token=imaginary_access_token"
return httptest.NewServer(http.HandlerFunc(
@ -51,9 +51,9 @@ func TestGitLabProviderDefaults(t *testing.T) {
p.Data().LoginURL.String())
assert.Equal(t, "https://gitlab.com/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://gitlab.com/api/v3/user",
assert.Equal(t, "https://gitlab.com/api/v4/user",
p.Data().ValidateURL.String())
assert.Equal(t, "api", p.Data().Scope)
assert.Equal(t, "read_user", p.Data().Scope)
}
func TestGitLabProviderOverrides(t *testing.T) {
@ -70,7 +70,7 @@ func TestGitLabProviderOverrides(t *testing.T) {
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api/v3/user"},
Path: "/api/v4/user"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "GitLab", p.Data().ProviderName)
@ -78,7 +78,7 @@ func TestGitLabProviderOverrides(t *testing.T) {
p.Data().LoginURL.String())
assert.Equal(t, "https://example.com/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://example.com/api/v3/user",
assert.Equal(t, "https://example.com/api/v4/user",
p.Data().ValidateURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}

View File

@ -67,7 +67,7 @@ func emailFromIdToken(idToken string) (string, error) {
// id_token is a base64 encode ID token payload
// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
jwt := strings.Split(idToken, ".")
b, err := jwtDecodeSegment(jwt[1])
b, err := base64.RawURLEncoding.DecodeString(jwt[1])
if err != nil {
return "", err
}
@ -89,14 +89,6 @@ func emailFromIdToken(idToken string) (string, error) {
return email.Email, nil
}
func jwtDecodeSegment(seg string) ([]byte, error) {
if l := len(seg) % 4; l > 0 {
seg += strings.Repeat("=", 4-l)
}
return base64.URLEncoding.DecodeString(seg)
}
func (p *GoogleProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
if code == "" {
err = errors.New("missing code")

View File

@ -8,7 +8,7 @@ import (
"net/url"
"testing"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
func newRedeemServer(body []byte) (*url.URL, *httptest.Server) {

View File

@ -72,3 +72,8 @@ func validateToken(p Provider, access_token string, header http.Header) bool {
log.Printf("token validation request failed: status %d - %s", resp.StatusCode, body)
return false
}
func updateURL(url *url.URL, hostname string) {
url.Scheme = "http"
url.Host = hostname
}

View File

@ -7,7 +7,7 @@ import (
"net/url"
"testing"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
type ValidateSessionStateTestProvider struct {

View File

@ -1,11 +1,12 @@
package providers
import (
"github.com/bmizerany/assert"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func testLinkedInProvider(hostname string) *LinkedInProvider {

View File

@ -1,58 +0,0 @@
package providers
import (
"log"
"net/http"
"net/url"
"github.com/bitly/oauth2_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.ValidateURL.String() == "" {
p.ValidateURL = &url.URL{Scheme: "https",
Host: myUsaHost,
Path: "/api/v1/tokeninfo"}
}
if p.Scope == "" {
p.Scope = "profile.email"
}
return &MyUsaProvider{ProviderData: p}
}
func (p *MyUsaProvider) GetEmailAddress(s *SessionState) (string, error) {
req, err := http.NewRequest("GET",
p.ProfileURL.String()+"?access_token="+s.AccessToken, 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()
}

85
providers/oidc.go Normal file
View File

@ -0,0 +1,85 @@
package providers
import (
"context"
"fmt"
"time"
"golang.org/x/oauth2"
oidc "github.com/coreos/go-oidc"
)
type OIDCProvider struct {
*ProviderData
Verifier *oidc.IDTokenVerifier
}
func NewOIDCProvider(p *ProviderData) *OIDCProvider {
p.ProviderName = "OpenID Connect"
return &OIDCProvider{ProviderData: p}
}
func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
ctx := context.Background()
c := oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: p.RedeemURL.String(),
},
RedirectURL: redirectURL,
}
token, err := c.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("token exchange: %v", err)
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("token response did not contain an id_token")
}
// Parse and verify ID Token payload.
idToken, err := p.Verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("could not verify id_token: %v", err)
}
// Extract custom claims.
var claims struct {
Email string `json:"email"`
Verified *bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("failed to parse id_token claims: %v", err)
}
if claims.Email == "" {
return nil, fmt.Errorf("id_token did not contain an email")
}
if claims.Verified != nil && !*claims.Verified {
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
}
s = &SessionState{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
ExpiresOn: token.Expiry,
Email: claims.Email,
}
return
}
func (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
return false, nil
}
origExpiration := s.ExpiresOn
s.ExpiresOn = time.Now().Add(time.Second).Truncate(time.Second)
fmt.Printf("refreshed access token %s (expired on %s)\n", s, origExpiration)
return false, nil
}

View File

@ -111,10 +111,15 @@ func (p *ProviderData) GetEmailAddress(s *SessionState) (string, error) {
return "", errors.New("not implemented")
}
func (p *ProviderData) GetGroups(s *SessionState, f string) (string, error) {
// GetUserName returns the Account username
func (p *ProviderData) GetUserName(s *SessionState) (string, error) {
return "", errors.New("not implemented")
}
func (p *ProviderData) GetGroups(s *SessionState, f string) ([]string, error) {
return []string{}, errors.New("not implemented")
}
// ValidateGroup validates that the provided email exists in the configured provider
// email group(s).
func (p *ProviderData) ValidateGroup(s *SessionState) bool {

View File

@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
func TestRefresh(t *testing.T) {

View File

@ -7,7 +7,8 @@ import (
type Provider interface {
Data() *ProviderData
GetEmailAddress(*SessionState) (string, error)
GetGroups(*SessionState, string) (string, error)
GetUserName(*SessionState) (string, error)
GetGroups(*SessionState, string) ([]string, error)
Redeem(string, string) (*SessionState, error)
ValidateGroup(*SessionState) bool
ValidateSessionState(*SessionState) bool
@ -19,8 +20,6 @@ type Provider interface {
func New(provider string, p *ProviderData) (Provider, error) {
switch provider {
case "myusa":
return NewMyUsaProvider(p), nil
case "linkedin":
return NewLinkedInProvider(p), nil
case "facebook":
@ -31,6 +30,8 @@ func New(provider string, p *ProviderData) (Provider, error) {
return NewAzureProvider(p), nil
case "gitlab":
return NewGitLabProvider(p), nil
case "oidc":
return NewOIDCProvider(p), nil
default:
return NewGoogleProvider(p), nil
}

View File

@ -1,12 +1,12 @@
package providers
import (
"encoding/base64"
"fmt"
"github.com/bitly/oauth2_proxy/cookie"
"strconv"
"strings"
"time"
"github.com/bitly/oauth2_proxy/cookie"
)
type SessionState struct {
@ -27,7 +27,7 @@ func (s *SessionState) IsExpired() bool {
}
func (s *SessionState) String() string {
o := fmt.Sprintf("Session{%s", s.userOrEmail())
o := fmt.Sprintf("Session{%s", s.accountInfo())
if s.AccessToken != "" {
o += " token:true"
}
@ -45,17 +45,13 @@ func (s *SessionState) String() string {
func (s *SessionState) EncodeSessionState(c *cookie.Cipher) (string, error) {
if c == nil || s.AccessToken == "" {
return s.userOrEmail(), nil
return s.accountInfo(), nil
}
return s.EncryptedString(c)
}
func (s *SessionState) userOrEmail() string {
u := s.User
if s.Email != "" {
u = s.Email
}
return u
func (s *SessionState) accountInfo() string {
return fmt.Sprintf("email:%s user:%s", s.Email, s.User)
}
func (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {
@ -65,59 +61,84 @@ func (s *SessionState) EncryptedString(c *cookie.Cipher) (string, error) {
}
a := s.AccessToken
if a != "" {
a, err = c.Encrypt(a)
if err != nil {
if a, err = c.Encrypt(a); err != nil {
return "", err
}
}
r := s.RefreshToken
if r != "" {
r, err = c.Encrypt(r)
if err != nil {
if r, err = c.Encrypt(r); err != nil {
return "", err
}
}
return fmt.Sprintf("%s:%s:%d:%s:%s", s.userOrEmail(), a, s.ExpiresOn.Unix(), r, s.Groups), nil
encoded_groups := base64.StdEncoding.EncodeToString([]byte(s.Groups))
return fmt.Sprintf("%s|%s|%d|%s|%s", s.accountInfo(), a, s.ExpiresOn.Unix(), r, encoded_groups), nil
}
func decodeSessionStatePlain(v string) (s *SessionState, err error) {
chunks := strings.Split(v, " ")
if len(chunks) != 2 {
return nil, fmt.Errorf("could not decode session state: expected 2 chunks got %d", len(chunks))
}
email := strings.TrimPrefix(chunks[0], "email:")
user := strings.TrimPrefix(chunks[1], "user:")
if user == "" {
user = strings.Split(email, "@")[0]
}
return &SessionState{User: user, Email: email}, nil
}
func decodeUserGroups(v string) (groups string, err error) {
decoded_groups, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return "", err
}
return string(decoded_groups), nil
}
func DecodeSessionState(v string, c *cookie.Cipher) (s *SessionState, err error) {
chunks := strings.Split(v, ":")
if len(chunks) == 1 {
if strings.Contains(chunks[0], "@") {
u := strings.Split(v, "@")[0]
return &SessionState{Email: v, User: u}, nil
}
return &SessionState{User: v}, nil
if c == nil {
return decodeSessionStatePlain(v)
}
chunks := strings.Split(v, "|")
if len(chunks) != 5 {
err = fmt.Errorf("invalid number of fields (got %d expected 5)", len(chunks))
return
}
s = &SessionState{}
if c != nil && chunks[1] != "" {
s.AccessToken, err = c.Decrypt(chunks[1])
sessionState, err := decodeSessionStatePlain(chunks[0])
if err != nil {
return nil, err
}
}
if c != nil && chunks[3] != "" {
s.RefreshToken, err = c.Decrypt(chunks[3])
if err != nil {
// Access Token
if chunks[1] != "" {
if sessionState.AccessToken, err = c.Decrypt(chunks[1]); err != nil {
return nil, err
}
}
if u := chunks[0]; strings.Contains(u, "@") {
s.Email = u
s.User = strings.Split(u, "@")[0]
} else {
s.User = u
}
if chunks[4] != "" {
s.Groups = chunks[4]
}
ts, _ := strconv.Atoi(chunks[2])
s.ExpiresOn = time.Unix(int64(ts), 0)
return
sessionState.ExpiresOn = time.Unix(int64(ts), 0)
// Refresh Token
if chunks[3] != "" {
if sessionState.RefreshToken, err = c.Decrypt(chunks[3]); err != nil {
return nil, err
}
}
// User groups
if chunks[4] != "" {
if sessionState.Groups, err = decodeUserGroups(chunks[4]); err != nil {
return nil, err
}
}
return sessionState, nil
}

View File

@ -1,12 +1,14 @@
package providers
import (
"fmt"
"log"
"strings"
"testing"
"time"
"github.com/bitly/oauth2_proxy/cookie"
"github.com/bmizerany/assert"
"github.com/stretchr/testify/assert"
)
const secret = "0123456789abcdefghijklmnopqrstuv"
@ -26,11 +28,15 @@ func TestSessionStateSerialization(t *testing.T) {
}
encoded, err := s.EncodeSessionState(c)
assert.Equal(t, nil, err)
assert.Equal(t, 4, strings.Count(encoded, ":"))
log.Printf("T = %v", t)
log.Printf("encoded = %v", encoded)
// assert.Equal(t, 4, strings.Count(encoded, ":"))
assert.Equal(t, 4, strings.Count(encoded, "|"))
ss, err := DecodeSessionState(encoded, c)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, "user", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.AccessToken, ss.AccessToken)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
@ -40,6 +46,44 @@ func TestSessionStateSerialization(t *testing.T) {
ss, err = DecodeSessionState(encoded, c2)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, "user", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
assert.NotEqual(t, s.RefreshToken, ss.RefreshToken)
}
func TestSessionStateSerializationWithUser(t *testing.T) {
c, err := cookie.NewCipher([]byte(secret))
assert.Equal(t, nil, err)
c2, err := cookie.NewCipher([]byte(altSecret))
assert.Equal(t, nil, err)
s := &SessionState{
User: "just-user",
Email: "user@domain.com",
AccessToken: "token1234",
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(c)
assert.Equal(t, nil, err)
log.Printf("Encoded string = %s", encoded)
assert.Equal(t, 4, strings.Count(encoded, "|"))
ss, err := DecodeSessionState(encoded, c)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.AccessToken, ss.AccessToken)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.Equal(t, s.RefreshToken, ss.RefreshToken)
// ensure a different cipher can't decode properly (ie: it gets gibberish)
ss, err = DecodeSessionState(encoded, c2)
t.Logf("%#v", ss)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
@ -47,7 +91,6 @@ func TestSessionStateSerialization(t *testing.T) {
}
func TestSessionStateSerializationNoCipher(t *testing.T) {
s := &SessionState{
Email: "user@domain.com",
AccessToken: "token1234",
@ -56,25 +99,51 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
}
encoded, err := s.EncodeSessionState(nil)
assert.Equal(t, nil, err)
assert.Equal(t, s.Email, encoded)
expected := fmt.Sprintf("email:%s user:", s.Email)
assert.Equal(t, expected, encoded)
// only email should have been serialized
ss, err := DecodeSessionState(encoded, nil)
assert.Equal(t, nil, err)
assert.Equal(t, "user", ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, "", ss.AccessToken)
assert.Equal(t, "", ss.RefreshToken)
}
func TestSessionStateUserOrEmail(t *testing.T) {
func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
s := &SessionState{
User: "just-user",
Email: "user@domain.com",
AccessToken: "token1234",
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
RefreshToken: "refresh4321",
}
encoded, err := s.EncodeSessionState(nil)
assert.Equal(t, nil, err)
expected := fmt.Sprintf("email:%s user:%s", s.Email, s.User)
assert.Equal(t, expected, encoded)
// only email should have been serialized
ss, err := DecodeSessionState(encoded, nil)
assert.Equal(t, nil, err)
assert.Equal(t, s.User, ss.User)
assert.Equal(t, s.Email, ss.Email)
assert.Equal(t, "", ss.AccessToken)
assert.Equal(t, "", ss.RefreshToken)
}
func TestSessionStateAccountInfo(t *testing.T) {
s := &SessionState{
Email: "user@domain.com",
User: "just-user",
}
assert.Equal(t, "user@domain.com", s.userOrEmail())
expected := fmt.Sprintf("email:%v user:%v", s.Email, s.User)
assert.Equal(t, expected, s.accountInfo())
s.Email = ""
assert.Equal(t, "just-user", s.userOrEmail())
expected = fmt.Sprintf("email:%v user:%v", s.Email, s.User)
assert.Equal(t, expected, s.accountInfo())
}
func TestExpired(t *testing.T) {

View File

@ -115,7 +115,7 @@ func getTemplates() *template.Template {
{{ if .SignInMessage }}
<p>{{.SignInMessage}}</p>
{{ end}}
<button type="submit" class="btn">Sign in with a {{.ProviderName}} Account</button><br/>
<button type="submit" class="btn">Sign in with {{.ProviderName}}</button><br/>
</form>
</div>

View File

@ -1,8 +1,9 @@
package main
import (
"github.com/bmizerany/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTemplatesCompile(t *testing.T) {

21
test.sh
View File

@ -1,11 +1,14 @@
#!/bin/bash
set -e
EXIT_CODE=0
echo "gofmt"
diff -u <(echo -n) <(gofmt -d $(find . -type f -name '*.go' -not -path "./.godeps/*"))
echo "go vet"
go vet ./...
echo "go test"
go test -timeout 60s ./...
echo "go test -race"
GOMAXPROCS=4 go test -timeout 60s -race ./...
diff -u <(echo -n) <(gofmt -d $(find . -type f -name '*.go' -not -path "./vendor/*")) || EXIT_CODE=1
for pkg in $(go list ./... | grep -v '/vendor/' ); do
echo "testing $pkg"
echo "go vet $pkg"
go vet "$pkg" || EXIT_CODE=1
echo "go test -v $pkg"
go test -v -timeout 90s "$pkg" || EXIT_CODE=1
echo "go test -v -race $pkg"
GOMAXPROCS=4 go test -v -timeout 90s0s -race "$pkg" || EXIT_CODE=1
done
exit $EXIT_CODE

View File

@ -20,7 +20,7 @@ func NewValidatorTest(t *testing.T) *ValidatorTest {
if err != nil {
t.Fatal("failed to create temp file: " + err.Error())
}
vt.done = make(chan bool)
vt.done = make(chan bool, 1)
return vt
}

View File

@ -86,7 +86,7 @@ func TestValidatorOverwriteEmailListViaRenameAndReplace(t *testing.T) {
vt.WriteEmails(t, []string{"xyzzy@example.com"})
domains := []string(nil)
updated := make(chan bool)
updated := make(chan bool, 1)
validator := vt.NewValidator(domains, updated)
if !validator("xyzzy@example.com") {