From 88d1e2b041a24ea85e9d3fcf7e7b7c19c908083e Mon Sep 17 00:00:00 2001
From: joannano <joanna.nosek@intel.com>
Date: Thu, 13 Dec 2018 12:22:53 +0100
Subject: [PATCH] keystone: test cases, refactoring and cleanup

---
 .travis.yml                         |   5 +-
 Dockerfile                          |   9 +-
 connector/connector.go              |   1 -
 connector/keystone/keystone.go      | 257 ++++++++-------
 connector/keystone/keystone_test.go | 467 ++++++++++++++++------------
 connector/keystone/types.go         | 139 +++------
 examples/config-keystone.yaml       |  55 ----
 server/handlers.go                  |   6 +-
 server/server.go                    |   4 +-
 storage/static.go                   |   1 +
 10 files changed, 474 insertions(+), 470 deletions(-)
 delete mode 100644 examples/config-keystone.yaml

diff --git a/.travis.yml b/.travis.yml
index 934e32e1..07b941bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,13 +13,14 @@ services:
   - docker
 
 env:
-  - DEX_POSTGRES_DATABASE=postgres DEX_POSTGRES_USER=postgres DEX_POSTGRES_HOST="localhost" DEX_ETCD_ENDPOINTS=http://localhost:2379 DEX_LDAP_TESTS=1 DEBIAN_FRONTEND=noninteractive
+  - DEX_POSTGRES_DATABASE=postgres DEX_POSTGRES_USER=postgres DEX_POSTGRES_HOST="localhost" DEX_ETCD_ENDPOINTS=http://localhost:2379 DEX_LDAP_TESTS=1 DEBIAN_FRONTEND=noninteractive DEX_KEYSTONE_URL=http://localhost:5000 DEX_KEYSTONE_ADMIN_URL=http://localhost:35357
 
 install:
   - sudo -E apt-get install -y --force-yes slapd time ldap-utils
   - sudo /etc/init.d/slapd stop
   - docker run -d --net=host gcr.io/etcd-development/etcd:v3.2.9
-
+  - docker run -d -p 0.0.0.0:5000:5000 -p 0.0.0.0:35357:35357 openio/openstack-keystone
+  - sleep 60s
 
 script:
   - make testall
diff --git a/Dockerfile b/Dockerfile
index d6ce6a9c..dbc0dd38 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,12 +11,15 @@ FROM alpine:3.8
 # experience when this doesn't work out of the box.
 #
 # OpenSSL is required so wget can query HTTPS endpoints for health checking.
-RUN apk add --update ca-certificates openssl bash
+RUN apk add --update ca-certificates openssl
+
+COPY --from=0 /go/bin/dex /usr/local/bin/dex
 
 # Import frontend assets and set the correct CWD directory so the assets
 # are in the default path.
 COPY web /web
 WORKDIR /
 
-EXPOSE 5500-5600
-CMD ["bash"]
+ENTRYPOINT ["dex"]
+
+CMD ["version"]
diff --git a/connector/connector.go b/connector/connector.go
index 0335ea94..c442c54a 100644
--- a/connector/connector.go
+++ b/connector/connector.go
@@ -35,7 +35,6 @@ type Identity struct {
 	//
 	// This data is never shared with end users, OAuth clients, or through the API.
 	ConnectorData []byte
-	Password string
 }
 
 // PasswordConnector is an interface implemented by connectors which take a
diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go
index d62aa179..8abff2b4 100644
--- a/connector/keystone/keystone.go
+++ b/connector/keystone/keystone.go
@@ -2,163 +2,186 @@
 package keystone
 
 import (
+	"bytes"
 	"context"
-	"fmt"
-	"github.com/dexidp/dex/connector"
-	"github.com/sirupsen/logrus"
 	"encoding/json"
-	"net/http"
-	"bytes"
+	"fmt"
 	"io/ioutil"
+	"net/http"
+
+	"github.com/sirupsen/logrus"
+
+	"github.com/dexidp/dex/connector"
 )
 
 var (
-	_ connector.PasswordConnector = &Connector{}
-  	_ connector.RefreshConnector = &Connector{}
+	_ connector.PasswordConnector = &keystoneConnector{}
+	_ connector.RefreshConnector  = &keystoneConnector{}
 )
 
 // Open returns an authentication strategy using Keystone.
 func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
-	return &Connector{c.Domain, c.KeystoneHost,
-	c.KeystoneUsername, c.KeystonePassword, logger}, nil
+	return &keystoneConnector{c.Domain, c.KeystoneHost,
+		c.KeystoneUsername, c.KeystonePassword, logger}, nil
 }
 
-func (p Connector) Close() error { return nil }
+func (p *keystoneConnector) Close() error { return nil }
 
