Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

*: add an OpenID Connect provider #389

Merged
merged 1 commit into from
Sep 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Godeps
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=*`.
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
41 changes: 34 additions & 7 deletions options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"crypto"
"crypto/tls"
"encoding/base64"
Expand All @@ -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
Expand Down Expand Up @@ -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"`
Expand All @@ -81,6 +84,7 @@ type Options struct {
CompiledRegex []*regexp.Regexp
provider providers.Provider
signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier
}

type SignatureData struct {
Expand Down Expand Up @@ -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")
Expand All @@ -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 != "" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, needs to come after overriding http.DefaultClient.

// 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 {
Expand Down Expand Up @@ -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 "))
Expand Down Expand Up @@ -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
}
Expand Down
84 changes: 84 additions & 0 deletions providers/oidc.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down