From 1cb4b32fcb3b18e4f998435d61558d478efcd893 Mon Sep 17 00:00:00 2001 From: Mark Sagi-Kazar Date: Mon, 15 Nov 2021 14:27:45 +0100 Subject: [PATCH 01/15] chore: upgrade alpine Signed-off-by: Mark Sagi-Kazar --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e32daf4d..b508fee7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,8 +29,8 @@ ARG TARGETVARIANT ENV GOMPLATE_VERSION=v3.9.0 RUN wget -O /usr/local/bin/gomplate \ - "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \ - && chmod +x /usr/local/bin/gomplate + "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \ + && chmod +x /usr/local/bin/gomplate FROM alpine:3.14.3 From 9284ffb8c0d96eef422378b72479a7083e409ba6 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Fri, 4 May 2018 12:43:09 -0400 Subject: [PATCH 02/15] Add generic oauth connector Co-authored-by: Shash Reddy Signed-off-by: Joshua Winters --- connector/oauth/oauth.go | 242 ++++++++++++++++++++++++++++++++++ connector/oauth/oauth_test.go | 234 ++++++++++++++++++++++++++++++++ server/server.go | 2 + 3 files changed, 478 insertions(+) create mode 100644 connector/oauth/oauth.go create mode 100644 connector/oauth/oauth_test.go diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go new file mode 100644 index 00000000..7bf480cd --- /dev/null +++ b/connector/oauth/oauth.go @@ -0,0 +1,242 @@ +package oauth + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "strings" + "time" + + "github.com/dexidp/dex/connector" + "github.com/dexidp/dex/pkg/log" + "golang.org/x/oauth2" +) + +type oauthConnector struct { + clientID string + clientSecret string + redirectURI string + tokenURL string + authorizationURL string + userInfoURL string + scopes []string + groupsKey string + httpClient *http.Client + logger log.Logger +} + +type connectorData struct { + AccessToken string +} + +type Config struct { + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + TokenURL string `json:"tokenURL"` + AuthorizationURL string `json:"authorizationURL"` + UserInfoURL string `json:"userInfoURL"` + Scopes []string `json:"scopes"` + GroupsKey string `json:"groupsKey"` + RootCAs []string `json:"rootCAs"` + InsecureSkipVerify bool `json:"insecureSkipVerify"` +} + +func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { + var err error + + oauthConn := &oauthConnector{ + clientID: c.ClientID, + clientSecret: c.ClientSecret, + tokenURL: c.TokenURL, + authorizationURL: c.AuthorizationURL, + userInfoURL: c.UserInfoURL, + scopes: c.Scopes, + groupsKey: c.GroupsKey, + redirectURI: c.RedirectURI, + logger: logger, + } + + oauthConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) + if err != nil { + return nil, err + } + + return oauthConn, err +} + +func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) { + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} + for _, rootCA := range rootCAs { + 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 *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { + + if c.redirectURI != callbackURL { + return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + } + + oauth2Config := &oauth2.Config{ + ClientID: c.clientID, + ClientSecret: c.clientSecret, + Endpoint: oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL}, + RedirectURL: c.redirectURI, + Scopes: c.scopes, + } + + return oauth2Config.AuthCodeURL(state), nil +} + +func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { + + q := r.URL.Query() + if errType := q.Get("error"); errType != "" { + return identity, errors.New(q.Get("error_description")) + } + + oauth2Config := &oauth2.Config{ + ClientID: c.clientID, + ClientSecret: c.clientSecret, + Endpoint: oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL}, + RedirectURL: c.redirectURI, + Scopes: c.scopes, + } + + ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient) + + token, err := oauth2Config.Exchange(ctx, q.Get("code")) + if err != nil { + return identity, fmt.Errorf("OAuth connector: failed to get token: %v", err) + } + + client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)) + + userInfoResp, err := client.Get(c.userInfoURL) + if err != nil { + return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: %v", err) + } + + if userInfoResp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: status %d", userInfoResp.StatusCode) + } + + defer userInfoResp.Body.Close() + + var userInfoResult map[string]interface{} + err = json.NewDecoder(userInfoResp.Body).Decode(&userInfoResult) + + if err != nil { + return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err) + } + + identity.UserID, _ = userInfoResult["user_id"].(string) + identity.Name, _ = userInfoResult["name"].(string) + identity.Username, _ = userInfoResult["user_name"].(string) + identity.Email, _ = userInfoResult["email"].(string) + identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) + + if s.Groups { + if c.groupsKey == "" { + c.groupsKey = "groups" + } + + groups := map[string]bool{} + + c.addGroupsFromMap(groups, userInfoResult) + c.addGroupsFromToken(groups, token.AccessToken) + + for groupName, _ := range groups { + identity.Groups = append(identity.Groups, groupName) + } + } + + if s.OfflineAccess { + data := connectorData{AccessToken: token.AccessToken} + connData, err := json.Marshal(data) + if err != nil { + return identity, fmt.Errorf("OAuth Connector: failed to parse connector data for offline access: %v", err) + } + identity.ConnectorData = connData + } + + return identity, nil +} + +func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[string]interface{}) error { + groupsClaim, ok := result[c.groupsKey].([]interface{}) + if !ok { + return errors.New("Cant convert to array") + } + + for _, group := range groupsClaim { + if groupString, ok := group.(string); ok { + groups[groupString] = true + } + } + + return nil +} + +func (c *oauthConnector) addGroupsFromToken(groups map[string]bool, token string) error { + parts := strings.Split(token, ".") + if len(parts) < 2 { + return errors.New("Invalid token") + } + + decoded, err := decode(parts[1]) + if err != nil { + return err + } + + var claimsMap map[string]interface{} + err = json.Unmarshal(decoded, &claimsMap) + if err != nil { + return err + } + + return c.addGroupsFromMap(groups, claimsMap) +} + +func decode(seg string) ([]byte, error) { + if l := len(seg) % 4; l > 0 { + seg += strings.Repeat("=", 4-l) + } + + return base64.URLEncoding.DecodeString(seg) +} diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go new file mode 100644 index 00000000..2a43d72d --- /dev/null +++ b/connector/oauth/oauth_test.go @@ -0,0 +1,234 @@ +package oauth + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "sort" + "testing" + + "github.com/dexidp/dex/connector" + "github.com/sirupsen/logrus" + jose "gopkg.in/square/go-jose.v2" +) + +func TestOpen(t *testing.T) { + tokenClaims := map[string]interface{}{} + userInfoClaims := map[string]interface{}{} + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + + sort.Strings(conn.scopes) + + expectEqual(t, conn.clientID, "testClient") + expectEqual(t, conn.clientSecret, "testSecret") + expectEqual(t, conn.redirectURI, testServer.URL+"/callback") + expectEqual(t, conn.tokenURL, testServer.URL+"/token") + expectEqual(t, conn.authorizationURL, testServer.URL+"/authorize") + expectEqual(t, conn.userInfoURL, testServer.URL+"/userinfo") + expectEqual(t, len(conn.scopes), 2) + expectEqual(t, conn.scopes[0], "groups") + expectEqual(t, conn.scopes[1], "openid") +} + +func TestLoginURL(t *testing.T) { + tokenClaims := map[string]interface{}{} + userInfoClaims := map[string]interface{}{} + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + + loginURL, err := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") + expectEqual(t, err, nil) + + expectedURL, err := url.Parse(testServer.URL + "/authorize") + expectEqual(t, err, nil) + + values := url.Values{} + values.Add("client_id", "testClient") + values.Add("redirect_uri", conn.redirectURI) + values.Add("response_type", "code") + values.Add("scope", "openid groups") + values.Add("state", "some-state") + expectedURL.RawQuery = values.Encode() + + expectEqual(t, loginURL, expectedURL.String()) +} + +func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { + + tokenClaims := map[string]interface{}{} + + userInfoClaims := map[string]interface{}{ + "name": "test-name", + "user_name": "test-username", + "user_id": "test-user-id", + "email": "test-email", + "email_verified": true, + "groups_key": []string{"admin-group", "user-group"}, + } + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + req := newRequestWithAuthCode(t, testServer.URL, "some-code") + + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + expectEqual(t, err, nil) + + sort.Strings(identity.Groups) + expectEqual(t, len(identity.Groups), 2) + expectEqual(t, identity.Groups[0], "admin-group") + expectEqual(t, identity.Groups[1], "user-group") + expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.Username, "test-username") + expectEqual(t, identity.Email, "test-email") + expectEqual(t, identity.EmailVerified, true) +} + +func TestHandleCallBackForGroupsInToken(t *testing.T) { + + tokenClaims := map[string]interface{}{ + "groups_key": []string{"test-group"}, + } + + userInfoClaims := map[string]interface{}{ + "name": "test-name", + "user_name": "test-username", + "user_id": "test-user-id", + "email": "test-email", + "email_verified": true, + } + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + req := newRequestWithAuthCode(t, testServer.URL, "some-code") + + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + expectEqual(t, err, nil) + + expectEqual(t, len(identity.Groups), 1) + expectEqual(t, identity.Groups[0], "test-group") + expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.Username, "test-username") + expectEqual(t, identity.Email, "test-email") + expectEqual(t, identity.EmailVerified, true) +} + +func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { + + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + t.Fatal("Failed to generate rsa key", err) + } + + jwk := jose.JSONWebKey{ + Key: key, + KeyID: "some-key", + Algorithm: "RSA", + } + + mux := http.NewServeMux() + + mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { + token, err := newToken(&jwk, tokenClaims) + if err != nil { + t.Fatal("unable to generate token", err) + } + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(&map[string]string{ + "access_token": token, + "id_token": token, + "token_type": "Bearer", + }) + }) + + mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(userInfoClaims) + }) + + return httptest.NewServer(mux) +} + +func newToken(key *jose.JSONWebKey, claims map[string]interface{}) (string, error) { + signingKey := jose.SigningKey{Key: key, Algorithm: jose.RS256} + + signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{}) + if err != nil { + return "", fmt.Errorf("new signer: %v", err) + } + + payload, err := json.Marshal(claims) + if err != nil { + return "", fmt.Errorf("marshaling claims: %v", err) + } + + signature, err := signer.Sign(payload) + if err != nil { + return "", fmt.Errorf("signing payload: %v", err) + } + + return signature.CompactSerialize() +} + +func newConnector(t *testing.T, serverURL string) *oauthConnector { + testConfig := Config{ + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: serverURL + "/callback", + TokenURL: serverURL + "/token", + AuthorizationURL: serverURL + "/authorize", + UserInfoURL: serverURL + "/userinfo", + Scopes: []string{"openid", "groups"}, + GroupsKey: "groups_key", + } + + log := logrus.New() + + conn, err := testConfig.Open("id", log) + if err != nil { + t.Fatal(err) + } + + oauthConn, ok := conn.(*oauthConnector) + if !ok { + t.Fatal(errors.New("failed to convert to oauthConnector")) + } + + return oauthConn +} + +func newRequestWithAuthCode(t *testing.T, serverURL string, code string) *http.Request { + req, err := http.NewRequest("GET", serverURL, nil) + if err != nil { + t.Fatal("failed to create request", err) + } + + values := req.URL.Query() + values.Add("code", code) + req.URL.RawQuery = values.Encode() + + return req +} + +func expectEqual(t *testing.T, a interface{}, b interface{}) { + if !reflect.DeepEqual(a, b) { + t.Fatalf("Expected %+v to equal %+v", a, b) + } +} diff --git a/server/server.go b/server/server.go index ecd6c935..6b653fdb 100755 --- a/server/server.go +++ b/server/server.go @@ -38,6 +38,7 @@ import ( "github.com/dexidp/dex/connector/linkedin" "github.com/dexidp/dex/connector/microsoft" "github.com/dexidp/dex/connector/mock" + "github.com/dexidp/dex/connector/oauth" "github.com/dexidp/dex/connector/oidc" "github.com/dexidp/dex/connector/openshift" "github.com/dexidp/dex/connector/saml" @@ -538,6 +539,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{ "gitlab": func() ConnectorConfig { return new(gitlab.Config) }, "google": func() ConnectorConfig { return new(google.Config) }, "oidc": func() ConnectorConfig { return new(oidc.Config) }, + "oauth": func() ConnectorConfig { return new(oauth.Config) }, "saml": func() ConnectorConfig { return new(saml.Config) }, "authproxy": func() ConnectorConfig { return new(authproxy.Config) }, "linkedin": func() ConnectorConfig { return new(linkedin.Config) }, From a087c05ebf0c56ff768352c5236baad2e9341123 Mon Sep 17 00:00:00 2001 From: Josh Winters Date: Wed, 27 Feb 2019 14:59:44 -0500 Subject: [PATCH 03/15] Make oauth user name and user id configurable Signed-off-by: Josh Winters Co-authored-by: Mark Huang --- connector/oauth/oauth.go | 26 ++++++++++++++++++++------ connector/oauth/oauth_test.go | 12 ++++++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 7bf480cd..74424c2d 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -28,6 +28,8 @@ type oauthConnector struct { userInfoURL string scopes []string groupsKey string + userIDKey string + userNameKey string httpClient *http.Client logger log.Logger } @@ -45,6 +47,8 @@ type Config struct { UserInfoURL string `json:"userInfoURL"` Scopes []string `json:"scopes"` GroupsKey string `json:"groupsKey"` + UserIDKey string `json:"userIDKey"` + UserNameKey string `json:"userNameKey"` RootCAs []string `json:"rootCAs"` InsecureSkipVerify bool `json:"insecureSkipVerify"` } @@ -60,6 +64,8 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) userInfoURL: c.UserInfoURL, scopes: c.Scopes, groupsKey: c.GroupsKey, + userIDKey: c.UserIDKey, + userNameKey: c.UserNameKey, redirectURI: c.RedirectURI, logger: logger, } @@ -165,17 +171,25 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err) } - identity.UserID, _ = userInfoResult["user_id"].(string) + if c.userIDKey == "" { + c.userIDKey = "user_id" + } + + if c.userNameKey == "" { + c.userNameKey = "user_name" + } + + if c.groupsKey == "" { + c.groupsKey = "groups" + } + + identity.UserID, _ = userInfoResult[c.userIDKey].(string) + identity.Username, _ = userInfoResult[c.userNameKey].(string) identity.Name, _ = userInfoResult["name"].(string) - identity.Username, _ = userInfoResult["user_name"].(string) identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) if s.Groups { - if c.groupsKey == "" { - c.groupsKey = "groups" - } - groups := map[string]bool{} c.addGroupsFromMap(groups, userInfoResult) diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index 2a43d72d..cd39d7f9 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -72,8 +72,8 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { userInfoClaims := map[string]interface{}{ "name": "test-name", - "user_name": "test-username", - "user_id": "test-user-id", + "user_id_key": "test-user-id", + "user_name_key": "test-username", "email": "test-email", "email_verified": true, "groups_key": []string{"admin-group", "user-group"}, @@ -93,6 +93,7 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { expectEqual(t, identity.Groups[0], "admin-group") expectEqual(t, identity.Groups[1], "user-group") expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") expectEqual(t, identity.EmailVerified, true) @@ -106,8 +107,8 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { userInfoClaims := map[string]interface{}{ "name": "test-name", - "user_name": "test-username", - "user_id": "test-user-id", + "user_id_key": "test-user-id", + "user_name_key": "test-username", "email": "test-email", "email_verified": true, } @@ -124,6 +125,7 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { expectEqual(t, len(identity.Groups), 1) expectEqual(t, identity.Groups[0], "test-group") expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") expectEqual(t, identity.EmailVerified, true) @@ -197,6 +199,8 @@ func newConnector(t *testing.T, serverURL string) *oauthConnector { UserInfoURL: serverURL + "/userinfo", Scopes: []string{"openid", "groups"}, GroupsKey: "groups_key", + UserIDKey: "user_id_key", + UserNameKey: "user_name_key", } log := logrus.New() From 930b331a5b2953d7955404e1e6a27698b6c82ac6 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Thu, 16 Jan 2020 15:30:47 -0500 Subject: [PATCH 04/15] use PreferredUsername Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 13 ++++++------- connector/oauth/oauth_test.go | 10 ++++------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 74424c2d..056a1715 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -14,9 +14,10 @@ import ( "strings" "time" + "golang.org/x/oauth2" + "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/log" - "golang.org/x/oauth2" ) type oauthConnector struct { @@ -113,7 +114,6 @@ func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, err } func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { - if c.redirectURI != callbackURL { return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } @@ -130,7 +130,6 @@ func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state st } func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { - q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, errors.New(q.Get("error_description")) @@ -185,7 +184,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id identity.UserID, _ = userInfoResult[c.userIDKey].(string) identity.Username, _ = userInfoResult[c.userNameKey].(string) - identity.Name, _ = userInfoResult["name"].(string) + identity.PreferredUsername, _ = userInfoResult["name"].(string) identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) @@ -195,7 +194,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id c.addGroupsFromMap(groups, userInfoResult) c.addGroupsFromToken(groups, token.AccessToken) - for groupName, _ := range groups { + for groupName := range groups { identity.Groups = append(identity.Groups, groupName) } } @@ -215,7 +214,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[string]interface{}) error { groupsClaim, ok := result[c.groupsKey].([]interface{}) if !ok { - return errors.New("Cant convert to array") + return errors.New("cant convert to array") } for _, group := range groupsClaim { @@ -230,7 +229,7 @@ func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[str func (c *oauthConnector) addGroupsFromToken(groups map[string]bool, token string) error { parts := strings.Split(token, ".") if len(parts) < 2 { - return errors.New("Invalid token") + return errors.New("invalid token") } decoded, err := decode(parts[1]) diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index cd39d7f9..a496bb82 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -13,9 +13,10 @@ import ( "sort" "testing" - "github.com/dexidp/dex/connector" "github.com/sirupsen/logrus" jose "gopkg.in/square/go-jose.v2" + + "github.com/dexidp/dex/connector" ) func TestOpen(t *testing.T) { @@ -67,7 +68,6 @@ func TestLoginURL(t *testing.T) { } func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { - tokenClaims := map[string]interface{}{} userInfoClaims := map[string]interface{}{ @@ -92,7 +92,7 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { expectEqual(t, len(identity.Groups), 2) expectEqual(t, identity.Groups[0], "admin-group") expectEqual(t, identity.Groups[1], "user-group") - expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.PreferredUsername, "test-name") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") @@ -100,7 +100,6 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { } func TestHandleCallBackForGroupsInToken(t *testing.T) { - tokenClaims := map[string]interface{}{ "groups_key": []string{"test-group"}, } @@ -124,7 +123,7 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { expectEqual(t, len(identity.Groups), 1) expectEqual(t, identity.Groups[0], "test-group") - expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.PreferredUsername, "test-name") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") @@ -132,7 +131,6 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { } func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { - key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { t.Fatal("Failed to generate rsa key", err) From fdf19e8014ae309f1cece9529b9354234d3bb459 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Fri, 31 Jul 2020 14:25:28 -0400 Subject: [PATCH 05/15] add docs for oauth connector Signed-off-by: Rui Yang --- README.md | 1 + docs/connectors/oauth.md | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 docs/connectors/oauth.md diff --git a/README.md b/README.md index 6ae2a7b0..2598f0c8 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Dex implements the following connectors: | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | | | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | | +| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | beta | Stable, beta, and alpha are defined as: diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md new file mode 100644 index 00000000..b4a5e9a5 --- /dev/null +++ b/docs/connectors/oauth.md @@ -0,0 +1,49 @@ +# Authentication using Generic OAuth 2.0 provider + +## Overview + +Dex users can make use of this connector to work with standards-compliant [OAuth 2.0](https://oauth.net/2/) authorization provider, in case of that authorization provider is not in the Dex connectors list. + +## Configuration + +The following is an example of a configuration for using OAuth connector with Reddit. + +```yaml +connectors: +- type: oauth + # ID of OAuth 2.0 provider + id: reddit + # Name of OAuth 2.0 provider + name: reddit + config: + # Connector config values starting with a "$" will read from the environment. + clientID: $REDDIT_CLIENT_ID + clientSecret: $REDDIT_CLIENT_SECRET + redirectURI: http://127.0.0.1:5556/callback + + tokenURL: https://www.reddit.com/api/v1/access_token + authorizationURL: https://www.reddit.com/api/v1/authorize + userInfoURL: https: https://www.reddit.com/api/v1/me + + # Optional: Specify whether to communicate to Auth provider without validating SSL certificates + # insecureSkipVerify: false + + # Optional: The location of file containing SSL certificates to commmunicate to Auth provider + # rootCAs: /etc/ssl/reddit.pem + + # Optional: List of scopes to request Auth provider for access user account + # scopes: + # - identity + + # Optional: Configurable keys for user id field look up + # Default: groups + # groupsKey: + + # Optional: Configurable keys for name field look up + # Default: user_id + # userIDKey: + + # Optional: Configurable keys for username field look up + # Default: user_name + # userNameKey: +``` \ No newline at end of file From 9952851cc45d74ab09f8e3e2d56902e8d8205b45 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Tue, 4 Aug 2020 11:00:40 -0400 Subject: [PATCH 06/15] add configurable preferred_username key Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 56 +++++++++++++++++++---------------- connector/oauth/oauth_test.go | 28 ++++++++++-------- docs/connectors/oauth.md | 12 ++++---- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 056a1715..849db331 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -21,18 +21,19 @@ import ( ) type oauthConnector struct { - clientID string - clientSecret string - redirectURI string - tokenURL string - authorizationURL string - userInfoURL string - scopes []string - groupsKey string - userIDKey string - userNameKey string - httpClient *http.Client - logger log.Logger + clientID string + clientSecret string + redirectURI string + tokenURL string + authorizationURL string + userInfoURL string + scopes []string + groupsKey string + userIDKey string + userNameKey string + preferredUsernameKey string + httpClient *http.Client + logger log.Logger } type connectorData struct { @@ -40,18 +41,19 @@ type connectorData struct { } type Config struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - TokenURL string `json:"tokenURL"` - AuthorizationURL string `json:"authorizationURL"` - UserInfoURL string `json:"userInfoURL"` - Scopes []string `json:"scopes"` - GroupsKey string `json:"groupsKey"` - UserIDKey string `json:"userIDKey"` - UserNameKey string `json:"userNameKey"` - RootCAs []string `json:"rootCAs"` - InsecureSkipVerify bool `json:"insecureSkipVerify"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + TokenURL string `json:"tokenURL"` + AuthorizationURL string `json:"authorizationURL"` + UserInfoURL string `json:"userInfoURL"` + Scopes []string `json:"scopes"` + GroupsKey string `json:"groupsKey"` + UserIDKey string `json:"userIDKey"` + UserNameKey string `json:"userNameKey"` + PreferredUsernameKey string `json:"preferredUsernameKey"` + RootCAs []string `json:"rootCAs"` + InsecureSkipVerify bool `json:"insecureSkipVerify"` } func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { @@ -182,9 +184,13 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id c.groupsKey = "groups" } + if c.preferredUsernameKey == "" { + c.preferredUsernameKey = "preferred_username" + } + identity.UserID, _ = userInfoResult[c.userIDKey].(string) identity.Username, _ = userInfoResult[c.userNameKey].(string) - identity.PreferredUsername, _ = userInfoResult["name"].(string) + identity.PreferredUsername, _ = userInfoResult[c.preferredUsernameKey].(string) identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index a496bb82..3ee06207 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -71,12 +71,13 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { tokenClaims := map[string]interface{}{} userInfoClaims := map[string]interface{}{ - "name": "test-name", - "user_id_key": "test-user-id", - "user_name_key": "test-username", - "email": "test-email", - "email_verified": true, - "groups_key": []string{"admin-group", "user-group"}, + "name": "test-name", + "user_id_key": "test-user-id", + "user_name_key": "test-username", + "preferred_username": "test-preferred-username", + "email": "test-email", + "email_verified": true, + "groups_key": []string{"admin-group", "user-group"}, } testServer := testSetup(t, tokenClaims, userInfoClaims) @@ -92,9 +93,9 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { expectEqual(t, len(identity.Groups), 2) expectEqual(t, identity.Groups[0], "admin-group") expectEqual(t, identity.Groups[1], "user-group") - expectEqual(t, identity.PreferredUsername, "test-name") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") + expectEqual(t, identity.PreferredUsername, "test-preferred-username") expectEqual(t, identity.Email, "test-email") expectEqual(t, identity.EmailVerified, true) } @@ -105,11 +106,12 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { } userInfoClaims := map[string]interface{}{ - "name": "test-name", - "user_id_key": "test-user-id", - "user_name_key": "test-username", - "email": "test-email", - "email_verified": true, + "name": "test-name", + "user_id_key": "test-user-id", + "user_name_key": "test-username", + "preferred_username": "test-preferred-username", + "email": "test-email", + "email_verified": true, } testServer := testSetup(t, tokenClaims, userInfoClaims) @@ -123,7 +125,7 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { expectEqual(t, len(identity.Groups), 1) expectEqual(t, identity.Groups[0], "test-group") - expectEqual(t, identity.PreferredUsername, "test-name") + expectEqual(t, identity.PreferredUsername, "test-preferred-username") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md index b4a5e9a5..2092b495 100644 --- a/docs/connectors/oauth.md +++ b/docs/connectors/oauth.md @@ -35,15 +35,15 @@ connectors: # scopes: # - identity - # Optional: Configurable keys for user id field look up + # Optional: Configurable keys for groups claim look up # Default: groups # groupsKey: - # Optional: Configurable keys for name field look up + # Optional: Configurable keys for user ID claim look up # Default: user_id # userIDKey: - # Optional: Configurable keys for username field look up - # Default: user_name - # userNameKey: -``` \ No newline at end of file + # Optional: Configurable keys for preferred username claim look up + # Default: preferred_username + # preferredUsernameKey: +``` From 60b8875780217975c67623def3733e64c9f63357 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Tue, 6 Oct 2020 21:04:06 -0400 Subject: [PATCH 07/15] use testify in oauth tests Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 2 +- connector/oauth/oauth_test.go | 66 ++++++++++++++++------------------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 849db331..d14319a5 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -220,7 +220,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[string]interface{}) error { groupsClaim, ok := result[c.groupsKey].([]interface{}) if !ok { - return errors.New("cant convert to array") + return errors.New("cannot convert to slice") } for _, group := range groupsClaim { diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index 3ee06207..7ba12be1 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -9,11 +9,11 @@ import ( "net/http" "net/http/httptest" "net/url" - "reflect" "sort" "testing" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" jose "gopkg.in/square/go-jose.v2" "github.com/dexidp/dex/connector" @@ -30,15 +30,15 @@ func TestOpen(t *testing.T) { sort.Strings(conn.scopes) - expectEqual(t, conn.clientID, "testClient") - expectEqual(t, conn.clientSecret, "testSecret") - expectEqual(t, conn.redirectURI, testServer.URL+"/callback") - expectEqual(t, conn.tokenURL, testServer.URL+"/token") - expectEqual(t, conn.authorizationURL, testServer.URL+"/authorize") - expectEqual(t, conn.userInfoURL, testServer.URL+"/userinfo") - expectEqual(t, len(conn.scopes), 2) - expectEqual(t, conn.scopes[0], "groups") - expectEqual(t, conn.scopes[1], "openid") + assert.Equal(t, conn.clientID, "testClient") + assert.Equal(t, conn.clientSecret, "testSecret") + assert.Equal(t, conn.redirectURI, testServer.URL+"/callback") + assert.Equal(t, conn.tokenURL, testServer.URL+"/token") + assert.Equal(t, conn.authorizationURL, testServer.URL+"/authorize") + assert.Equal(t, conn.userInfoURL, testServer.URL+"/userinfo") + assert.Equal(t, len(conn.scopes), 2) + assert.Equal(t, conn.scopes[0], "groups") + assert.Equal(t, conn.scopes[1], "openid") } func TestLoginURL(t *testing.T) { @@ -51,10 +51,10 @@ func TestLoginURL(t *testing.T) { conn := newConnector(t, testServer.URL) loginURL, err := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") - expectEqual(t, err, nil) + assert.Equal(t, err, nil) expectedURL, err := url.Parse(testServer.URL + "/authorize") - expectEqual(t, err, nil) + assert.Equal(t, err, nil) values := url.Values{} values.Add("client_id", "testClient") @@ -64,7 +64,7 @@ func TestLoginURL(t *testing.T) { values.Add("state", "some-state") expectedURL.RawQuery = values.Encode() - expectEqual(t, loginURL, expectedURL.String()) + assert.Equal(t, loginURL, expectedURL.String()) } func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { @@ -87,17 +87,17 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { req := newRequestWithAuthCode(t, testServer.URL, "some-code") identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) - expectEqual(t, err, nil) + assert.Equal(t, err, nil) sort.Strings(identity.Groups) - expectEqual(t, len(identity.Groups), 2) - expectEqual(t, identity.Groups[0], "admin-group") - expectEqual(t, identity.Groups[1], "user-group") - expectEqual(t, identity.UserID, "test-user-id") - expectEqual(t, identity.Username, "test-username") - expectEqual(t, identity.PreferredUsername, "test-preferred-username") - expectEqual(t, identity.Email, "test-email") - expectEqual(t, identity.EmailVerified, true) + assert.Equal(t, len(identity.Groups), 2) + assert.Equal(t, identity.Groups[0], "admin-group") + assert.Equal(t, identity.Groups[1], "user-group") + assert.Equal(t, identity.UserID, "test-user-id") + assert.Equal(t, identity.Username, "test-username") + assert.Equal(t, identity.PreferredUsername, "test-preferred-username") + assert.Equal(t, identity.Email, "test-email") + assert.Equal(t, identity.EmailVerified, true) } func TestHandleCallBackForGroupsInToken(t *testing.T) { @@ -121,15 +121,15 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { req := newRequestWithAuthCode(t, testServer.URL, "some-code") identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) - expectEqual(t, err, nil) + assert.Equal(t, err, nil) - expectEqual(t, len(identity.Groups), 1) - expectEqual(t, identity.Groups[0], "test-group") - expectEqual(t, identity.PreferredUsername, "test-preferred-username") - expectEqual(t, identity.UserID, "test-user-id") - expectEqual(t, identity.Username, "test-username") - expectEqual(t, identity.Email, "test-email") - expectEqual(t, identity.EmailVerified, true) + assert.Equal(t, len(identity.Groups), 1) + assert.Equal(t, identity.Groups[0], "test-group") + assert.Equal(t, identity.PreferredUsername, "test-preferred-username") + assert.Equal(t, identity.UserID, "test-user-id") + assert.Equal(t, identity.Username, "test-username") + assert.Equal(t, identity.Email, "test-email") + assert.Equal(t, identity.EmailVerified, true) } func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { @@ -230,9 +230,3 @@ func newRequestWithAuthCode(t *testing.T, serverURL string, code string) *http.R return req } - -func expectEqual(t *testing.T, a interface{}, b interface{}) { - if !reflect.DeepEqual(a, b) { - t.Fatalf("Expected %+v to equal %+v", a, b) - } -} From 02860da8b6e1f88e7d32abc2c077ca609692849a Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 19 Oct 2020 22:18:25 -0400 Subject: [PATCH 08/15] use claim mappings when retrieving user identity Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 88 ++++++++++++++++++++++------------- connector/oauth/oauth_test.go | 19 ++++---- docs/connectors/oauth.md | 33 +++++++++---- 3 files changed, 90 insertions(+), 50 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index d14319a5..ad83dad2 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -28,10 +28,12 @@ type oauthConnector struct { authorizationURL string userInfoURL string scopes []string - groupsKey string userIDKey string userNameKey string preferredUsernameKey string + emailKey string + emailVerifiedKey string + groupsKey string httpClient *http.Client logger log.Logger } @@ -41,36 +43,43 @@ type connectorData struct { } type Config struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - TokenURL string `json:"tokenURL"` - AuthorizationURL string `json:"authorizationURL"` - UserInfoURL string `json:"userInfoURL"` - Scopes []string `json:"scopes"` - GroupsKey string `json:"groupsKey"` - UserIDKey string `json:"userIDKey"` - UserNameKey string `json:"userNameKey"` - PreferredUsernameKey string `json:"preferredUsernameKey"` - RootCAs []string `json:"rootCAs"` - InsecureSkipVerify bool `json:"insecureSkipVerify"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + TokenURL string `json:"tokenURL"` + AuthorizationURL string `json:"authorizationURL"` + UserInfoURL string `json:"userInfoURL"` + Scopes []string `json:"scopes"` + RootCAs []string `json:"rootCAs"` + InsecureSkipVerify bool `json:"insecureSkipVerify"` + UserIDKey string `json:"userIDKey"` // defaults to "id" + ClaimMapping struct { + UserNameKey string `json:"userNameKey"` // defaults to "user_name" + PreferredUsernameKey string `json:"preferredUsernameKey"` // defaults to "preferred_username" + GroupsKey string `json:"groupsKey"` // defaults to "groups" + EmailKey string `json:"emailKey"` // defaults to "email" + EmailVerifiedKey string `json:"emailVerifiedKey"` // defaults to "email_verified" + } `json:"claimMapping"` } func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error oauthConn := &oauthConnector{ - clientID: c.ClientID, - clientSecret: c.ClientSecret, - tokenURL: c.TokenURL, - authorizationURL: c.AuthorizationURL, - userInfoURL: c.UserInfoURL, - scopes: c.Scopes, - groupsKey: c.GroupsKey, - userIDKey: c.UserIDKey, - userNameKey: c.UserNameKey, - redirectURI: c.RedirectURI, - logger: logger, + clientID: c.ClientID, + clientSecret: c.ClientSecret, + tokenURL: c.TokenURL, + authorizationURL: c.AuthorizationURL, + userInfoURL: c.UserInfoURL, + scopes: c.Scopes, + redirectURI: c.RedirectURI, + logger: logger, + userIDKey: c.UserIDKey, + userNameKey: c.ClaimMapping.UserNameKey, + preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey, + groupsKey: c.ClaimMapping.GroupsKey, + emailKey: c.ClaimMapping.EmailKey, + emailVerifiedKey: c.ClaimMapping.EmailVerifiedKey, } oauthConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) @@ -173,26 +182,39 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id } if c.userIDKey == "" { - c.userIDKey = "user_id" + c.userIDKey = "id" } + userID, found := userInfoResult[c.userIDKey].(string) + if !found { + return identity, fmt.Errorf("OAuth Connector: not found %v claim", c.userIDKey) + } + + identity.UserID = userID if c.userNameKey == "" { c.userNameKey = "user_name" } - if c.groupsKey == "" { - c.groupsKey = "groups" - } - if c.preferredUsernameKey == "" { c.preferredUsernameKey = "preferred_username" } - identity.UserID, _ = userInfoResult[c.userIDKey].(string) + if c.groupsKey == "" { + c.groupsKey = "groups" + } + + if c.emailKey == "" { + c.emailKey = "email" + } + + if c.emailVerifiedKey == "" { + c.emailVerifiedKey = "email_verified" + } + identity.Username, _ = userInfoResult[c.userNameKey].(string) identity.PreferredUsername, _ = userInfoResult[c.preferredUsernameKey].(string) - identity.Email, _ = userInfoResult["email"].(string) - identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) + identity.Email, _ = userInfoResult[c.emailKey].(string) + identity.EmailVerified, _ = userInfoResult[c.emailVerifiedKey].(bool) if s.Groups { groups := map[string]bool{} diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index 7ba12be1..b8074aa4 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -75,8 +75,8 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { "user_id_key": "test-user-id", "user_name_key": "test-username", "preferred_username": "test-preferred-username", - "email": "test-email", - "email_verified": true, + "mail": "mod_mail", + "has_verified_email": false, "groups_key": []string{"admin-group", "user-group"}, } @@ -96,8 +96,8 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { assert.Equal(t, identity.UserID, "test-user-id") assert.Equal(t, identity.Username, "test-username") assert.Equal(t, identity.PreferredUsername, "test-preferred-username") - assert.Equal(t, identity.Email, "test-email") - assert.Equal(t, identity.EmailVerified, true) + assert.Equal(t, identity.Email, "mod_mail") + assert.Equal(t, identity.EmailVerified, false) } func TestHandleCallBackForGroupsInToken(t *testing.T) { @@ -128,8 +128,8 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { assert.Equal(t, identity.PreferredUsername, "test-preferred-username") assert.Equal(t, identity.UserID, "test-user-id") assert.Equal(t, identity.Username, "test-username") - assert.Equal(t, identity.Email, "test-email") - assert.Equal(t, identity.EmailVerified, true) + assert.Equal(t, identity.Email, "") + assert.Equal(t, identity.EmailVerified, false) } func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { @@ -198,11 +198,14 @@ func newConnector(t *testing.T, serverURL string) *oauthConnector { AuthorizationURL: serverURL + "/authorize", UserInfoURL: serverURL + "/userinfo", Scopes: []string{"openid", "groups"}, - GroupsKey: "groups_key", UserIDKey: "user_id_key", - UserNameKey: "user_name_key", } + testConfig.ClaimMapping.UserNameKey = "user_name_key" + testConfig.ClaimMapping.GroupsKey = "groups_key" + testConfig.ClaimMapping.EmailKey = "mail" + testConfig.ClaimMapping.EmailVerifiedKey = "has_verified_email" + log := logrus.New() conn, err := testConfig.Open("id", log) diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md index 2092b495..d129bc5f 100644 --- a/docs/connectors/oauth.md +++ b/docs/connectors/oauth.md @@ -35,15 +35,30 @@ connectors: # scopes: # - identity - # Optional: Configurable keys for groups claim look up - # Default: groups - # groupsKey: - - # Optional: Configurable keys for user ID claim look up - # Default: user_id + # Optional: Configurable keys for user ID look up + # Default: id # userIDKey: - # Optional: Configurable keys for preferred username claim look up - # Default: preferred_username - # preferredUsernameKey: + # Auth roviders return non-standard user identity profile + # Use claimMapping to map those user infomations to standard claims: + claimMapping: + # Optional: Configurable keys for user name look up + # Default: user_name + # userNameKey: + + # Optional: Configurable keys for preferred username look up + # Default: preferred_username + # preferredUsernameKey: + + # Optional: Configurable keys for user groups look up + # Default: groups + # groupsKey: + + # Optional: Configurable keys for email look up + # Default: email + # emailKey: + + # Optional: Configurable keys for email verified look up + # Default: email_verified + # emailVerifiedKey: ``` From 49cb30af264af9f75044ad27a005b5ec577669ed Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Wed, 4 Nov 2020 21:59:10 -0500 Subject: [PATCH 09/15] readme minor fix for oauth connector Signed-off-by: Rui Yang --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2598f0c8..05238b1b 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Dex implements the following connectors: | [SAML 2.0](https://dexidp.io/docs/connectors/saml/) | no | yes | no | stable | WARNING: Unmaintained and likely vulnerable to auth bypasses ([#1884](https://github.com/dexidp/dex/discussions/1884)) | | [GitLab](https://dexidp.io/docs/connectors/gitlab/) | yes | yes | yes | beta | | | [OpenID Connect](https://dexidp.io/docs/connectors/oidc/) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. | +| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth.md) | no | yes | yes | beta | | | [Google](https://dexidp.io/docs/connectors/google/) | yes | yes | yes | alpha | | | [LinkedIn](https://dexidp.io/docs/connectors/linkedin/) | yes | no | no | beta | | | [Microsoft](https://dexidp.io/docs/connectors/microsoft/) | yes | yes | no | beta | | @@ -80,7 +81,7 @@ Dex implements the following connectors: | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | | | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | | -| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | beta | +| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | Stable, beta, and alpha are defined as: From 8ea121b45ae1a82ab8f7c5aebf03b159b31065d8 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 15 Mar 2021 14:19:49 -0400 Subject: [PATCH 10/15] move oauth connector doc to dex website repo move default key values configure to connector construct function Signed-off-by: Rui Yang --- README.md | 4 +-- connector/oauth/oauth.go | 48 +++++++++++++++--------------- docs/connectors/oauth.md | 64 ---------------------------------------- 3 files changed, 26 insertions(+), 90 deletions(-) delete mode 100644 docs/connectors/oauth.md diff --git a/README.md b/README.md index 05238b1b..5e18e515 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Dex implements the following connectors: | [SAML 2.0](https://dexidp.io/docs/connectors/saml/) | no | yes | no | stable | WARNING: Unmaintained and likely vulnerable to auth bypasses ([#1884](https://github.com/dexidp/dex/discussions/1884)) | | [GitLab](https://dexidp.io/docs/connectors/gitlab/) | yes | yes | yes | beta | | | [OpenID Connect](https://dexidp.io/docs/connectors/oidc/) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. | -| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth.md) | no | yes | yes | beta | | +| [OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | | | [Google](https://dexidp.io/docs/connectors/google/) | yes | yes | yes | alpha | | | [LinkedIn](https://dexidp.io/docs/connectors/linkedin/) | yes | no | no | beta | | | [Microsoft](https://dexidp.io/docs/connectors/microsoft/) | yes | yes | no | beta | | @@ -81,7 +81,7 @@ Dex implements the following connectors: | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | | | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | | -| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | +| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | | Stable, beta, and alpha are defined as: diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index ad83dad2..c709531e 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -65,6 +65,30 @@ type Config struct { func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error + if c.UserIDKey == "" { + c.UserIDKey = "id" + } + + if c.ClaimMapping.UserNameKey == "" { + c.ClaimMapping.UserNameKey = "user_name" + } + + if c.ClaimMapping.PreferredUsernameKey == "" { + c.ClaimMapping.PreferredUsernameKey = "preferred_username" + } + + if c.ClaimMapping.GroupsKey == "" { + c.ClaimMapping.GroupsKey = "groups" + } + + if c.ClaimMapping.EmailKey == "" { + c.ClaimMapping.EmailKey = "email" + } + + if c.ClaimMapping.EmailVerifiedKey == "" { + c.ClaimMapping.EmailVerifiedKey = "email_verified" + } + oauthConn := &oauthConnector{ clientID: c.ClientID, clientSecret: c.ClientSecret, @@ -181,36 +205,12 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err) } - if c.userIDKey == "" { - c.userIDKey = "id" - } - userID, found := userInfoResult[c.userIDKey].(string) if !found { return identity, fmt.Errorf("OAuth Connector: not found %v claim", c.userIDKey) } identity.UserID = userID - if c.userNameKey == "" { - c.userNameKey = "user_name" - } - - if c.preferredUsernameKey == "" { - c.preferredUsernameKey = "preferred_username" - } - - if c.groupsKey == "" { - c.groupsKey = "groups" - } - - if c.emailKey == "" { - c.emailKey = "email" - } - - if c.emailVerifiedKey == "" { - c.emailVerifiedKey = "email_verified" - } - identity.Username, _ = userInfoResult[c.userNameKey].(string) identity.PreferredUsername, _ = userInfoResult[c.preferredUsernameKey].(string) identity.Email, _ = userInfoResult[c.emailKey].(string) diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md deleted file mode 100644 index d129bc5f..00000000 --- a/docs/connectors/oauth.md +++ /dev/null @@ -1,64 +0,0 @@ -# Authentication using Generic OAuth 2.0 provider - -## Overview - -Dex users can make use of this connector to work with standards-compliant [OAuth 2.0](https://oauth.net/2/) authorization provider, in case of that authorization provider is not in the Dex connectors list. - -## Configuration - -The following is an example of a configuration for using OAuth connector with Reddit. - -```yaml -connectors: -- type: oauth - # ID of OAuth 2.0 provider - id: reddit - # Name of OAuth 2.0 provider - name: reddit - config: - # Connector config values starting with a "$" will read from the environment. - clientID: $REDDIT_CLIENT_ID - clientSecret: $REDDIT_CLIENT_SECRET - redirectURI: http://127.0.0.1:5556/callback - - tokenURL: https://www.reddit.com/api/v1/access_token - authorizationURL: https://www.reddit.com/api/v1/authorize - userInfoURL: https: https://www.reddit.com/api/v1/me - - # Optional: Specify whether to communicate to Auth provider without validating SSL certificates - # insecureSkipVerify: false - - # Optional: The location of file containing SSL certificates to commmunicate to Auth provider - # rootCAs: /etc/ssl/reddit.pem - - # Optional: List of scopes to request Auth provider for access user account - # scopes: - # - identity - - # Optional: Configurable keys for user ID look up - # Default: id - # userIDKey: - - # Auth roviders return non-standard user identity profile - # Use claimMapping to map those user infomations to standard claims: - claimMapping: - # Optional: Configurable keys for user name look up - # Default: user_name - # userNameKey: - - # Optional: Configurable keys for preferred username look up - # Default: preferred_username - # preferredUsernameKey: - - # Optional: Configurable keys for user groups look up - # Default: groups - # groupsKey: - - # Optional: Configurable keys for email look up - # Default: email - # emailKey: - - # Optional: Configurable keys for email verified look up - # Default: email_verified - # emailVerifiedKey: -``` From f980d3e0a794b8a836c62516558ad4c9676b0471 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 10 May 2021 15:41:50 -0400 Subject: [PATCH 11/15] cleanup and optimization Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index c709531e..9ff69510 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -191,16 +191,14 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id if err != nil { return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: %v", err) } + defer userInfoResp.Body.Close() if userInfoResp.StatusCode != http.StatusOK { return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: status %d", userInfoResp.StatusCode) } - defer userInfoResp.Body.Close() - var userInfoResult map[string]interface{} err = json.NewDecoder(userInfoResp.Body).Decode(&userInfoResult) - if err != nil { return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err) } @@ -217,7 +215,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id identity.EmailVerified, _ = userInfoResult[c.emailVerifiedKey].(bool) if s.Groups { - groups := map[string]bool{} + groups := map[string]struct{}{} c.addGroupsFromMap(groups, userInfoResult) c.addGroupsFromToken(groups, token.AccessToken) @@ -239,7 +237,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id return identity, nil } -func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[string]interface{}) error { +func (c *oauthConnector) addGroupsFromMap(groups map[string]struct{}, result map[string]interface{}) error { groupsClaim, ok := result[c.groupsKey].([]interface{}) if !ok { return errors.New("cannot convert to slice") @@ -247,14 +245,14 @@ func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[str for _, group := range groupsClaim { if groupString, ok := group.(string); ok { - groups[groupString] = true + groups[groupString] = struct{}{} } } return nil } -func (c *oauthConnector) addGroupsFromToken(groups map[string]bool, token string) error { +func (c *oauthConnector) addGroupsFromToken(groups map[string]struct{}, token string) error { parts := strings.Split(token, ".") if len(parts) < 2 { return errors.New("invalid token") From 45932bd38a7a6283d33a4acacfdb73c89ab9058d Mon Sep 17 00:00:00 2001 From: Vlad Safronov Date: Mon, 25 Oct 2021 21:42:05 +0300 Subject: [PATCH 12/15] skymarshal: behaviour: Handle groups as maps There are cases when groups are represented as a list of maps, not strings e.g. "groups":[{"id":"1", "name":"gr1"},{"id": "2", "name":"gr2"}]. Handle groups represented as a list of maps. concourse/dex#23 Signed-off-by: Vlad Safronov --- connector/oauth/oauth.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 9ff69510..1bb9b068 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -247,6 +247,9 @@ func (c *oauthConnector) addGroupsFromMap(groups map[string]struct{}, result map if groupString, ok := group.(string); ok { groups[groupString] = struct{}{} } + if groupMap, ok := group.(map[string]interface{}); ok { + groups[groupMap["name"]] = true + } } return nil From 7c80e44cafa0761d3eb40bca36c6740e43e8228d Mon Sep 17 00:00:00 2001 From: Vlad Safronov Date: Wed, 3 Nov 2021 22:41:29 +0300 Subject: [PATCH 13/15] Add a test case Signed-off-by: Vlad Safronov --- connector/oauth/oauth.go | 4 +++- connector/oauth/oauth_test.go | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 1bb9b068..e3ecd55b 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -248,7 +248,9 @@ func (c *oauthConnector) addGroupsFromMap(groups map[string]struct{}, result map groups[groupString] = struct{}{} } if groupMap, ok := group.(map[string]interface{}); ok { - groups[groupMap["name"]] = true + if groupName, ok := groupMap["name"].(string); ok { + groups[groupName] = true + } } } diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index b8074aa4..082a3aa5 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -100,6 +100,42 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { assert.Equal(t, identity.EmailVerified, false) } +func TestHandleCallBackForGroupMapsInUserInfo(t *testing.T) { + tokenClaims := map[string]interface{}{} + + userInfoClaims := map[string]interface{}{ + "name": "test-name", + "user_id_key": "test-user-id", + "user_name_key": "test-username", + "preferred_username": "test-preferred-username", + "mail": "mod_mail", + "has_verified_email": false, + "groups_key": []interface{}{ + map[string]string{"name": "admin-group", "id": "111"}, + map[string]string{"name": "user-group", "id": "222"}, + }, + } + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + req := newRequestWithAuthCode(t, testServer.URL, "some-code") + + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + assert.Equal(t, err, nil) + + sort.Strings(identity.Groups) + assert.Equal(t, len(identity.Groups), 2) + assert.Equal(t, identity.Groups[0], "admin-group") + assert.Equal(t, identity.Groups[1], "user-group") + assert.Equal(t, identity.UserID, "test-user-id") + assert.Equal(t, identity.Username, "test-username") + assert.Equal(t, identity.PreferredUsername, "test-preferred-username") + assert.Equal(t, identity.Email, "mod_mail") + assert.Equal(t, identity.EmailVerified, false) +} + func TestHandleCallBackForGroupsInToken(t *testing.T) { tokenClaims := map[string]interface{}{ "groups_key": []string{"test-group"}, From 8b865169bd36c789597e3dc50b5f87f166c2a56f Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Wed, 17 Nov 2021 17:42:55 -0500 Subject: [PATCH 14/15] fix minor compilation error for group claim us 'os' insteak of 'io/ioutil' Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index e3ecd55b..55527670 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -8,9 +8,9 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "net" "net/http" + "os" "strings" "time" @@ -122,7 +122,7 @@ func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, err tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} for _, rootCA := range rootCAs { - rootCABytes, err := ioutil.ReadFile(rootCA) + rootCABytes, err := os.ReadFile(rootCA) if err != nil { return nil, fmt.Errorf("failed to read root-ca: %v", err) } @@ -249,7 +249,7 @@ func (c *oauthConnector) addGroupsFromMap(groups map[string]struct{}, result map } if groupMap, ok := group.(map[string]interface{}); ok { if groupName, ok := groupMap["name"].(string); ok { - groups[groupName] = true + groups[groupName] = struct{}{} } } } From 539e08ba50d18ba6cec1b296ae160750435e3518 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Wed, 1 Dec 2021 12:45:25 -0500 Subject: [PATCH 15/15] small refactors and cleanup Signed-off-by: Rui Yang --- README.md | 1 - connector/oauth/oauth.go | 42 +++++++++++++++++++++++----------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5e18e515..b69bdc0a 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,6 @@ Dex implements the following connectors: | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | | | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | | -| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | | Stable, beta, and alpha are defined as: diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 55527670..e37932ad 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -65,28 +65,34 @@ type Config struct { func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error - if c.UserIDKey == "" { - c.UserIDKey = "id" + userIDKey := c.UserIDKey + if userIDKey == "" { + userIDKey = "id" } - if c.ClaimMapping.UserNameKey == "" { - c.ClaimMapping.UserNameKey = "user_name" + userNameKey := c.ClaimMapping.UserNameKey + if userNameKey == "" { + userNameKey = "user_name" } - if c.ClaimMapping.PreferredUsernameKey == "" { - c.ClaimMapping.PreferredUsernameKey = "preferred_username" + preferredUsernameKey := c.ClaimMapping.PreferredUsernameKey + if preferredUsernameKey == "" { + preferredUsernameKey = "preferred_username" } - if c.ClaimMapping.GroupsKey == "" { - c.ClaimMapping.GroupsKey = "groups" + groupsKey := c.ClaimMapping.GroupsKey + if groupsKey == "" { + groupsKey = "groups" } - if c.ClaimMapping.EmailKey == "" { - c.ClaimMapping.EmailKey = "email" + emailKey := c.ClaimMapping.EmailKey + if emailKey == "" { + emailKey = "email" } - if c.ClaimMapping.EmailVerifiedKey == "" { - c.ClaimMapping.EmailVerifiedKey = "email_verified" + emailVerifiedKey := c.ClaimMapping.EmailVerifiedKey + if emailVerifiedKey == "" { + emailVerifiedKey = "email_verified" } oauthConn := &oauthConnector{ @@ -98,12 +104,12 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) scopes: c.Scopes, redirectURI: c.RedirectURI, logger: logger, - userIDKey: c.UserIDKey, - userNameKey: c.ClaimMapping.UserNameKey, - preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey, - groupsKey: c.ClaimMapping.GroupsKey, - emailKey: c.ClaimMapping.EmailKey, - emailVerifiedKey: c.ClaimMapping.EmailVerifiedKey, + userIDKey: userIDKey, + userNameKey: userNameKey, + preferredUsernameKey: preferredUsernameKey, + groupsKey: groupsKey, + emailKey: emailKey, + emailVerifiedKey: emailVerifiedKey, } oauthConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify)