Repository: Nerzal/gocloak (go)

Issue:
Implement the "remove a specific user session" api
**Is your feature request related to a problem? Please describe.**
It would be nice to have the `DELETE /{realm}/sessions/{session} ` call implemented so we could remove a specific user session. This is to circumvent the lack of session limiting in keycloak (https://issues.redhat.com/browse/KEYCLOAK-849)

**Describe the solution you'd like**
Implement the API call.

**Describe alternatives you've considered**
There's none as I cannot kill all sessions if I don't have an access token. On login, if I want to guarantee single session per user, I would have to:

1. login
2. kill all session with my new token
3. login again 

**Additional context**
N/A


Pull request:
implement DELETE /{realm}/sessions/{session}
This fixes #193 

Patch:
diff --git a/README.md b/README.md
index d86e453..4a3002f 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,7 @@ type GoCloak interface {
 	Logout(ctx context.Context, clientID, clientSecret, realm, refreshToken string) error
 	LogoutPublicClient(ctx context.Context, clientID, realm, accessToken, refreshToken string) error
 	LogoutAllSessions(ctx context.Context, accessToken, realm, userID string) error
+	LogoutUserSession(ctx context.Context, accessToken, realm, session string) error
 	LoginClient(ctx context.Context, clientID, clientSecret, realm string) (*JWT, error)
 	LoginClientSignedJWT(ctx context.Context, clientID, realm string, key interface{}, signedMethod jwt.SigningMethod, expiresAt int64) (*JWT, error)
 	LoginAdmin(ctx context.Context, username, password, realm string) (*JWT, error)
diff --git a/client.go b/client.go
index 75994d2..799e078 100644
--- a/client.go
+++ b/client.go
@@ -484,6 +484,16 @@ func (client *gocloak) LogoutAllSessions(ctx context.Context, accessToken, realm
 	return checkForError(resp, err, errMessage)
 }
 
+// LogoutUserSessions logs out a single sessions of a user given a session id
+func (client *gocloak) LogoutUserSession(ctx context.Context, accessToken, realm, session string) error {
+	const errMessage = "could not logout"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, accessToken).
+		Delete(client.getAdminRealmURL(realm, "sessions", session))
+
+	return checkForError(resp, err, errMessage)
+}
+
 // ExecuteActionsEmail executes an actions email
 func (client *gocloak) ExecuteActionsEmail(ctx context.Context, token, realm string, params ExecuteActionsEmail) error {
 	const errMessage = "could not execute actions email"
diff --git a/gocloak.go b/gocloak.go
index 97a4ffe..b9d6d01 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -28,6 +28,9 @@ type GoCloak interface {
 	LogoutPublicClient(ctx context.Context, clientID, realm, accessToken, refreshToken string) error
 	// LogoutAllSessions logs out all sessions of a user given an id
 	LogoutAllSessions(ctx context.Context, accessToken, realm, userID string) error
+	// LogoutUserSessions logs out a single sessions of a user given a session id.
+	// NOTE: this uses bearer token, but this token must belong to a user with proper privileges
+	LogoutUserSession(ctx context.Context, accessToken, realm, session string) error
 	// LoginClient sends a request to the token endpoint using client credentials
 	LoginClient(ctx context.Context, clientID, clientSecret, realm string) (*JWT, error)
 	// LoginClientSignedJWT performs a login with client credentials and signed jwt claims


Test patch:
diff --git a/client_test.go b/client_test.go
index fb5d322..1f253f2 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1699,6 +1699,22 @@ func TestGocloak_LogoutAllSessions(t *testing.T) {
 	require.NoError(t, err, "Logout failed")
 }
 
+func TestGocloak_LogoutUserSession(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	uToken := GetUserToken(t, client)
+	aToken := GetAdminToken(t, client)
+
+	err := client.LogoutUserSession(
+		context.Background(),
+		aToken.AccessToken,
+		cfg.GoCloak.Realm,
+		uToken.SessionState,
+	)
+	require.NoError(t, err, "Logout failed")
+}
+
 func TestGocloak_GetRealm(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)