From cb48577ede89837567a4d030570cc7cbdbf245dc Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Tue, 9 May 2017 11:20:35 -0700 Subject: [PATCH] *: add an OpenID Connect provider See the README for usage with Dex or any other OIDC provider. To test run a backend: python3 -m http.server Run dex and modify the example config with the proxy callback: go get github.com/coreos/dex/cmd/dex cd $GOPATH/src/github.com/coreos/dex sed -i.bak \ 's|http://127.0.0.1:5555/callback|http://127.0.0.1:5555/oauth2/callback|g' \ examples/config-dev.yaml make ./bin/dex serve examples/config-dev.yaml Then run the oauth2_proxy oauth2_proxy \ --oidc-issuer-url http://127.0.0.1:5556/dex \ --upstream http://localhost:8000 \ --client-id example-app \ --client-secret ZXhhbXBsZS1hcHAtc2VjcmV0 \ --cookie-secret foo \ --email-domain '*' \ --http-address http://127.0.0.1:5555 \ --redirect-url http://127.0.0.1:5555/oauth2/callback \ --cookie-secure=false Login with the username/password "admin@example.com:password" --- Godeps | 3 ++ README.md | 16 ++++++++ main.go | 1 + options.go | 41 +++++++++++++++++---- providers/oidc.go | 84 ++++++++++++++++++++++++++++++++++++++++++ providers/providers.go | 2 + 6 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 providers/oidc.go diff --git a/Godeps b/Godeps index 21884cd..efbaf7a 100644 --- a/Godeps +++ b/Godeps @@ -8,3 +8,6 @@ golang.org/x/oauth2 7fdf09982454086d5570c7db3e11f360194830c golang.org/x/net/context 242b6b35177ec3909636b6cf6a47e8c2c6324b5d google.golang.org/api/admin/directory/v1 650535c7d6201e8304c92f38c922a9a3a36c6877 cloud.google.com/go/compute/metadata v0.7.0 +github.com/coreos/go-oidc c797a55f1c1001ec3169f1d0fbb4c5523563bec6 +gopkg.in/square/go-jose.v2 v2.1.1 +github.com/pquerna/cachecontrol 9299cc36e57c32f83e47ffb3c25d8a3dec10ea0b diff --git a/README.md b/README.md index be73f36..a332ec5 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,22 @@ For adding an application to the Microsoft Azure AD follow [these steps to add a Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server. +### 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=*`. diff --git a/main.go b/main.go index ab0e4d3..b9d9c96 100644 --- a/main.go +++ b/main.go @@ -69,6 +69,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") diff --git a/options.go b/options.go index f1df916..9ed02ad 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto" "crypto/tls" "encoding/base64" @@ -14,6 +15,7 @@ import ( "github.com/18F/hmacauth" "github.com/bitly/oauth2_proxy/providers" + oidc "github.com/coreos/go-oidc" ) // Configuration Options that can be set by Command Line Flag, or Config File @@ -63,6 +65,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"` @@ -81,6 +84,7 @@ type Options struct { CompiledRegex []*regexp.Regexp provider providers.Provider signatureData *SignatureData + oidcVerifier *oidc.IDTokenVerifier } type SignatureData struct { @@ -120,6 +124,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 len(o.Upstreams) < 1 { msgs = append(msgs, "missing setting: upstream") @@ -137,6 +149,22 @@ func (o *Options) Validate() error { msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required.\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 { @@ -210,13 +238,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 ")) @@ -252,6 +273,12 @@ func parseProviderInfo(o *Options, msgs []string) []string { p.SetGroupRestriction(o.GoogleGroups, 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 } diff --git a/providers/oidc.go b/providers/oidc.go new file mode 100644 index 0000000..ec0152a --- /dev/null +++ b/providers/oidc.go @@ -0,0 +1,84 @@ +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 { + 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 +} diff --git a/providers/providers.go b/providers/providers.go index fb2e5fc..3aa4f39 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -30,6 +30,8 @@ func New(provider string, p *ProviderData) Provider { return NewAzureProvider(p) case "gitlab": return NewGitLabProvider(p) + case "oidc": + return NewOIDCProvider(p) default: return NewGoogleProvider(p) }