diff --git a/client/cmdToken/tokenRefresh.go b/client/cmdToken/tokenRefresh.go
index 2fb9e5901238d2c756de98d8d3c240f641d9c2d4..107f05e4909e36aa38fec8f6887622d51913bdc1 100644
--- a/client/cmdToken/tokenRefresh.go
+++ b/client/cmdToken/tokenRefresh.go
@@ -34,7 +34,7 @@ func (cmd *Refresh) Run(nc *nc.NexusClient, args []string) int {
 		return 1
 	}
 
-	err := nc.RefreshAuthToken()
+	err := nc.RefreshToken()
 	if err != nil {
 		u.PrintlnErr(err)
 		return 1
diff --git a/client/nexus-cli/nexus-cli.go b/client/nexus-cli/nexus-cli.go
index 9c5437ed964ea33ecdb7481c0f4ff1ab7234b38b..3c33d28b3cd61557a2cec4e5bfa6b5de1cbd3cd1 100644
--- a/client/nexus-cli/nexus-cli.go
+++ b/client/nexus-cli/nexus-cli.go
@@ -70,6 +70,15 @@ var cmdList = []cmd.Command{
 	&cmdVM.StartWithCreds{Name: "vmstartwithcreds"},
 }
 
+var exitFn func()
+
+func exit(code int) {
+	if exitFn != nil {
+		exitFn()
+	}
+	os.Exit(code)
+}
+
 func run() int {
 	var appname = path.Base(os.Args[0])
 	clientVersion := version.Get()
@@ -94,7 +103,11 @@ func run() int {
 		// return 1
 	}
 
-	nc := nc.New(serverEnvVar)
+	nc, err := nc.New(serverEnvVar)
+	if err != nil {
+		u.PrintlnErr("Error: " + err.Error())
+		return 1
+	}
 
 	// Checks the client version is compatible with the server's API.
 	if !cmdVersion.CheckServerCompatibility(nc, appname) {
@@ -106,13 +119,23 @@ func run() int {
 
 	found, cmd := cmd.Match(cmdName, cmdList)
 	if found {
+		exitFn = func() { nc.Close() }
+
+		// This thread acts as a signal handler for SIGINT or SIGTERM.
+		// When one of these signals is received, the temporary certificate file is deleted.
+		// Without this "handler", the temporary certificate file wouldn't be deleted.
+		go func() {
+			u.WaitForSignals()
+			exit(1)
+		}()
+
 		if cmdName != "login" {
 			token, found := os.LookupEnv(g.ENV_NEXUS_TOKEN)
 			if !found || len(token) == 0 {
 				u.PrintlnErr("Environment variable \"" + g.ENV_NEXUS_TOKEN + "\" must be set!")
 				return 1
 			}
-			nc.SetAuthToken(token)
+			nc.AuthenticateWithToken(token)
 		}
 		return cmd.Run(nc, cmdArgs)
 	}
@@ -122,5 +145,5 @@ func run() int {
 }
 
 func main() {
-	os.Exit(run())
+	exit(run())
 }
diff --git a/client/nexush/nexush.go b/client/nexush/nexush.go
index 4b28602bfbcdf34198a8b204c20c9e188006a714..5adbdf96870a64381f9eb6cd138c76043c4a0034 100644
--- a/client/nexush/nexush.go
+++ b/client/nexush/nexush.go
@@ -76,9 +76,17 @@ var cmdList = []cmd.Command{
 	&cmdVM.StartWithCreds{Name: "vmstartwithcreds"},
 }
 
+var exitFn func()
 var prompt *liner.State = nil
 var savedTermState *term.State = nil
 
+func exit(code int) {
+	if exitFn != nil {
+		exitFn()
+	}
+	os.Exit(code)
+}
+
 func run() int {
 	var appname = path.Base(os.Args[0])
 	clientVersion := version.Get()
@@ -130,20 +138,15 @@ func run() int {
 		return 1
 	}
 
-	certFile, err := nc.GetVMAttachCertificate()
-	if err != nil {
-		u.PrintlnErr("Error: " + err.Error())
-		return 1
-	}
+	exitFn = func() { nc.Close() }
 
 	// This thread acts as a signal handler for SIGINT or SIGTERM.
 	// When one of these signals is received, the temporary certificate file is deleted.
 	// Without this "handler", the temporary certificate file wouldn't be deleted.
 	go func() {
 		u.WaitForSignals()
-		os.Remove(certFile)
 		restoreTerm()
-		os.Exit(1)
+		exit(1)
 	}()
 
 	shell(nc)
@@ -211,7 +214,7 @@ Type: "help" for help on commands
 			found, cmd := cmd.Match(typedCmd, cmdList)
 			if found {
 				if typedCmd == "refresh" {
-					err := nc.RefreshAuthToken()
+					err := nc.RefreshToken()
 					if err != nil {
 						u.PrintlnErr("refresh error: " + err.Error())
 						break
@@ -286,5 +289,5 @@ func extractArgs(line string) ([]string, error) {
 }
 
 func main() {
-	os.Exit(run())
+	exit(run())
 }
diff --git a/libclient/client.go b/libclient/client.go
index 8434649ff0b7febf138c82b95b5d9e4144f44a38..adfa41fd04e092b2e64cfeb3a8b57b366ff31275 100644
--- a/libclient/client.go
+++ b/libclient/client.go
@@ -2,15 +2,25 @@ package nexusclient
 
 import (
 	"crypto/tls"
+	"encoding/json"
+	"errors"
 	"net/url"
+	"os"
+	"path/filepath"
 	"time"
 
+	"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/params"
+	"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/response"
+	u "gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/utils"
 	"github.com/go-resty/resty/v2"
+	"github.com/google/uuid"
 )
 
 type NexusClient struct {
 	client   *resty.Client
 	hostname string // host, e.g. "127.0.0.1" or "my.domain.net"
+	certFile string // path to the CA certificate tied to the hostname above
+	// (this cert is required by the spice client)
 }
 
 // The host argument has the following format:
@@ -31,9 +41,61 @@ func New(host string) (*NexusClient, error) {
 		return nil, err
 	}
 	nc.hostname = url.Hostname()
+	nc.certFile = ""
 	return nc, nil
 }
 
+// Authenticate a user.
+// curl --cacert ca-cert.pem -X POST https://localhost:8000/login -H 'Content-Type: application/json' -d '{"email": "johndoe@nexus.org", "pwd":"pipomolo"}'
+// Returns an JWT token if authentication succeeded or an error if it failed.
+// IMPORTANT: caller MUST call the Close() function before exiting!
+func (nc *NexusClient) Authenticate(user, pwd string) (string, error) {
+	loginArgs := &params.Login{user, pwd}
+	resp, err := nc.client.R().SetBody(loginArgs).Post("/login")
+	if err != nil {
+		return "", err
+	}
+
+	if resp.IsSuccess() {
+		var token params.Token
+		err = json.Unmarshal(resp.Body(), &token)
+		if err != nil {
+			return "", err
+		}
+		nc.client.SetAuthToken(token.Token)
+		// Download the CA certificate required by Spice clients
+		nc.certFile, err = nc.downloadVMAttachCert()
+		if err != nil {
+			return "", err
+		}
+		return token.Token, nil
+	} else {
+		return "", response.ErrorToMsg(resp)
+	}
+}
+
+// "Authenticate" by using an already established connection using a previously received token.
+// Useful when wanting to access the API without authenticating.
+// IMPORTANT: caller MUST call the Close() function before exiting!
+func (nc *NexusClient) AuthenticateWithToken(token string) error {
+	nc.client.SetAuthToken(token)
+	// Download the CA certificate required by Spice clients
+	var err error
+	nc.certFile, err = nc.downloadVMAttachCert()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// Releases/deletes resources allocated by Authenticate and AuthenticateWithToken.
+// IMPORTANT: caller MUST call this function before exiting!
+func (nc *NexusClient) Close() {
+	if nc.certFile != "" {
+		os.Remove(nc.certFile)
+	}
+}
+
 // Returns the hostname, e.g. "127.0.0.1" or "my.domain.net".
 func (nc *NexusClient) GetHostname() string {
 	return nc.hostname
@@ -43,3 +105,51 @@ func (nc *NexusClient) GetHostname() string {
 func (nc *NexusClient) SetTimeout(timeout time.Duration) {
 	nc.client.SetTimeout(timeout)
 }
+
+// Obtain a new JWT token from the server.
+func (nc *NexusClient) RefreshToken() error {
+	resp, err := nc.client.R().Get("/misc/token")
+	if err != nil {
+		return err
+	} else {
+		if resp.IsSuccess() {
+			var token params.Token
+			err = json.Unmarshal(resp.Body(), &token)
+			if err != nil {
+				return err
+			}
+			nc.client.SetAuthToken(token.Token)
+			return nil
+		} else {
+			return response.ErrorToMsg(resp)
+		}
+	}
+}
+
+func (nc *NexusClient) getVMAttachCertFile() string {
+	return nc.certFile
+}
+
+// Retrieve and save the certificate required by the various VMAttachXXX methods.
+// This public CA certificate is required by Spice remote viewers.
+// The certificate file is saved in the OS' temporary directory.
+// Its name is randomly generated and garanteed to be unique.
+func (nc *NexusClient) downloadVMAttachCert() (string, error) {
+	uniqPostfix, err := uuid.NewRandom()
+	if err != nil {
+		return "", errors.New("Failed saving CA certificate, code 1:\n" + err.Error())
+	}
+
+	outputFile := filepath.Join(os.TempDir(), "cert_"+uniqPostfix.String()+".pem")
+	resp, err := nc.client.R().SetOutput(outputFile).Get("/misc/cert")
+	if err != nil {
+		return "", errors.New("Failed saving CA certificate, code 2:\n" + err.Error())
+	}
+
+	if resp.IsSuccess() {
+		return outputFile, nil
+	} else {
+		errorMsg, _ := u.FileToString(outputFile)
+		return "", errors.New("Failed saving CA certificate, code 3\n" + resp.Status() + ": " + errorMsg)
+	}
+}
diff --git a/libclient/login.go b/libclient/login.go
deleted file mode 100644
index 21cfe129403240def4b4ccde6cda77c70071365b..0000000000000000000000000000000000000000
--- a/libclient/login.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package nexusclient
-
-import (
-	"encoding/json"
-
-	"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/params"
-	"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/response"
-)
-
-// Authenticate a user.
-// curl --cacert ca-cert.pem -X POST https://localhost:8000/login -H 'Content-Type: application/json' -d '{"email": "johndoe@nexus.org", "pwd":"pipomolo"}'
-// Returns an JWT token if authentication succeeded or an error if it failed.
-func (nc *NexusClient) Authenticate(user, pwd string) (string, error) {
-	loginArgs := &params.Login{user, pwd}
-	resp, err := nc.client.R().SetBody(loginArgs).Post("/login")
-	if err != nil {
-		return "", err
-	}
-
-	if resp.IsSuccess() {
-		var token params.Token
-		err = json.Unmarshal(resp.Body(), &token)
-		if err != nil {
-			return "", err
-		}
-		nc.client.SetAuthToken(token.Token)
-		return token.Token, nil
-	} else {
-		return "", response.ErrorToMsg(resp)
-	}
-}
-
-// Obtain a new JWT token from the server.
-func (nc *NexusClient) RefreshAuthToken() error {
-	resp, err := nc.client.R().Get("/misc/token")
-	if err != nil {
-		return err
-	} else {
-		if resp.IsSuccess() {
-			var token params.Token
-			err = json.Unmarshal(resp.Body(), &token)
-			if err != nil {
-				return err
-			}
-			nc.client.SetAuthToken(token.Token)
-			return nil
-		} else {
-			return response.ErrorToMsg(resp)
-		}
-	}
-}
-
-// Set the JWT token in the client.
-func (nc *NexusClient) SetAuthToken(token string) {
-	nc.client.SetAuthToken(token)
-}
diff --git a/libclient/remote-viewer.go b/libclient/remote-viewer.go
index f69239e34fc040ef068bdc7a66d828d862326dfb..b089b16452c8d271464dfe2368690f76114d7eae 100644
--- a/libclient/remote-viewer.go
+++ b/libclient/remote-viewer.go
@@ -2,9 +2,9 @@ package nexusclient
 
 import (
 	"errors"
-	"fmt"
 	"os/exec"
 	"runtime"
+	"strconv"
 	"strings"
 )
 
@@ -12,7 +12,7 @@ const (
 	remoteViewerBinary = "remote-viewer"
 )
 
-func checkRemoteViewer() error {
+func (nc *NexusClient) checkRemoteViewer() error {
 	os := runtime.GOOS
 	output, err := exec.Command(remoteViewerBinary, "--version").Output()
 	if os == "linux" {
@@ -45,26 +45,25 @@ func checkRemoteViewer() error {
 // name is the VM's name.
 // port is the port the VM spice server is listening to.
 // pwd is the spice password to connect to the running VM.
-func runRemoteViewer(hostname string, cert string, name string, port int, pwd string, fullscreen bool) error {
-	// portStr := strconv.Itoa(port)
-	// spice := "spice://" + hostname + "?tls-port=" + portStr + "&password=" + pwd
-	// spiceCert := "--spice-ca-file=" + cert
-	// spiceSecure := "--spice-secure-channels=all"
+func (nc *NexusClient) runRemoteViewer(name string, port int, pwd string, fullscreen bool) error {
+	certFile := nc.getVMAttachCertFile()
+	portStr := strconv.Itoa(port)
+	spice := "spice://" + nc.GetHostname() + "?tls-port=" + portStr + "&password=" + pwd
+	spiceCert := "--spice-ca-file=" + certFile
+	spiceSecure := "--spice-secure-channels=all"
 	// To exit fullscreen mode, either:
 	// - move the mouse to the middle of the top of the screen.
 	// - use ctrl+F12 as specified below
 	// To activate kiosk mode, add these options: "-k --kiosk-quit=on-disconnect"
 	var c *exec.Cmd
-	// if fullscreen {
-	// 	c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12", "-f")
-	// } else {
-	// 	c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12")
-	// }
-	c = exec.Command("remote-viewer", "spice://crap")
+	if fullscreen {
+		c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12", "-f")
+	} else {
+		c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12")
+	}
 	stdoutStderr, err := c.CombinedOutput()
 	if err != nil {
 		return errors.New("err: remote-viewer failed: " + string(stdoutStderr[:]))
 	}
-	fmt.Println("err: remote-viewer failed: " + string(stdoutStderr[:]))
 	return nil
 }
diff --git a/libclient/vm.go b/libclient/vm.go
index 5db4c4ce1f9dddb52968e4630400bbdc04d73011..bbeedf264e2f62333a706b2e0f6d8369fc94759c 100644
--- a/libclient/vm.go
+++ b/libclient/vm.go
@@ -3,8 +3,6 @@ package nexusclient
 import (
 	"encoding/json"
 	"errors"
-	"os"
-	"path/filepath"
 
 	"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/params"
 	"gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/common/vm"
@@ -12,7 +10,6 @@ import (
 	u "gitedu.hesge.ch/flg_projects/nexus_vdi/nexus/libclient/utils"
 	"github.com/go-playground/validator/v10"
 	"github.com/go-resty/resty/v2"
-	"github.com/google/uuid"
 )
 
 const (
@@ -182,21 +179,21 @@ func (nc *NexusClient) VMStop(vmID string) error {
 	}
 }
 
-func (nc *NexusClient) VMAttachFromID(vmID string, certificateFile string, fullscreen bool) error {
+func (nc *NexusClient) VMAttachFromID(vmID string, fullscreen bool) error {
 	creds, err := nc.vmIdToAttachCreds(vmID)
 	if err != nil {
 		return err
 	}
 
-	err = nc.VMAttachFromCreds(*creds, certificateFile, fullscreen)
+	err = nc.VMAttachFromCreds(*creds, fullscreen)
 	if err != nil {
 		return err
 	}
 	return nil
 }
 
-func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, certificateFile string, fullscreen bool) error {
-	if err := checkRemoteViewer(); err != nil {
+func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, fullscreen bool) error {
+	if err := nc.checkRemoteViewer(); err != nil {
 		return err
 	}
 
@@ -206,8 +203,7 @@ func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, cer
 		return err
 	}
 
-	// First return value (ignored) is of type: stdoutStderr
-	err = runRemoteViewer(nc.hostname, certificateFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
+	err = nc.runRemoteViewer(creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
 	if err != nil {
 		return err
 	}
@@ -215,8 +211,8 @@ func (nc *NexusClient) VMAttachFromCreds(v vm.VMAttachCredentialsSerialized, cer
 	return nil
 }
 
-func (nc *NexusClient) VMAttachFromPwd(pwd string, certificateFile string, fullscreen bool) error {
-	if err := checkRemoteViewer(); err != nil {
+func (nc *NexusClient) VMAttachFromPwd(pwd string, fullscreen bool) error {
+	if err := nc.checkRemoteViewer(); err != nil {
 		return err
 	}
 
@@ -225,7 +221,7 @@ func (nc *NexusClient) VMAttachFromPwd(pwd string, certificateFile string, fulls
 	if err != nil {
 		return err
 	} else {
-		err := runRemoteViewer(nc.hostname, certificateFile, creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
+		err := nc.runRemoteViewer(creds.Name, creds.SpicePort, creds.SpicePwd, fullscreen)
 		if err != nil {
 			return err
 		}
@@ -234,31 +230,6 @@ func (nc *NexusClient) VMAttachFromPwd(pwd string, certificateFile string, fulls
 	return nil
 }
 
-// Obtain the certificate required by the various VMAttachXXX methods.
-// This public CA certificate is required by Spice remote viewers.
-// The certificate file is saved in the OS' temporary directory.
-// Its name is randomly generated and garanteed to be unique.
-// It is the caller's responsability to delete it before exiting.
-func (nc *NexusClient) GetVMAttachCertificate() (string, error) {
-	uniqPostfix, err := uuid.NewRandom()
-	if err != nil {
-		return "", errors.New("Failed saving CA certificate, code 1:\n" + err.Error())
-	}
-
-	outputFile := filepath.Join(os.TempDir(), "cert_"+uniqPostfix.String()+".pem")
-	resp, err := nc.client.R().SetOutput(outputFile).Get("/misc/cert")
-	if err != nil {
-		return "", errors.New("Failed saving CA certificate, code 2:\n" + err.Error())
-	}
-
-	if resp.IsSuccess() {
-		return outputFile, nil
-	} else {
-		errorMsg, _ := u.FileToString(outputFile)
-		return "", errors.New("Failed saving CA certificate, code 3\n" + resp.Status() + ": " + errorMsg)
-	}
-}
-
 func (nc *NexusClient) VMAddAccess(vmID, vmName, email string, vmAccessCaps *params.VMAddAccess) error {
 	resp, err := nc.client.R().SetBody(vmAccessCaps).Put("/vms/" + vmID + "/access/" + email)
 	if err != nil {