*: 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"
This commit is contained in:
Eric Chiang 2017-05-09 11:20:35 -07:00
parent ea2540bc89
commit cb48577ede
6 changed files with 140 additions and 7 deletions

3
Godeps
View File

@ -8,3 +8,6 @@ golang.org/x/oauth2 7fdf09982454086d5570c7db3e11f360194830c
golang.org/x/net/context 242b6b35177ec3909636b6cf6a47e8c2c6324b5d golang.org/x/net/context 242b6b35177ec3909636b6cf6a47e8c2c6324b5d
google.golang.org/api/admin/directory/v1 650535c7d6201e8304c92f38c922a9a3a36c6877 google.golang.org/api/admin/directory/v1 650535c7d6201e8304c92f38c922a9a3a36c6877
cloud.google.com/go/compute/metadata v0.7.0 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

View File

@ -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. 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 ## 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=*`. 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=*`.

View File

@ -69,6 +69,7 @@ func main() {
flagSet.Bool("request-logging", true, "Log requests to stdout") flagSet.Bool("request-logging", true, "Log requests to stdout")
flagSet.String("provider", "google", "OAuth provider") 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("login-url", "", "Authentication endpoint")
flagSet.String("redeem-url", "", "Token redemption endpoint") flagSet.String("redeem-url", "", "Token redemption endpoint")
flagSet.String("profile-url", "", "Profile access endpoint") flagSet.String("profile-url", "", "Profile access endpoint")

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"crypto" "crypto"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
@ -14,6 +15,7 @@ import (
"github.com/18F/hmacauth" "github.com/18F/hmacauth"
"github.com/bitly/oauth2_proxy/providers" "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 // 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 // These options allow for other providers besides Google, with
// potential overrides. // potential overrides.
Provider string `flag:"provider" cfg:"provider"` Provider string `flag:"provider" cfg:"provider"`
OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"`
LoginURL string `flag:"login-url" cfg:"login_url"` LoginURL string `flag:"login-url" cfg:"login_url"`
RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
ProfileURL string `flag:"profile-url" cfg:"profile_url"` ProfileURL string `flag:"profile-url" cfg:"profile_url"`
@ -81,6 +84,7 @@ type Options struct {
CompiledRegex []*regexp.Regexp CompiledRegex []*regexp.Regexp
provider providers.Provider provider providers.Provider
signatureData *SignatureData signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier
} }
type SignatureData struct { type SignatureData struct {
@ -120,6 +124,14 @@ func parseURL(to_parse string, urltype string, msgs []string) (*url.URL, []strin
} }
func (o *Options) Validate() error { 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) msgs := make([]string, 0)
if len(o.Upstreams) < 1 { if len(o.Upstreams) < 1 {
msgs = append(msgs, "missing setting: upstream") 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") 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) o.redirectURL, msgs = parseURL(o.RedirectURL, "redirect", msgs)
for _, u := range o.Upstreams { for _, u := range o.Upstreams {
@ -210,13 +238,6 @@ func (o *Options) Validate() error {
msgs = parseSignatureKey(o, msgs) msgs = parseSignatureKey(o, msgs)
msgs = validateCookieName(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 { if len(msgs) != 0 {
return fmt.Errorf("Invalid configuration:\n %s", return fmt.Errorf("Invalid configuration:\n %s",
strings.Join(msgs, "\n ")) strings.Join(msgs, "\n "))
@ -252,6 +273,12 @@ func parseProviderInfo(o *Options, msgs []string) []string {
p.SetGroupRestriction(o.GoogleGroups, o.GoogleAdminEmail, file) 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 return msgs
} }

84
providers/oidc.go Normal file
View File

@ -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
}

View File

@ -30,6 +30,8 @@ func New(provider string, p *ProviderData) Provider {
return NewAzureProvider(p) return NewAzureProvider(p)
case "gitlab": case "gitlab":
return NewGitLabProvider(p) return NewGitLabProvider(p)
case "oidc":
return NewOIDCProvider(p)
default: default:
return NewGoogleProvider(p) return NewGoogleProvider(p)
} }