-func (p Connector) Login(ctx context.Context, s connector.Scopes, username, password string) (
-		identity connector.Identity, validPassword bool, err error) {
-	response, err := p.getTokenResponse(username, password)
+func (p *keystoneConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (
+	identity connector.Identity, validPassword bool, err error) {
+	resp, err := p.getTokenResponse(ctx, username, password)
+	if err != nil {
+		return identity, false, fmt.Errorf("keystone: error %v", err)
+	}
 
 	// Providing wrong password or wrong keystone URI throws error
-	if err == nil && response.StatusCode == 201 {
-    	token := response.Header["X-Subject-Token"][0]
-		data, _ := ioutil.ReadAll(response.Body)
-
-    	var tokenResponse = new(TokenResponse)
-    	err := json.Unmarshal(data, &tokenResponse)
-
-    	if err != nil {
-      		fmt.Printf("keystone: invalid token response: %v", err)
-      		return identity, false, err
-    	}
-    	groups, err := p.getUserGroups(tokenResponse.Token.User.ID, token)
-
-    	if err != nil {
-      		return identity, false, err
-    	}
-
-		identity.Username =	username
-    	identity.UserID = tokenResponse.Token.User.ID
-   	 	identity.Groups = groups
+	if resp.StatusCode == 201 {
+		token := resp.Header.Get("X-Subject-Token")
+		data, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			return identity, false, err
+		}
+		defer resp.Body.Close()
+
+		var tokenResp = new(tokenResponse)
+		err = json.Unmarshal(data, &tokenResp)
+		if err != nil {
+			return identity, false, fmt.Errorf("keystone: invalid token response: %v", err)
+		}
+		groups, err := p.getUserGroups(ctx, tokenResp.Token.User.ID, token)
+		if err != nil {
+			return identity, false, err
+		}
+
+		identity.Username = username
+		identity.UserID = tokenResp.Token.User.ID
+		identity.Groups = groups
 		return identity, true, nil
 
-	} else if err != nil {
-    	fmt.Printf("keystone: error %v", err)
-		return identity, false, err
-
-	} else {
-		data, _ := ioutil.ReadAll(response.Body)
-		fmt.Println(string(data))
-		return identity, false, err
 	}
+
 	return identity, false, nil
 }
 
-func (p Connector) Prompt() string { return "username" }
+func (p *keystoneConnector) Prompt() string { return "username" }
 
-func (p Connector) Refresh(
+func (p *keystoneConnector) Refresh(
 	ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
 
-  	if len(identity.ConnectorData) == 0 {
-  		return identity, nil
+	token, err := p.getAdminToken(ctx)
+	if err != nil {
+		return identity, fmt.Errorf("keystone: failed to obtain admin token: %v", err)
 	}
 
-	token, err := p.getAdminToken()
-
-  	if err != nil {
-    	fmt.Printf("keystone: failed to obtain admin token")
-    	return identity, err
-  	}
-
-  	ok := p.checkIfUserExists(identity.UserID, token)
-  	if !ok {
-  		fmt.Printf("keystone: user %q does not exist\n", identity.UserID)
-     	return identity, fmt.Errorf("keystone: user %q does not exist", identity.UserID)
-  	}
+	ok, err := p.checkIfUserExists(ctx, identity.UserID, token)
+	if err != nil {
+		return identity, err
+	}
+	if !ok {
+		return identity, fmt.Errorf("keystone: user %q does not exist", identity.UserID)
+	}
 
-  	groups, err := p.getUserGroups(identity.UserID, token)
-  	if err != nil {
-    	fmt.Printf("keystone: Failed to fetch user %q groups", identity.UserID)
-    	return identity, fmt.Errorf("keystone: failed to fetch user %q groups", identity.UserID)
-  	}
+	groups, err := p.getUserGroups(ctx, identity.UserID, token)
+	if err != nil {
+		return identity, err
+	}
 
-  	identity.Groups = groups
-  	fmt.Printf("Identity data after use of refresh token: %v", identity)
+	identity.Groups = groups
 	return identity, nil
 }
 
-
-func (p Connector) getTokenResponse(username, password string) (response *http.Response, err error) {
-	jsonData := LoginRequestData{
-		Auth: Auth{
-			Identity: Identity{
-				Methods:[]string{"password"},
-				Password: Password{
-					User: User{
-						Name: username,
-						Domain: Domain{ID:p.Domain},
-						Password: password,
+func (p *keystoneConnector) getTokenResponse(ctx context.Context, username, pass string) (response *http.Response, err error) {
+	client := &http.Client{}
+	jsonData := loginRequestData{
+		auth: auth{
+			Identity: identity{
+				Methods: []string{"password"},
+				Password: password{
+					User: user{
+						Name:     username,
+						Domain:   domain{ID: p.Domain},
+						Password: pass,
 					},
 				},
 			},
 		},
 	}
-	jsonValue, _ := json.Marshal(jsonData)
-  	loginURI := p.KeystoneHost + "/v3/auth/tokens"
-	return http.Post(loginURI, "application/json", bytes.NewBuffer(jsonValue))
+	jsonValue, err := json.Marshal(jsonData)
+	if err != nil {
+		return nil, err
+	}
+
+	authTokenURL := p.KeystoneHost + "/v3/auth/tokens/"
+	req, err := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(jsonValue))
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	req = req.WithContext(ctx)
+
+	return client.Do(req)
 }
 
-func (p Connector) getAdminToken()(string, error) {
-  	response, err := p.getTokenResponse(p.KeystoneUsername, p.KeystonePassword)
-  	if err!= nil {
-    	return "", err
-  	}
-  	token := response.Header["X-Subject-Token"][0]
-  	return token, nil
+func (p *keystoneConnector) getAdminToken(ctx context.Context) (string, error) {
+	resp, err := p.getTokenResponse(ctx, p.KeystoneUsername, p.KeystonePassword)
+	if err != nil {
+		return "", err
+	}
+	token := resp.Header.Get("X-Subject-Token")
+	return token, nil
 }
 
-func (p Connector) checkIfUserExists(userID string, token string) (bool) {
-  	groupsURI := p.KeystoneHost + "/v3/users/" + userID
-  	client := &http.Client{}
-  	req, _ := http.NewRequest("GET", groupsURI, nil)
-  	req.Header.Set("X-Auth-Token", token)
-  	response, err :=  client.Do(req)
-  	if err == nil && response.StatusCode == 200 {
-    	return true
-  	}
-  	return false
+func (p *keystoneConnector) checkIfUserExists(ctx context.Context, userID string, token string) (bool, error) {
+	userURL := p.KeystoneHost + "/v3/users/" + userID
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", userURL, nil)
+	if err != nil {
+		return false, err
+	}
+
+	req.Header.Set("X-Auth-Token", token)
+	req = req.WithContext(ctx)
+	resp, err := client.Do(req)
+	if err != nil {
+		return false, err
+	}
+
+	if resp.StatusCode == 200 {
+		return true, nil
+	}
+	return false, err
 }
 
-func (p Connector) getUserGroups(userID string, token string) ([]string, error) {
-  	groupsURI := p.KeystoneHost + "/v3/users/" + userID + "/groups"
-  	client := &http.Client{}
-  	req, _ := http.NewRequest("GET", groupsURI, nil)
-  	req.Header.Set("X-Auth-Token", token)
-  	response, err :=  client.Do(req)
-
-  	if err != nil {
-    	fmt.Printf("keystone: error while fetching user %q groups\n", userID)
-    	return nil, err
-  	}
-  	data, _ := ioutil.ReadAll(response.Body)
-  	var groupsResponse = new(GroupsResponse)
-  	err = json.Unmarshal(data, &groupsResponse)
-  	if err != nil {
-    	return nil, err
-  	}
-  	groups := []string{}
-  	for _, group := range groupsResponse.Groups {
-  		groups = append(groups, group.Name)
-  	}
-  	return groups, nil
+func (p *keystoneConnector) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) {
+	client := &http.Client{}
+	groupsURL := p.KeystoneHost + "/v3/users/" + userID + "/groups"
+
+	req, err := http.NewRequest("GET", groupsURL, nil)
+	req.Header.Set("X-Auth-Token", token)
+	req = req.WithContext(ctx)
+	resp, err := client.Do(req)
+	if err != nil {
+		p.Logger.Errorf("keystone: error while fetching user %q groups\n", userID)
+		return nil, err
+	}
+
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	var groupsResp = new(groupsResponse)
+
+	err = json.Unmarshal(data, &groupsResp)
+	if err != nil {
+		return nil, err
+	}
+
+	groups := make([]string, len(groupsResp.Groups))
+	for i, group := range groupsResp.Groups {
+		groups[i] = group.Name
+	}
+	return groups, nil
 }
diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go
index b8bba0fe..0c40888e 100644
--- a/connector/keystone/keystone_test.go
+++ b/connector/keystone/keystone_test.go
@@ -1,275 +1,358 @@
 package keystone
 
 import (
-	"testing"
-	"github.com/dexidp/dex/connector"
-
-	"fmt"
-	"io"
-	"os"
-  	"time"
-  	"net/http"
-
-	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/client"
-   	networktypes "github.com/docker/docker/api/types/network"
-  	"github.com/docker/go-connections/nat"
-	"golang.org/x/net/context"
 	"bytes"
+	"context"
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
-)
-
-const dockerCliVersion = "1.37"
-
-const exposedKeystonePort = "5000"
-const exposedKeystonePortAdmin = "35357"
-
-const keystoneHost = "http://localhost"
-const keystoneURL = keystoneHost + ":" + exposedKeystonePort
-const keystoneAdminURL = keystoneHost + ":" + exposedKeystonePortAdmin
-const authTokenURL = keystoneURL + "/v3/auth/tokens/"
-const userURL = keystoneAdminURL + "/v3/users/"
-const groupURL = keystoneAdminURL + "/v3/groups/"
-
-func startKeystoneContainer() string {
-	ctx := context.Background()
-	cli, err := client.NewClientWithOpts(client.WithVersion(dockerCliVersion))
+	"net/http"
+	"os"
+	"reflect"
+	"strings"
+	"testing"
 
-	if err != nil {
-    	fmt.Printf("Error %v", err)
-		return ""
-	}
+	"github.com/dexidp/dex/connector"
+)
 
-	imageName := "openio/openstack-keystone"
-	out, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
-	if err != nil {
-    	fmt.Printf("Error %v", err)
-		return ""
-	}
-	io.Copy(os.Stdout, out)
-
-	resp, err := cli.ContainerCreate(ctx, &container.Config{
-		Image: imageName,
-    }, &container.HostConfig{
-    		PortBindings: nat.PortMap{
-        		"5000/tcp": []nat.PortBinding{
-            		{
-                		HostIP:   "0.0.0.0",
-                		HostPort: exposedKeystonePort,
-            		},
-        		},
-				"35357/tcp": []nat.PortBinding{
-					{
-					HostIP:   "0.0.0.0",
-					HostPort: exposedKeystonePortAdmin,
-					},
-				},
-    		},
-		}, &networktypes.NetworkingConfig{}, "dex_keystone_test")
+const (
+	adminUser   = "demo"
+	adminPass   = "DEMO_PASS"
+	invalidPass = "WRONG_PASS"
 
-	if err != nil {
-    	fmt.Printf("Error %v", err)
-		return ""
-	}
+	testUser   = "test_user"
+	testPass   = "test_pass"
+	testEmail  = "test@example.com"
+	testGroup  = "test_group"
+	testDomain = "default"
+)
 
-	if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
-		panic(err)
-	}
+var (
+	keystoneURL      = ""
+	keystoneAdminURL = ""
+	authTokenURL     = ""
+	usersURL         = ""
+	groupsURL        = ""
+)
 
-	fmt.Println(resp.ID)
-  	return resp.ID
+type userResponse struct {
+	User struct {
+		ID string `json:"id"`
+	} `json:"user"`
 }
 
-func cleanKeystoneContainer(ID string) {
-	ctx := context.Background()
-	cli, err := client.NewClientWithOpts(client.WithVersion(dockerCliVersion))
-	if err != nil {
-		fmt.Printf("Error %v", err)
-		return
-	}
-	duration := time.Duration(1)
-	if err:= cli.ContainerStop(ctx, ID, &duration); err != nil {
-		fmt.Printf("Error %v", err)
-		return
-	}
-	if err:= cli.ContainerRemove(ctx, ID, types.ContainerRemoveOptions{}); err != nil {
-		fmt.Printf("Error %v", err)
-	}
+type groupResponse struct {
+	Group struct {
+		ID string `json:"id"`
+	} `json:"group"`
 }
 
-func getAdminToken(admin_name, admin_pass string) (token string) {
+func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) {
+	t.Helper()
 	client := &http.Client{}
 
-	jsonData := LoginRequestData{
-		Auth: Auth{
-			Identity: Identity{
-				Methods:[]string{"password"},
-				Password: Password{
-					User: User{
-						Name: admin_name,
-						Domain: Domain{ID: "default"},
-						Password: admin_pass,
+	jsonData := loginRequestData{
+		auth: auth{
+			Identity: identity{
+				Methods: []string{"password"},
+				Password: password{
+					User: user{
+						Name:     adminName,
+						Domain:   domain{ID: testDomain},
+						Password: adminPass,
 					},
 				},
 			},
 		},
 	}
 
-	body, _ := json.Marshal(jsonData)
+	body, err := json.Marshal(jsonData)
+	if err != nil {
+		t.Fatal(err)
+	}
 
-	req, _ := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(body))
+	req, err := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(body))
+	if err != nil {
+		t.Fatalf("keystone: failed to obtain admin token: %v\n", err)
+	}
 
 	req.Header.Set("Content-Type", "application/json")
-	resp, _ := client.Do(req)
+	resp, err := client.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
 
-	token = resp.Header["X-Subject-Token"][0]
-	return token
+	token = resp.Header.Get("X-Subject-Token")
+
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+
+	var tokenResp = new(tokenResponse)
+	err = json.Unmarshal(data, &tokenResp)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return token, tokenResp.Token.User.ID
 }
 
-func createUser(token, user_name, user_email, user_pass string) (string){
+func createUser(t *testing.T, token, userName, userEmail, userPass string) string {
+	t.Helper()
 	client := &http.Client{}
 
-	createUserData := CreateUserRequest{
-		CreateUser: CreateUserForm{
-			Name: user_name,
-			Email: user_email,
-			Enabled: true,
-			Password: user_pass,
-			Roles: []string{"admin"},
+	createUserData := map[string]interface{}{
+		"user": map[string]interface{}{
+			"name":     userName,
+			"email":    userEmail,
+			"enabled":  true,
+			"password": userPass,
+			"roles":    []string{"admin"},
 		},
 	}
 
-	body, _ := json.Marshal(createUserData)
+	body, err := json.Marshal(createUserData)
+	if err != nil {
+		t.Fatal(err)
+	}
 
-	req, _ := http.NewRequest("POST", userURL, bytes.NewBuffer(body))
+	req, err := http.NewRequest("POST", usersURL, bytes.NewBuffer(body))
+	if err != nil {
+		t.Fatal(err)
+	}
 	req.Header.Set("X-Auth-Token", token)
 	req.Header.Add("Content-Type", "application/json")
-	resp, _ := client.Do(req)
+	resp, err := client.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
 
-	data, _ := ioutil.ReadAll(resp.Body)
-	var userResponse = new(UserResponse)
-	err := json.Unmarshal(data, &userResponse)
+	data, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
-		fmt.Println(err)
+		t.Fatal(err)
 	}
+	defer resp.Body.Close()
 
-	fmt.Println(userResponse.User.ID)
-	return userResponse.User.ID
+	var userResp = new(userResponse)
+	err = json.Unmarshal(data, &userResp)
+	if err != nil {
+		t.Fatal(err)
+	}
 
+	return userResp.User.ID
 }
 
-func deleteUser(token, id string) {
+// delete group or user
+func delete(t *testing.T, token, id, uri string) {
+	t.Helper()
 	client := &http.Client{}
 
-	deleteUserURI := userURL + id
-	fmt.Println(deleteUserURI)
-	req, _ := http.NewRequest("DELETE", deleteUserURI, nil)
+	deleteURI := uri + id
+	req, err := http.NewRequest("DELETE", deleteURI, nil)
+	if err != nil {
+		t.Fatalf("error: %v", err)
+	}
 	req.Header.Set("X-Auth-Token", token)
-	resp, _ := client.Do(req)
-	fmt.Println(resp)
+	client.Do(req)
 }
 
-func createGroup(token, description, name string) string{
+func createGroup(t *testing.T, token, description, name string) string {
+	t.Helper()
 	client := &http.Client{}
 
-	createGroupData := CreateGroup{
-		CreateGroupForm{
-			Description: description,
-			Name: name,
+	createGroupData := map[string]interface{}{
+		"group": map[string]interface{}{
+			"name":        name,
+			"description": description,
 		},
 	}
 
-	body, _ := json.Marshal(createGroupData)
+	body, err := json.Marshal(createGroupData)
+	if err != nil {
+		t.Fatal(err)
+	}
 
-	req, _ := http.NewRequest("POST", groupURL, bytes.NewBuffer(body))
+	req, err := http.NewRequest("POST", groupsURL, bytes.NewBuffer(body))
+	if err != nil {
+		t.Fatal(err)
+	}
 	req.Header.Set("X-Auth-Token", token)
 	req.Header.Add("Content-Type", "application/json")
-	resp, _ := client.Do(req)
-	data, _ := ioutil.ReadAll(resp.Body)
+	resp, err := client.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
 
-	var groupResponse = new(GroupID)
-	err := json.Unmarshal(data, &groupResponse)
+	var groupResp = new(groupResponse)
+	err = json.Unmarshal(data, &groupResp)
 	if err != nil {
-		fmt.Println(err)
+		t.Fatal(err)
 	}
 
-	return groupResponse.Group.ID
+	return groupResp.Group.ID
 }
 
-func addUserToGroup(token, groupId, userId string) {
-	uri := groupURL + groupId + "/users/" + userId
+func addUserToGroup(t *testing.T, token, groupID, userID string) error {
+	t.Helper()
+	uri := groupsURL + groupID + "/users/" + userID
 	client := &http.Client{}
-	req, _ := http.NewRequest("PUT", uri, nil)
+	req, err := http.NewRequest("PUT", uri, nil)
+	if err != nil {
+		return err
+	}
 	req.Header.Set("X-Auth-Token", token)
-	resp, _ := client.Do(req)
-	fmt.Println(resp)
+	client.Do(req)
+	return nil
 }
 
-const adminUser = "demo"
-const adminPass = "DEMO_PASS"
-const invalidPass = "WRONG_PASS"
-
-const testUser = "test_user"
-const testPass = "test_pass"
-const testEmail = "test@example.com"
-
-const domain = "default"
-
 func TestIncorrectCredentialsLogin(t *testing.T) {
-  	c := Connector{KeystoneHost: keystoneURL, Domain: domain,
-  				   KeystoneUsername: adminUser, KeystonePassword: adminPass}
-  	s := connector.Scopes{OfflineAccess: true, Groups: true}
-  	_, validPW, _ := c.Login(context.Background(), s, adminUser, invalidPass)
-
-  	if validPW {
-  		t.Fail()
-  	}
+	c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain,
+		KeystoneUsername: adminUser, KeystonePassword: adminPass}
+	s := connector.Scopes{OfflineAccess: true, Groups: true}
+	_, validPW, err := c.Login(context.Background(), s, adminUser, invalidPass)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	if validPW {
+		t.Fail()
+	}
 }
 
 func TestValidUserLogin(t *testing.T) {
-	token := getAdminToken(adminUser, adminPass)
-	userID := createUser(token, testUser, testEmail, testPass)
-  	c := Connector{KeystoneHost: keystoneURL, Domain: domain,
-  				  KeystoneUsername: adminUser, KeystonePassword: adminPass}
-  	s := connector.Scopes{OfflineAccess: true, Groups: true}
-  	_, validPW, _ := c.Login(context.Background(), s, testUser, testPass)
-  	if !validPW {
-     	t.Fail()
-  	}
-  	deleteUser(token, userID)
+	token, _ := getAdminToken(t, adminUser, adminPass)
+	userID := createUser(t, token, testUser, testEmail, testPass)
+	c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain,
+		KeystoneUsername: adminUser, KeystonePassword: adminPass}
+	s := connector.Scopes{OfflineAccess: true, Groups: true}
+	identity, validPW, err := c.Login(context.Background(), s, testUser, testPass)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+	t.Log(identity)
+
+	if !validPW {
+		t.Fail()
+	}
+	delete(t, token, userID, usersURL)
 }
 
 func TestUseRefreshToken(t *testing.T) {
-  t.Fatal("Not implemented")
+	token, adminID := getAdminToken(t, adminUser, adminPass)
+	groupID := createGroup(t, token, "Test group description", testGroup)
+	addUserToGroup(t, token, groupID, adminID)
+
+	c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain,
+		KeystoneUsername: adminUser, KeystonePassword: adminPass}
+	s := connector.Scopes{OfflineAccess: true, Groups: true}
+
+	identityLogin, _, err := c.Login(context.Background(), s, adminUser, adminPass)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	identityRefresh, err := c.Refresh(context.Background(), s, identityLogin)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	delete(t, token, groupID, groupsURL)
+
+	expectEquals(t, 1, len(identityRefresh.Groups))
+	expectEquals(t, testGroup, string(identityRefresh.Groups[0]))
 }
 
-func TestUseRefreshTokenUserDeleted(t *testing.T){
-  t.Fatal("Not implemented")
+func TestUseRefreshTokenUserDeleted(t *testing.T) {
+	token, _ := getAdminToken(t, adminUser, adminPass)
+	userID := createUser(t, token, testUser, testEmail, testPass)
+
+	c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain,
+		KeystoneUsername: adminUser, KeystonePassword: adminPass}
+	s := connector.Scopes{OfflineAccess: true, Groups: true}
+
+	identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	_, err = c.Refresh(context.Background(), s, identityLogin)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	delete(t, token, userID, usersURL)
+	_, err = c.Refresh(context.Background(), s, identityLogin)
+
+	if !strings.Contains(err.Error(), "does not exist") {
+		t.Errorf("unexpected error: %s", err.Error())
+	}
 }
 
-func TestUseRefreshTokenGroupsChanged(t *testing.T){
-	t.Fatal("Not implemented")
+func TestUseRefreshTokenGroupsChanged(t *testing.T) {
+	token, _ := getAdminToken(t, adminUser, adminPass)
+	userID := createUser(t, token, testUser, testEmail, testPass)
+
+	c := keystoneConnector{KeystoneHost: keystoneURL, Domain: testDomain,
+		KeystoneUsername: adminUser, KeystonePassword: adminPass}
+	s := connector.Scopes{OfflineAccess: true, Groups: true}
+
+	identityLogin, _, err := c.Login(context.Background(), s, testUser, testPass)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	identityRefresh, err := c.Refresh(context.Background(), s, identityLogin)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	expectEquals(t, 0, len(identityRefresh.Groups))
+
+	groupID := createGroup(t, token, "Test group description", testGroup)
+	addUserToGroup(t, token, groupID, userID)
+
+	identityRefresh, err = c.Refresh(context.Background(), s, identityLogin)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	delete(t, token, groupID, groupsURL)
+	delete(t, token, userID, usersURL)
+
+	expectEquals(t, 1, len(identityRefresh.Groups))
 }
 
 func TestMain(m *testing.M) {
-	dockerID := startKeystoneContainer()
-  	repeats := 10
-  	running := false
-  	for i := 0; i < repeats; i++ {
-   		_, err := http.Get(keystoneURL)
-   		if err == nil {
-     		running = true
-     		break
-   		}
-   		time.Sleep(10 * time.Second)
-  	}
-  	if !running {
-    	fmt.Printf("Failed to start keystone container")
-    	os.Exit(1)
-  	}
-  	defer cleanKeystoneContainer(dockerID)
-  	// run all tests
+	keystoneURLEnv := "DEX_KEYSTONE_URL"
+	keystoneAdminURLEnv := "DEX_KEYSTONE_ADMIN_URL"
+	keystoneURL = os.Getenv(keystoneURLEnv)
+	if keystoneURL == "" {
+		fmt.Printf("variable %q not set, skipping keystone connector tests\n", keystoneURLEnv)
+		return
+	}
+	keystoneAdminURL := os.Getenv(keystoneAdminURLEnv)
+	if keystoneAdminURL == "" {
+		fmt.Printf("variable %q not set, skipping keystone connector tests\n", keystoneAdminURLEnv)
+		return
+	}
+	authTokenURL = keystoneURL + "/v3/auth/tokens/"
+	fmt.Printf("Auth token url %q\n", authTokenURL)
+	fmt.Printf("Keystone URL %q\n", keystoneURL)
+	usersURL = keystoneAdminURL + "/v3/users/"
+	groupsURL = keystoneAdminURL + "/v3/groups/"
+	// run all tests
 	m.Run()
 }
+
+func expectEquals(t *testing.T, a interface{}, b interface{}) {
+	if !reflect.DeepEqual(a, b) {
+		t.Errorf("Expected %v to be equal %v", a, b)
+	}
+}
diff --git a/connector/keystone/types.go b/connector/keystone/types.go
index 9868a815..fe6b67ae 100644
--- a/connector/keystone/types.go
+++ b/connector/keystone/types.go
@@ -4,133 +4,84 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-type Connector struct {
-	Domain 			 string
-	KeystoneHost 	 string
+type keystoneConnector struct {
+	Domain           string
+	KeystoneHost     string
 	KeystoneUsername string
 	KeystonePassword string
-	Logger 			 logrus.FieldLogger
+	Logger           logrus.FieldLogger
 }
 
-type ConnectorData struct {
-	AccessToken string `json:"accessToken"`
+type userKeystone struct {
+	Domain domainKeystone `json:"domain"`
+	ID     string         `json:"id"`
+	Name   string         `json:"name"`
 }
 
-type KeystoneUser struct {
-	Domain KeystoneDomain `json:"domain"`
-	ID 	   string 		  `json:"id"`
-	Name   string 		  `json:"name"`
-}
-
-type KeystoneDomain struct {
-	ID string   `json:"id"`
+type domainKeystone struct {
+	ID   string `json:"id"`
 	Name string `json:"name"`
 }
 
+// Config holds the configuration parameters for Keystone connector.
+// Keystone should expose API v3
+// An example config:
+//	connectors:
+//		type: keystone
+//		id: keystone
+//		name: Keystone
+//		config:
+//			keystoneHost: http://example:5000
+//			domain: default
+//      keystoneUsername: demo
+//      keystonePassword: DEMO_PASS
 type Config struct {
-	Domain 			 string `json:"domain"`
-	KeystoneHost 	 string `json:"keystoneHost"`
+	Domain           string `json:"domain"`
+	KeystoneHost     string `json:"keystoneHost"`
 	KeystoneUsername string `json:"keystoneUsername"`
 	KeystonePassword string `json:"keystonePassword"`
 }
 
-type LoginRequestData struct {
-	Auth `json:"auth"`
+type loginRequestData struct {
+	auth `json:"auth"`
 }
 
-type Auth struct {
-	Identity `json:"identity"`
+type auth struct {
+	Identity identity `json:"identity"`
 }
 
-type Identity struct {
+type identity struct {
 	Methods  []string `json:"methods"`
-	Password 		  `json:"password"`
+	Password password `json:"password"`
 }
 
-type Password struct {
-	User `json:"user"`
+type password struct {
+	User user `json:"user"`
 }
 
-type User struct {
-	Name   string 	`json:"name"`
-	Domain 			`json:"domain"`
-	Password string `json:"password"`
-}
-
-type Domain struct {
-	ID string `json:"id"`
-}
-
-type Token struct {
-	IssuedAt  string 	   			 `json:"issued_at"`
-	Extras 	  map[string]interface{} `json:"extras"`
-	Methods   []string 	   			 `json:"methods"`
-	ExpiresAt string 	   			 `json:"expires_at"`
-	User 	  KeystoneUser 			 `json:"user"`
-}
-
-type TokenResponse struct {
-	Token Token `json:"token"`
-}
-
-type CreateUserRequest struct {
-	CreateUser CreateUserForm  `json:"user"`
-}
-
-type CreateUserForm struct {
+type user struct {
 	Name     string `json:"name"`
-	Email    string `json:"email"`
-	Enabled  bool   `json:"enabled"`
+	Domain   domain `json:"domain"`
 	Password string `json:"password"`
-	Roles  []string `json:"roles"`
-}
-
-type UserResponse struct {
-	User CreateUserResponse `json:"user"`
-}
-
-type CreateUserResponse struct {
-	Username string   `json:"username"`
-	Name 	 string   `json:"name"`
-	Roles 	 []string `json:"roles"`
-	Enabled  bool     `json:"enabled"`
-	Options  string   `json:"options"`
-	ID 		 string   `json:"id"`
-	Email 	 string   `json:"email"`
-}
-
-type CreateGroup struct {
-	Group CreateGroupForm `json:"group"`
-}
-
-type CreateGroupForm struct {
-	Description string `json:"description"`
-	Name 		string `json:"name"`
 }
 
-type GroupID struct {
-	Group GroupIDForm `json:"group"`
+type domain struct {
+	ID string `json:"id"`
 }
 
-type GroupIDForm struct {
-	ID string `json:"id"`
+type token struct {
+	User userKeystone `json:"user"`
 }
 
-type Links struct {
-	Self string `json:"self"`
-	Previous string `json:"previous"`
-	Next string `json:"next"`
+type tokenResponse struct {
+	Token token `json:"token"`
 }
 
-type Group struct {
-	DomainID 	string `json:"domain_id`
-	Description string `json:"description"`
-	ID 			string `json:"id"`
-	Links 		Links  `json:"links"`
-	Name 		string `json:"name"`
+type group struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
 }
 
-type GroupsResponse struct {
-	Links  Links   `json:"links"`
-	Groups []Group `json:"groups"`
+type groupsResponse struct {
+	Groups []group `json:"groups"`
 }
diff --git a/examples/config-keystone.yaml b/examples/config-keystone.yaml
deleted file mode 100644
index 9d5dfdf7..00000000
--- a/examples/config-keystone.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-# The base path of dex and the external name of the OpenID Connect service.
-# This is the canonical URL that all clients MUST use to refer to dex. If a
-# path is provided, dex's HTTP service will listen at a non-root URL.
-issuer: http://0.0.0.0:5556/dex
-
-# The storage configuration determines where dex stores its state. Supported
-# options include SQL flavors and Kubernetes third party resources.
-#
-# See the storage document at Documentation/storage.md for further information.
-storage:
-  type: sqlite3
-  config:
-    file: examples/dex.db   #be in the dex directory, else change path here
-
-# Configuration for the HTTP endpoints.
-web:
-  https: 0.0.0.0:5556
-  # Uncomment for HTTPS options.
-  # https: 127.0.0.1:5554
-  tlsCert: ./ssl/dex.crt
-  tlsKey: ./ssl/dex.key
-
-# Configuration for telemetry
-telemetry:
-  http: 0.0.0.0:5558
-
-oauth2:
-  responseTypes: ["id_token"]
-
-# Instead of reading from an external storage, use this list of clients.
-staticClients:
-- id: example-app
-  redirectURIs:
-  - 'http://127.0.0.1:5555/callback'
-  name: 'Example App'
-  secret: ZXhhbXBsZS1hcHAtc2VjcmV0
-
-#Provide Keystone connector and its config here
-# /v3/auth/tokens
-connectors:
-- type: keystone
-  id: keystone
-  name: Keystone
-  config:
-    keystoneHost: http://localhost:5000
-    domain: default
-    keystoneUsername: demo
-    keystonePassword: DEMO_PASS
-
-# Let dex keep a list of passwords which can be used to login to dex.
-enablePasswordDB: true
-
-oauth2:
-  skipApprovalScreen: true
-
diff --git a/server/handlers.go b/server/handlers.go
index 100f5a38..5bdf39f0 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -211,7 +211,6 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 	}
 
 	authReqID := r.FormValue("req")
-  s.logger.Errorf("Auth req id %v", authReqID)
 
 	authReq, err := s.storage.GetAuthRequest(authReqID)
 	if err != nil {
@@ -346,7 +345,7 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 			s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
 			return
 		}
-		s.logger.Errorf("2Failed to get auth request: %v", err)
+		s.logger.Errorf("Failed to get auth request: %v", err)
 		s.renderError(w, http.StatusInternalServerError, "Database error.")
 		return
 	}
@@ -358,7 +357,6 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	}
 
 	conn, err := s.getConnector(authReq.ConnectorID)
-  s.logger.Errorf("X Connector %v", conn)
 	if err != nil {
 		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
 		s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
@@ -437,7 +435,7 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
 func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 	authReq, err := s.storage.GetAuthRequest(r.FormValue("req"))
 	if err != nil {
-		s.logger.Errorf("3Failed to get auth request: %v", err)
+		s.logger.Errorf("Failed to get auth request: %v", err)
 		s.renderError(w, http.StatusInternalServerError, "Database error.")
 		return
 	}
diff --git a/server/server.go b/server/server.go
index 518200cd..ee3355b5 100644
--- a/server/server.go
+++ b/server/server.go
@@ -27,6 +27,7 @@ import (
 	"github.com/dexidp/dex/connector/bitbucketcloud"
 	"github.com/dexidp/dex/connector/github"
 	"github.com/dexidp/dex/connector/gitlab"
+	"github.com/dexidp/dex/connector/keystone"
 	"github.com/dexidp/dex/connector/ldap"
 	"github.com/dexidp/dex/connector/linkedin"
 	"github.com/dexidp/dex/connector/microsoft"
@@ -34,7 +35,6 @@ import (
 	"github.com/dexidp/dex/connector/oidc"
 	"github.com/dexidp/dex/connector/saml"
 	"github.com/dexidp/dex/storage"
-  "github.com/dexidp/dex/connector/keystone"
 )
 
 // LocalConnector is the local passwordDB connector which is an internal
@@ -456,7 +456,7 @@ func openConnector(logger logrus.FieldLogger, conn storage.Connector) (connector
 
 	f, ok := ConnectorsConfig[conn.Type]
 	if !ok {
-		return c, fmt.Errorf("xunknown connector type %q", conn.Type)
+		return c, fmt.Errorf("unknown connector type %q", conn.Type)
 	}
 
 	connConfig := f()
diff --git a/storage/static.go b/storage/static.go
index abf0ab7f..5ae4f783 100644
--- a/storage/static.go
+++ b/storage/static.go
@@ -3,6 +3,7 @@ package storage
 import (
 	"errors"
 	"strings"
+
 	"github.com/sirupsen/logrus"
 )
 
-- 
GitLab