git.iko.soy/typeable/oauth2-proxy
on master help sign in
*: add an OpenID Connect provider
cb48577Eric Chiang
@@ -8,3 +8,6 @@ golang.org/x/oauth2 7fdf09982454086d5570c7db3e11f360194830c
8golang.org/x/net/context 242b6b35177ec3909636b6cf6a47e8c2c6324b5d
8golang.org/x/net/context 242b6b35177ec3909636b6cf6a47e8c2c6324b5d
9google.golang.org/api/admin/directory/v1 650535c7d6201e8304c92f38c922a9a3a36c6877
9google.golang.org/api/admin/directory/v1 650535c7d6201e8304c92f38c922a9a3a36c6877
10cloud.google.com/go/compute/metadata v0.7.0
10cloud.google.com/go/compute/metadata v0.7.0
11+github.com/coreos/go-oidc c797a55f1c1001ec3169f1d0fbb4c5523563bec6
12+gopkg.in/square/go-jose.v2 v2.1.1
13+github.com/pquerna/cachecontrol 9299cc36e57c32f83e47ffb3c25d8a3dec10ea0b
README.md+16
@@ -139,6 +139,22 @@ For adding an application to the Microsoft Azure AD follow [these steps to add a
139
139
140Take 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.
140Take 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.
141
141
142+### OpenID Connect Provider
143+
144+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.
145+
146+1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md).
147+2. Setup oauth2_proxy with the correct provider and using the default ports and callbacks.
148+3. Login with the fixture use in the dex guide and run the oauth2_proxy with the following args:
149+
150+ -provider oidc
151+ -client-id oauth2_proxy
152+ -client-secret proxy
153+ -redirect-url http://127.0.0.1:4180/oauth2/callback
154+ -oidc-issuer-url http://127.0.0.1:5556
155+ -cookie-secure=false
156+ -email-domain example.com
157+
142## Email Authentication
158## Email Authentication
143
159
144To 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=*`.
160To 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=*`.
main.go+1
@@ -69,6 +69,7 @@ func main() {
69 flagSet.Bool("request-logging", true, "Log requests to stdout")
69 flagSet.Bool("request-logging", true, "Log requests to stdout")
70
70
71 flagSet.String("provider", "google", "OAuth provider")
71 flagSet.String("provider", "google", "OAuth provider")
72+ flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)")
72 flagSet.String("login-url", "", "Authentication endpoint")
73 flagSet.String("login-url", "", "Authentication endpoint")
73 flagSet.String("redeem-url", "", "Token redemption endpoint")
74 flagSet.String("redeem-url", "", "Token redemption endpoint")
74 flagSet.String("profile-url", "", "Profile access endpoint")
75 flagSet.String("profile-url", "", "Profile access endpoint")
options.go+34−7
@@ -1,6 +1,7 @@
1package main
1package main
2
2
3import (
3import (
4+ "context"
4 "crypto"
5 "crypto"
5 "crypto/tls"
6 "crypto/tls"
6 "encoding/base64"
7 "encoding/base64"
@@ -14,6 +15,7 @@ import (
14
15
15 "github.com/18F/hmacauth"
16 "github.com/18F/hmacauth"
16 "github.com/bitly/oauth2_proxy/providers"
17 "github.com/bitly/oauth2_proxy/providers"
18+ oidc "github.com/coreos/go-oidc"
17)
19)
18
20
19// Configuration Options that can be set by Command Line Flag, or Config File
21// Configuration Options that can be set by Command Line Flag, or Config File
@@ -63,6 +65,7 @@ type Options struct {
63 // These options allow for other providers besides Google, with
65 // These options allow for other providers besides Google, with
64 // potential overrides.
66 // potential overrides.
65 Provider string `flag:"provider" cfg:"provider"`
67 Provider string `flag:"provider" cfg:"provider"`
68+ OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
66 LoginURL string `flag:"login-url" cfg:"login_url"`
69 LoginURL string `flag:"login-url" cfg:"login_url"`
67 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
70 RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
68 ProfileURL string `flag:"profile-url" cfg:"profile_url"`
71 ProfileURL string `flag:"profile-url" cfg:"profile_url"`
@@ -81,6 +84,7 @@ type Options struct {
81 CompiledRegex []*regexp.Regexp
84 CompiledRegex []*regexp.Regexp
82 provider providers.Provider
85 provider providers.Provider
83 signatureData *SignatureData
86 signatureData *SignatureData
87+ oidcVerifier *oidc.IDTokenVerifier
84}
88}
85
89
86type SignatureData struct {
90type SignatureData struct {
@@ -120,6 +124,14 @@ func parseURL(to_parse string, urltype string, msgs []string) (*url.URL, []strin
120}
124}
121
125
122func (o *Options) Validate() error {
126func (o *Options) Validate() error {
127+ if o.SSLInsecureSkipVerify {
128+ // TODO: Accept a certificate bundle.
129+ insecureTransport := &http.Transport{
130+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
131+ }
132+ http.DefaultClient = &http.Client{Transport: insecureTransport}
133+ }
134+
123 msgs := make([]string, 0)
135 msgs := make([]string, 0)
124 if len(o.Upstreams) < 1 {
136 if len(o.Upstreams) < 1 {
125 msgs = append(msgs, "missing setting: upstream")
137 msgs = append(msgs, "missing setting: upstream")
@@ -137,6 +149,22 @@ func (o *Options) Validate() error {
137 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required.\n use email-domain=* to authorize all email addresses")
149 msgs = append(msgs, "missing setting for email validation: email-domain or authenticated-emails-file required.\n use email-domain=* to authorize all email addresses")
138 }
150 }
139
151
152+ if o.OIDCIssuerURL != "" {
153+ // Configure discoverable provider data.
154+ provider, err := oidc.NewProvider(context.Background(), o.OIDCIssuerURL)
155+ if err != nil {
156+ return err
157+ }
158+ o.oidcVerifier = provider.Verifier(&oidc.Config{
159+ ClientID: o.ClientID,
160+ })
161+ o.LoginURL = provider.Endpoint().AuthURL
162+ o.RedeemURL = provider.Endpoint().TokenURL
163+ if o.Scope == "" {
164+ o.Scope = "openid email profile"
165+ }
166+ }
167+
140 o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
168 o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
141
169
142 for _, u := range o.Upstreams {
170 for _, u := range o.Upstreams {
@@ -210,13 +238,6 @@ func (o *Options) Validate() error {
210 msgs = parseSignatureKey(o, msgs)
238 msgs = parseSignatureKey(o, msgs)
211 msgs = validateCookieName(o, msgs)
239 msgs = validateCookieName(o, msgs)
212
240
213− if o.SSLInsecureSkipVerify {
214− insecureTransport := &http.Transport{
215− TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
216− }
217− http.DefaultClient = &http.Client{Transport: insecureTransport}
218− }
219−
220 if len(msgs) != 0 {
241 if len(msgs) != 0 {
221 return fmt.Errorf("Invalid configuration:\n %s",
242 return fmt.Errorf("Invalid configuration:\n %s",
222 strings.Join(msgs, "\n "))
243 strings.Join(msgs, "\n "))
@@ -252,6 +273,12 @@ func parseProviderInfo(o *Options, msgs []string) []string {
252 p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
273 p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file)
253 }
274 }
254 }
275 }
276+ case *providers.OIDCProvider:
277+ if o.oidcVerifier == nil {
278+ msgs = append(msgs, "oidc provider requires an oidc issuer URL")
279+ } else {
280+ p.Verifier = o.oidcVerifier
281+ }
255 }
282 }
256 return msgs
283 return msgs
257}
284}
providers/oidc.gonew file+84
@@ -0,0 +1,84 @@
1+package providers
2+
3+import (
4+ "context"
5+ "fmt"
6+ "time"
7+
8+ "golang.org/x/oauth2"
9+
10+ oidc "github.com/coreos/go-oidc"
11+)
12+
13+type OIDCProvider struct {
14+ *ProviderData
15+
16+ Verifier *oidc.IDTokenVerifier
17+}
18+
19+func NewOIDCProvider(p *ProviderData) *OIDCProvider {
20+ return &OIDCProvider{ProviderData: p}
21+}
22+
23+func (p *OIDCProvider) Redeem(redirectURL, code string) (s *SessionState, err error) {
24+ ctx := context.Background()
25+ c := oauth2.Config{
26+ ClientID: p.ClientID,
27+ ClientSecret: p.ClientSecret,
28+ Endpoint: oauth2.Endpoint{
29+ TokenURL: p.RedeemURL.String(),
30+ },
31+ RedirectURL: redirectURL,
32+ }
33+ token, err := c.Exchange(ctx, code)
34+ if err != nil {
35+ return nil, fmt.Errorf("token exchange: %v", err)
36+ }
37+
38+ rawIDToken, ok := token.Extra("id_token").(string)
39+ if !ok {
40+ return nil, fmt.Errorf("token response did not contain an id_token")
41+ }
42+
43+ // Parse and verify ID Token payload.
44+ idToken, err := p.Verifier.Verify(ctx, rawIDToken)
45+ if err != nil {
46+ return nil, fmt.Errorf("could not verify id_token: %v", err)
47+ }
48+
49+ // Extract custom claims.
50+ var claims struct {
51+ Email string `json:"email"`
52+ Verified *bool `json:"email_verified"`
53+ }
54+ if err := idToken.Claims(&claims); err != nil {
55+ return nil, fmt.Errorf("failed to parse id_token claims: %v", err)
56+ }
57+
58+ if claims.Email == "" {
59+ return nil, fmt.Errorf("id_token did not contain an email")
60+ }
61+ if claims.Verified != nil && !*claims.Verified {
62+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
63+ }
64+
65+ s = &SessionState{
66+ AccessToken: token.AccessToken,
67+ RefreshToken: token.RefreshToken,
68+ ExpiresOn: token.Expiry,
69+ Email: claims.Email,
70+ }
71+
72+ return
73+}
74+
75+func (p *OIDCProvider) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
76+ if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
77+ return false, nil
78+ }
79+
80+ origExpiration := s.ExpiresOn
81+ s.ExpiresOn = time.Now().Add(time.Second).Truncate(time.Second)
82+ fmt.Printf("refreshed access token %s (expired on %s)\n", s, origExpiration)
83+ return false, nil
84+}
providers/providers.go+2
@@ -30,6 +30,8 @@ func New(provider string, p *ProviderData) Provider {
30 return NewAzureProvider(p)
30 return NewAzureProvider(p)
31 case "gitlab":
31 case "gitlab":
32 return NewGitLabProvider(p)
32 return NewGitLabProvider(p)
33+ case "oidc":
34+ return NewOIDCProvider(p)
33 default:
35 default:
34 return NewGoogleProvider(p)
36 return NewGoogleProvider(p)
35 }
37 }