Repository: Nerzal/gocloak
Pull request #181 (2020-06-22 12:40:04)

Cherry-picking #178 to v5 branch


Patch:
diff --git a/.gitignore b/.gitignore
index 9dfe1c3..fa9acc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@
 *.out
 
 \.idea/
+
+# Compile objects
+/bin/
diff --git a/README.md b/README.md
index 8f7fc09..5e28a64 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,7 @@ type GoCloak interface {
 	GetRequestingPartyToken(token, realm string, options RequestingPartyTokenOptions) (*JWT, error)
 
 	Login(clientID string, clientSecret string, realm string, username string, password string) (*JWT, error)
+	LoginOtp(clientID string, clientSecret string, realm string, username string, password string, totp string) (*JWT, error) 
 	Logout(clientID, clientSecret, realm, refreshToken string) error
 	LogoutPublicClient(clientID, realm, accessToken, refreshToken string) error
 	LoginClient(clientID, clientSecret, realm string) (*JWT, error)
@@ -202,7 +203,7 @@ type GoCloak interface {
 	GetClientRolesByGroupID(token string, realm string, clientID string, groupID string) ([]*Role, error)
 	GetCompositeClientRolesByUserID(token string, realm string, clientID string, userID string) ([]*Role, error)
 	GetCompositeClientRolesByGroupID(token string, realm string, clientID string, groupID string) ([]*Role, error)
-    GetAvailableClientRolesByUserID(token string, realm string, clientID string, userID string) ([]*Role, error)
+	GetAvailableClientRolesByUserID(token string, realm string, clientID string, userID string) ([]*Role, error)
 
 	// *** Realm ***
 
@@ -284,6 +285,12 @@ docker run -d \
 go test
 ```
 
+Or you can run with docker compose using the run-tests script
+```bash
+./run-tests.sh
+```
+
+
 Or you can run the tests on you own keycloak:
 ```bash
 export GOCLOAK_TEST_CONFIG=/path/to/gocloak/config.json
diff --git a/client.go b/client.go
index 72bd02e..b4c9372 100644
--- a/client.go
+++ b/client.go
@@ -384,6 +384,18 @@ func (client *gocloak) Login(clientID, clientSecret, realm, username, password s
 	})
 }
 
+// LoginOtp performs a login with user credentials and otp token
+func (client *gocloak) LoginOtp(clientID, clientSecret, realm, username, password, totp string) (*JWT, error) {
+	return client.GetToken(realm, TokenOptions{
+		ClientID:     &clientID,
+		ClientSecret: &clientSecret,
+		GrantType:    StringP("password"),
+		Username:     &username,
+		Password:     &password,
+		Totp:         &totp,
+	})
+}
+
 // Logout logs out users with refresh token
 func (client *gocloak) Logout(clientID, clientSecret, realm, refreshToken string) error {
 	const errMessage = "could not logout"
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d0b25ac
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,14 @@
+version: '3'
+
+services:
+  keycloak:
+    image: jboss/keycloak:latest
+    command: -Dkeycloak.profile.feature.upload_scripts=enabled
+    environment:
+      KEYCLOAK_IMPORT: /tmp/gocloak-realm.json
+      KEYCLOAK_USER: admin
+      KEYCLOAK_PASSWORD: secret
+    ports:
+      - "8080:8080"
+    volumes:
+      -  ./testdata/gocloak-realm.json:/tmp/gocloak-realm.json
\ No newline at end of file
diff --git a/gocloak.go b/gocloak.go
index 986980c..37db2fd 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -18,6 +18,8 @@ type GoCloak interface {
 	GetRequestingPartyToken(token, realm string, options RequestingPartyTokenOptions) (*JWT, error)
 	// Login sends a request to the token endpoint using user and client credentials
 	Login(clientID, clientSecret, realm, username, password string) (*JWT, error)
+	// LoginOtp performs a login with user credentials and otp token
+	LoginOtp(clientID, clientSecret, realm, username, password, totp string) (*JWT, error)
 	// Logout sends a request to the logout endpoint using refresh token
 	Logout(clientID, clientSecret, realm, refreshToken string) error
 	// LogoutPublicClient sends a request to the logout endpoint using refresh token
diff --git a/models.go b/models.go
index 94a1a5b..5e30415 100644
--- a/models.go
+++ b/models.go
@@ -670,6 +670,7 @@ type TokenOptions struct {
 	Permission    *string  `json:"permission,omitempty"`
 	Username      *string  `json:"username,omitempty"`
 	Password      *string  `json:"password,omitempty"`
+	Totp          *string  `json:"totp,omitempty"`
 }
 
 // FormData returns a map of options to be used in SetFormData function


Test patch:
diff --git a/client_test.go b/client_test.go
index 27198e6..77449e7 100644
--- a/client_test.go
+++ b/client_test.go
@@ -680,6 +680,25 @@ func TestGocloak_Login(t *testing.T) {
 	require.NoError(t, err, "Login failed")
 }
 
+func TestGocloak_LoginOtp(t *testing.T) {
+
+	totp := "123456"
+
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	SetUpTestUser(t, client)
+	_, err := client.LoginOtp(
+		cfg.GoCloak.ClientID,
+		cfg.GoCloak.ClientSecret,
+		cfg.GoCloak.Realm,
+		cfg.GoCloak.UserName,
+		cfg.GoCloak.Password,
+		totp,
+	)
+	require.NoError(t, err, "Login failed")
+}
+
 func TestGocloak_GetToken(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 0000000..92d56d6
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+docker-compose down
+docker-compose up -d
+
+sleep 10
+
+mkdir -p bin
+go test -failfast -race -cover -coverprofile=bin/coverage.txt -covermode=atomic -cpu 1,2 -bench . -benchmem
+
+docker-compose down
\ No newline at end of file