Merge pull request #389 from ericchiang/oidc-provider
*: add an OpenID Connect provider
This commit is contained in:
		
						commit
						e87c3eee13
					
				
							
								
								
									
										3
									
								
								Godeps
								
								
								
								
							
							
						
						
									
										3
									
								
								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 | ||||
|  |  | |||
							
								
								
									
										16
									
								
								README.md
								
								
								
								
							
							
						
						
									
										16
									
								
								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=*`. | ||||
|  |  | |||
							
								
								
									
										1
									
								
								main.go
								
								
								
								
							
							
						
						
									
										1
									
								
								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") | ||||
|  |  | |||
							
								
								
									
										41
									
								
								options.go
								
								
								
								
							
							
						
						
									
										41
									
								
								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 o.CookieSecret == "" { | ||||
| 		msgs = append(msgs, "missing setting: cookie-secret") | ||||
|  | @ -135,6 +147,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 { | ||||
|  | @ -207,13 +235,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  ")) | ||||
|  | @ -249,6 +270,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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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) | ||||
| 	} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue