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
|
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
|
||||||
|
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.
|
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=*`.
|
||||||
|
1
main.go
1
main.go
@ -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")
|
||||||
|
41
options.go
41
options.go
@ -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 o.CookieSecret == "" {
|
if o.CookieSecret == "" {
|
||||||
msgs = append(msgs, "missing setting: cookie-secret")
|
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")
|
"\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 {
|
||||||
@ -207,13 +235,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 "))
|
||||||
@ -249,6 +270,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
84
providers/oidc.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user