Repository: influxdata/influxdb-client-go (go)

Issue:
Cannot login with user/password
Specifying correct credentials for admin member, the login fails using `development` branch:

	http := &http.Client{Timeout: writeTimeout}
	options := []influxdb.Option{
        influxdb.WithAddress(url),
        influxdb.WithUserAndPass(user, password),
    }
	client, err := influxdb.New(http, options...)

result:

    unauthorized: unauthorized access


Pull request description:
feat: Added SignIn and SignOut
Closes #45

## Proposed Changes
Allowing a user to authenticate via username and password. Available via explicit calls to UsersAPI.SignIn() and UsersAPI.SignOut()

## Checklist

- [X] CHANGELOG.md updated
- [X] Rebased/mergeable
- [X] A test has been added if appropriate
- [X] Tests pass
- [X] Commit messages are in [semantic format](https://seesparkbox.com/foundry/semantic_commit_messages)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7c05ce..a216590 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,7 @@
 ## 2.1.0 [in progress]
+### Features
+1. [#193](https://github.com/influxdata/influxdb-client-go/pull/193) Added authentication using username and password. See `UsersAPI.SignIn()` and `UsersAPI.SignOut()`
+
 ### Bug fixes
 1. [#191](https://github.com/influxdata/influxdb-client-go/pull/191) Fixed QueryTableResult.Next() failed to parse boolean datatype.
 1. [#192](https://github.com/influxdata/influxdb-client-go/pull/192) Client.Close() closes idle connections of internally created HTTP client
diff --git a/api/http/options.go b/api/http/options.go
index b75a71c..5b21676 100644
--- a/api/http/options.go
+++ b/api/http/options.go
@@ -50,6 +50,7 @@ func (o *Options) HTTPClient() *http.Client {
 //
 // Setting the HTTPClient will cause the other HTTP options
 // to be ignored.
+// In case of UsersAPI.SignIn() is used, HTTPClient.Jar will be used for storing session cookie.
 func (o *Options) SetHTTPClient(c *http.Client) *Options {
 	o.httpClient = c
 	o.ownClient = false
diff --git a/api/http/service.go b/api/http/service.go
index 6bff4d3..3daa4eb 100644
--- a/api/http/service.go
+++ b/api/http/service.go
@@ -124,7 +124,9 @@ func (s *service) DoHTTPRequest(req *http.Request, requestCallback RequestCallba
 
 func (s *service) DoHTTPRequestWithResponse(req *http.Request, requestCallback RequestCallback) (*http.Response, error) {
 	log.Infof("HTTP %s req to %s", req.Method, req.URL.String())
-	req.Header.Set("Authorization", s.authorization)
+	if len(s.authorization) > 0 {
+		req.Header.Set("Authorization", s.authorization)
+	}
 	req.Header.Set("User-Agent", http2.UserAgent)
 	if requestCallback != nil {
 		requestCallback(req)
diff --git a/api/users.go b/api/users.go
index a49bad2..1e6c201 100644
--- a/api/users.go
+++ b/api/users.go
@@ -6,8 +6,15 @@ package api
 
 import (
 	"context"
+	"encoding/base64"
 	"fmt"
+	nethttp "net/http"
+	"net/http/cookiejar"
+	"sync"
+
+	"github.com/influxdata/influxdb-client-go/v2/api/http"
 	"github.com/influxdata/influxdb-client-go/v2/domain"
+	"golang.org/x/net/publicsuffix"
 )
 
 // UsersAPI provides methods for managing users in a InfluxDB server
@@ -35,16 +42,26 @@ type UsersAPI interface {
 	// Me returns actual user
 	Me(ctx context.Context) (*domain.User, error)
 	// MeUpdatePassword set password of actual user
-	MeUpdatePassword(ctx context.Context, password string) error
+	MeUpdatePassword(ctx context.Context, oldPassword, newPassword string) error
+	// SignIn exchanges username and password credentials to establish an authenticated session with the InfluxDB server. The Client's authentication token is then ignored, it can be empty.
+	SignIn(ctx context.Context, username, password string) error
+	// SignOut signs out previously signed in user
+	SignOut(ctx context.Context) error
 }
 
 type usersAPI struct {
-	apiClient *domain.ClientWithResponses
+	apiClient       *domain.ClientWithResponses
+	httpService     http.Service
+	httpClient      *nethttp.Client
+	deleteCookieJar bool
+	lock            sync.Mutex
 }
 
-func NewUsersAPI(apiClient *domain.ClientWithResponses) UsersAPI {
+func NewUsersAPI(apiClient *domain.ClientWithResponses, httpService http.Service, httpClient *nethttp.Client) UsersAPI {
 	return &usersAPI{
-		apiClient: apiClient,
+		apiClient:   apiClient,
+		httpService: httpService,
+		httpClient:  httpClient,
 	}
 }
 
@@ -164,9 +181,19 @@ func (u *usersAPI) Me(ctx context.Context) (*domain.User, error) {
 	return response.JSON200, nil
 }
 
-func (u *usersAPI) MeUpdatePassword(ctx context.Context, password string) error {
+func (u *usersAPI) MeUpdatePassword(ctx context.Context, oldPassword, newPassword string) error {
+	u.lock.Lock()
+	defer u.lock.Unlock()
+	me, err := u.Me(ctx)
+	if err != nil {
+		return err
+	}
+	creds := base64.StdEncoding.EncodeToString([]byte(me.Name + ":" + oldPassword))
+	auth := u.httpService.Authorization()
+	defer u.httpService.SetAuthorization(auth)
+	u.httpService.SetAuthorization("Basic " + creds)
 	params := &domain.PutMePasswordParams{}
-	body := &domain.PasswordResetBody{Password: password}
+	body := &domain.PasswordResetBody{Password: newPassword}
 	response, err := u.apiClient.PutMePasswordWithResponse(ctx, params, domain.PutMePasswordJSONRequestBody(*body))
 	if err != nil {
 		return err
@@ -176,3 +203,52 @@ func (u *usersAPI) MeUpdatePassword(ctx context.Context, password string) error
 	}
 	return nil
 }
+
+func (u *usersAPI) SignIn(ctx context.Context, username, password string) error {
+	u.lock.Lock()
+	defer u.lock.Unlock()
+	if u.httpClient.Jar == nil {
+		jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
+		if err != nil {
+			return err
+		}
+		u.httpClient.Jar = jar
+		u.deleteCookieJar = true
+	}
+	creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
+	u.httpService.SetAuthorization("Basic " + creds)
+	defer u.httpService.SetAuthorization("")
+	resp, err := u.apiClient.PostSigninWithResponse(ctx, &domain.PostSigninParams{})
+	if err != nil {
+		return err
+	}
+	if resp.JSONDefault != nil {
+		return domain.DomainErrorToError(resp.JSONDefault, resp.StatusCode())
+	}
+	if resp.JSON401 != nil {
+		return domain.DomainErrorToError(resp.JSON401, resp.StatusCode())
+	}
+	if resp.JSON403 != nil {
+		return domain.DomainErrorToError(resp.JSON403, resp.StatusCode())
+	}
+	return nil
+}
+
+func (u *usersAPI) SignOut(ctx context.Context) error {
+	u.lock.Lock()
+	defer u.lock.Unlock()
+	resp, err := u.apiClient.PostSignoutWithResponse(ctx, &domain.PostSignoutParams{})
+	if err != nil {
+		return err
+	}
+	if resp.JSONDefault != nil {
+		return domain.DomainErrorToError(resp.JSONDefault, resp.StatusCode())
+	}
+	if resp.JSON401 != nil {
+		return domain.DomainErrorToError(resp.JSON401, resp.StatusCode())
+	}
+	if u.deleteCookieJar {
+		u.httpClient.Jar = nil
+	}
+	return nil
+}
diff --git a/client.go b/client.go
index 7657d24..32fd513 100644
--- a/client.go
+++ b/client.go
@@ -98,7 +98,11 @@ func NewClientWithOptions(serverURL string, authToken string, options *Options)
 		// For subsequent path parts concatenation, url has to end with '/'
 		normServerURL = serverURL + "/"
 	}
-	service := http.NewService(normServerURL, "Token "+authToken, options.httpOptions)
+	authorization := ""
+	if len(authToken) > 0 {
+		authorization = "Token " + authToken
+	}
+	service := http.NewService(normServerURL, authorization, options.httpOptions)
 	client := &clientImpl{
 		serverURL:     serverURL,
 		options:       options,
@@ -245,7 +249,7 @@ func (c *clientImpl) UsersAPI() api.UsersAPI {
 	c.lock.Lock()
 	defer c.lock.Unlock()
 	if c.usersAPI == nil {
-		c.usersAPI = api.NewUsersAPI(c.apiClient)
+		c.usersAPI = api.NewUsersAPI(c.apiClient, c.httpService, c.options.HTTPClient())
 	}
 	return c.usersAPI
 }
diff --git a/go.mod b/go.mod
index 1f5a8ff..cf71cad 100644
--- a/go.mod
+++ b/go.mod
@@ -7,5 +7,6 @@ require (
 	github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.4.0 // test dependency
+	golang.org/x/net v0.0.0-20191112182307-2180aed22343
 	gopkg.in/yaml.v2 v2.2.5
 )
diff --git a/options.go b/options.go
index 3f87785..9e1622c 100644
--- a/options.go
+++ b/options.go
@@ -144,6 +144,7 @@ func (o *Options) HTTPClient() *nethttp.Client {
 //
 // Setting the HTTPClient will cause the other HTTP options
 // to be ignored.
+// In case of UsersAPI.SignIn() is used, HTTPClient.Jar will be used for storing session cookie.
 func (o *Options) SetHTTPClient(c *nethttp.Client) *Options {
 	o.httpOptions.SetHTTPClient(c)
 	return o


Test patch:
diff --git a/api/examples_test.go b/api/examples_test.go
index 782e5db..c4d231f 100644
--- a/api/examples_test.go
+++ b/api/examples_test.go
@@ -325,6 +325,33 @@ func ExampleUsersAPI() {
 	client.Close()
 }
 
+func ExampleUsersAPI_signInOut() {
+	// Create a new client using an InfluxDB server base URL and empty token
+	client := influxdb2.NewClient("http://localhost:9999", "")
+	// Always close client at the end
+	defer client.Close()
+
+	ctx := context.Background()
+
+	// The first call must be signIn
+	err := client.UsersAPI().SignIn(ctx, "username", "password")
+	if err != nil {
+		panic(err)
+	}
+
+	// Perform some authorized operations
+	err = client.WriteAPIBlocking("my-org", "my-bucket").WriteRecord(ctx, "test,a=rock,b=local f=1.2,i=-5i")
+	if err != nil {
+		panic(err)
+	}
+
+	// Sign out at the end
+	err = client.UsersAPI().SignOut(ctx)
+	if err != nil {
+		panic(err)
+	}
+}
+
 func ExampleLabelsAPI() {
 	// Create a new client using an InfluxDB server base URL and an authentication token
 	client := influxdb2.NewClient("http://localhost:9999", "my-token")
diff --git a/api/users_e2e_test.go b/api/users_e2e_test.go
index 2d4d790..9fee3b3 100644
--- a/api/users_e2e_test.go
+++ b/api/users_e2e_test.go
@@ -75,9 +75,12 @@ func TestUsersAPI(t *testing.T) {
 	require.NotNil(t, users)
 	assert.Len(t, *users, 1)
 
-	// this fails now
-	err = usersAPI.MeUpdatePassword(ctx, "new-password")
-	assert.NotNil(t, err)
+	// it fails, https://github.com/influxdata/influxdb/pull/15981
+	//err = usersAPI.MeUpdatePassword(ctx, "my-password", "my-new-password")
+	//assert.Nil(t, err)
+
+	//err = usersAPI.MeUpdatePassword(ctx, "my-new-password", "my-password")
+	//assert.Nil(t, err)
 }
 
 func TestUsersAPI_failing(t *testing.T) {
@@ -136,6 +139,89 @@ func TestUsersAPI_requestFailing(t *testing.T) {
 	_, err = usersAPI.Me(ctx)
 	assert.NotNil(t, err)
 
-	err = usersAPI.MeUpdatePassword(ctx, "pass")
+	err = usersAPI.MeUpdatePassword(ctx, "my-password", "my-new-password")
+	assert.NotNil(t, err)
+
+	err = usersAPI.SignIn(ctx, "user", "my-password")
+	assert.NotNil(t, err)
+
+	err = usersAPI.SignOut(ctx)
+	assert.NotNil(t, err)
+}
+
+func TestSignInOut(t *testing.T) {
+	ctx := context.Background()
+	client := influxdb2.NewClient("http://localhost:9999", "")
+
+	usersAPI := client.UsersAPI()
+
+	err := usersAPI.SignIn(ctx, "my-user", "my-password")
+	require.Nil(t, err)
+
+	// try authorized calls
+	orgs, err := client.OrganizationsAPI().GetOrganizations(ctx)
+	assert.Nil(t, err)
+	assert.NotNil(t, orgs)
+
+	// try authorized calls
+	buckets, err := client.BucketsAPI().GetBuckets(ctx)
+	assert.Nil(t, err)
+	assert.NotNil(t, buckets)
+
+	// try authorized calls
+	err = client.WriteAPIBlocking("my-org", "my-bucket").WriteRecord(ctx, "test,a=rock,b=local f=1.2,i=-5i")
+	assert.Nil(t, err)
+
+	res, err := client.QueryAPI("my-org").QueryRaw(context.Background(), `from(bucket:"my-bucket")|> range(start: -24h) |> filter(fn: (r) => r._measurement == "test")`, influxdb2.DefaultDialect())
+	assert.Nil(t, err)
+	assert.NotNil(t, res)
+
+	err = usersAPI.SignOut(ctx)
+	assert.Nil(t, err)
+
+	// unauthorized signout
+	err = usersAPI.SignOut(ctx)
+	assert.NotNil(t, err)
+
+	// Unauthorized call
+	_, err = client.OrganizationsAPI().GetOrganizations(ctx)
 	assert.NotNil(t, err)
+
+	// test wrong credentials
+	err = usersAPI.SignIn(ctx, "my-user", "password")
+	assert.NotNil(t, err)
+
+	client.HTTPService().SetAuthorization("Token my-token")
+
+	user, err := usersAPI.CreateUserWithName(ctx, "user-01")
+	require.Nil(t, err)
+	require.NotNil(t, user)
+
+	// 2nd client to use for new user auth
+	client2 := influxdb2.NewClient("http://localhost:9999", "")
+
+	err = usersAPI.UpdateUserPassword(ctx, user, "123password")
+	assert.Nil(t, err)
+
+	err = client2.UsersAPI().SignIn(ctx, "user-01", "123password")
+	assert.Nil(t, err)
+
+	err = client2.UsersAPI().SignOut(ctx)
+	assert.Nil(t, err)
+
+	status := domain.UserStatusInactive
+	user.Status = &status
+	u, err := usersAPI.UpdateUser(ctx, user)
+	assert.Nil(t, err)
+	assert.NotNil(t, u)
+
+	// log in inactive user,
+	//err = client2.SignIn(ctx, "user-01", "123password")
+	//assert.NotNil(t, err)
+
+	err = usersAPI.DeleteUser(ctx, user)
+	assert.Nil(t, err)
+
+	client.Close()
+	client2.Close()
 }
diff --git a/examples_test.go b/examples_test.go
index 955162d..53b6819 100644
--- a/examples_test.go
+++ b/examples_test.go
@@ -12,7 +12,7 @@ func ExampleClient_newClient() {
 	// Create a new client using an InfluxDB server base URL and an authentication token
 	client := influxdb2.NewClient("http://localhost:9999", "my-token")
 
-	// always close client at the end
+	// Always close client at the end
 	defer client.Close()
 }
 
@@ -22,18 +22,25 @@ func ExampleClient_newClientWithOptions() {
 	client := influxdb2.NewClientWithOptions("http://localhost:9999", "my-token",
 		influxdb2.DefaultOptions().SetBatchSize(20))
 
-	// always close client at the end
+	// Always close client at the end
 	defer client.Close()
 }
 
 func ExampleClient_customServerAPICall() {
+	// Create a new client using an InfluxDB server base URL and empty token
 	client := influxdb2.NewClient("http://localhost:9999", "my-token")
+	// Always close client at the end
+	defer client.Close()
+	// Get generated client for server API calls
 	apiClient := domain.NewClientWithResponses(client.HTTPService())
+	// Get an organization that will own task
 	org, err := client.OrganizationsAPI().FindOrganizationByName(context.Background(), "my-org")
 	if err != nil {
 		//return err
 		panic(err)
 	}
+
+	// Basic task properties
 	taskDescription := "Example task"
 	taskFlux := `option task = {
   name: "My task",
@@ -42,6 +49,8 @@ func ExampleClient_customServerAPICall() {
 
 from(bucket:"my-bucket") |> range(start: -1m) |> last()`
 	taskStatus := domain.TaskStatusTypeActive
+
+	// Create TaskCreateRequest object
 	taskRequest := domain.TaskCreateRequest{
 		Org:         &org.Name,
 		OrgID:       org.Id,
@@ -49,13 +58,19 @@ from(bucket:"my-bucket") |> range(start: -1m) |> last()`
 		Flux:        taskFlux,
 		Status:      &taskStatus,
 	}
+
+	// Issue an API call
 	resp, err := apiClient.PostTasksWithResponse(context.Background(), &domain.PostTasksParams{}, domain.PostTasksJSONRequestBody(taskRequest))
 	if err != nil {
 		panic(err)
 	}
+
+	// Always check generated response errors
 	if resp.JSONDefault != nil {
 		panic(resp.JSONDefault.Message)
 	}
+
+	// Use API call result
 	task := resp.JSON201
 	fmt.Println("Created task: ", task.Name)
 }