connector/github: add support for github enterprise.

This commit is contained in:
rithu john 2017-04-07 14:55:22 -07:00
parent 943253fece
commit 76b9eb1db9
2 changed files with 125 additions and 6 deletions

View file

@ -31,4 +31,37 @@ connectors:
org: my-oranization
```
## GitHub Enterprise
Users can use their GitHub Enterprise account to login to dex. The following configuration can be used to enable a GitHub Enterprise connector on dex:
```yaml
connectors:
- type: github
# Required field for connector id.
id: github
# Required field for connector name.
name: GitHub
config:
# Required fields. Dex must be pre-registered with GitHub Enterprise
# to get the following values.
# Credentials can be string literals or pulled from the environment.
clientID: $GITHUB_CLIENT_ID
clientSecret: $GITHUB_CLIENT_SECRET
redirectURI: http://127.0.0.1:5556/dex/callback
# Optional organization to pull teams from, communicate through the
# "groups" scope.
#
# NOTE: This is an EXPERIMENTAL config option and will likely change.
org: my-oranization
# Required ONLY for GitHub Enterprise.
# This is the Hostname of the GitHub Enterprise account listed on the
# management console. Ensure this domain is routable on your network.
hostName: git.example.com
# ONLY for GitHub Enterprise. Optional field.
# Used to support self-signed or untrusted CA root certificates.
rootCA: /etc/dex/ca.crt
```
[github-oauth2]: https://github.com/settings/applications/new

View file

@ -3,13 +3,18 @@ package github
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
@ -19,7 +24,7 @@ import (
)
const (
baseURL = "https://api.github.com"
apiURL = "https://api.github.com"
scopeEmail = "user:email"
scopeOrgs = "read:org"
)
@ -30,17 +35,45 @@ type Config struct {
ClientSecret string `json:"clientSecret"`
RedirectURI string `json:"redirectURI"`
Org string `json:"org"`
HostName string `json:"hostName"`
RootCA string `json:"rootCA"`
}
// Open returns a strategy for logging in through GitHub.
func (c *Config) Open(logger logrus.FieldLogger) (connector.Connector, error) {
return &githubConnector{
g := githubConnector{
redirectURI: c.RedirectURI,
org: c.Org,
clientID: c.ClientID,
clientSecret: c.ClientSecret,
apiURL: apiURL,
logger: logger,
}, nil
}
if c.HostName != "" {
// ensure this is a hostname and not a URL or path.
if strings.Contains(c.HostName, "/") {
return nil, errors.New("invalid hostname: hostname cannot contain `/`")
}
g.hostName = c.HostName
g.apiURL = "https://" + c.HostName + "/api/v3"
}
if c.RootCA != "" {
if c.HostName == "" {
return nil, errors.New("invalid connector config: Host name field required for a root certificate file")
}
g.rootCA = c.RootCA
var err error
if g.httpClient, err = newHTTPClient(g.rootCA); err != nil {
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
}
}
return &g, nil
}
type connectorData struct {
@ -59,6 +92,14 @@ type githubConnector struct {
clientID string
clientSecret string
logger logrus.FieldLogger
// apiURL defaults to "https://api.github.com"
apiURL string
// hostName of the GitHub enterprise account.
hostName string
// Used to support untrusted/self-signed CA certs.
rootCA string
// HTTP Client that trusts the custom delcared rootCA cert.
httpClient *http.Client
}
func (c *githubConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
@ -68,10 +109,21 @@ func (c *githubConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
} else {
githubScopes = []string{scopeEmail}
}
endpoint := github.Endpoint
// case when it is a GitHub Enterprise account.
if c.hostName != "" {
endpoint = oauth2.Endpoint{
AuthURL: "https://" + c.hostName + "/login/oauth/authorize",
TokenURL: "https://" + c.hostName + "/login/oauth/access_token",
}
}
return &oauth2.Config{
ClientID: c.clientID,
ClientSecret: c.clientSecret,
Endpoint: github.Endpoint,
Endpoint: endpoint,
Scopes: githubScopes,
}
}
@ -80,6 +132,7 @@ func (c *githubConnector) LoginURL(scopes connector.Scopes, callbackURL, state s
if c.redirectURI != callbackURL {
return "", fmt.Errorf("expected callback URL did not match the URL in the config")
}
return c.oauth2Config(scopes).AuthCodeURL(state), nil
}
@ -95,6 +148,34 @@ func (e *oauth2Error) Error() string {
return e.error + ": " + e.errorDescription
}
// newHTTPClient returns a new HTTP client that trusts the custom delcared rootCA cert.
func newHTTPClient(rootCA string) (*http.Client, error) {
tlsConfig := tls.Config{RootCAs: x509.NewCertPool()}
rootCABytes, err := ioutil.ReadFile(rootCA)
if err != nil {
return nil, fmt.Errorf("failed to read root-ca: %v", err)
}
if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) {
return nil, fmt.Errorf("no certs found in root CA file %q", rootCA)
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}, nil
}
func (c *githubConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
q := r.URL.Query()
if errType := q.Get("error"); errType != "" {
@ -102,7 +183,12 @@ func (c *githubConnector) HandleCallback(s connector.Scopes, r *http.Request) (i
}
oauth2Config := c.oauth2Config(s)
ctx := r.Context()
// GitHub Enterprise account
if c.httpClient != nil {
ctx = context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient)
}
token, err := oauth2Config.Exchange(ctx, q.Get("code"))
if err != nil {
@ -192,7 +278,7 @@ type user struct {
// a bearer token as part of the request.
func (c *githubConnector) user(ctx context.Context, client *http.Client) (user, error) {
var u user
req, err := http.NewRequest("GET", baseURL+"/user", nil)
req, err := http.NewRequest("GET", c.apiURL+"/user", nil)
if err != nil {
return u, fmt.Errorf("github: new req: %v", err)
}
@ -228,7 +314,7 @@ func (c *githubConnector) teams(ctx context.Context, client *http.Client, org st
// https://developer.github.com/v3/#pagination
reNext := regexp.MustCompile("<(.*)>; rel=\"next\"")
reLast := regexp.MustCompile("<(.*)>; rel=\"last\"")
apiURL := baseURL + "/user/teams"
apiURL := c.apiURL + "/user/teams"
for {
req, err := http.NewRequest("GET", apiURL, nil)