Skip to content

Commit

Permalink
Merge pull request #112 from rbriski/rbriski/auth_change
Browse files Browse the repository at this point in the history
Moving to RoundTripper for authentication
  • Loading branch information
andygrunwald authored Feb 28, 2018
2 parents e04b453 + 02b410e commit 78dbcf2
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 63 deletions.
93 changes: 43 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,68 +80,49 @@ func main() {
}
```

### Authenticate with jira
### Authentication

Some actions require an authenticated user.
The `go-jira` library does not handle most authentication directly. Instead, authentication should be handled within
an `http.Client`. That client can then be passed into the `NewClient` function when creating a jira client.

#### Authenticate with basic auth
For convenience, capability for basic and cookie-based authentication is included in the main library.

Here is an example with basic auth authentication.
#### Basic auth example

```go
package main

import (
"fmt"
"github.com/andygrunwald/go-jira"
)
A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory.

```go
func main() {
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
if err != nil {
panic(err)
tp := jira.BasicAuthTransport{
Username: "username",
Password: "password",
}
jiraClient.Authentication.SetBasicAuth("username", "password")

issue, _, err := jiraClient.Issue.Get("SYS-5156", nil)
if err != nil {
panic(err)
}
client, err := jira.NewClient(tp.Client(), "https://my.jira.com")

fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
u, _, err := client.User.Get("some_user")

fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress)
}
```

#### Authenticate with session cookie

Here is an example with session cookie authentication.
A more thorough, [runnable example](examples/cookieauth/main.go) is provided in the examples directory.

```go
package main
Note: The `AuthURL` is almost always going to have the path `/rest/auth/1/session`

import (
"fmt"
"github.com/andygrunwald/go-jira"
)

func main() {
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
if err != nil {
panic(err)
}

res, err := jiraClient.Authentication.AcquireSessionCookie("username", "password")
if err != nil || res == false {
fmt.Printf("Result: %v\n", res)
panic(err)
```go
tp := jira.CookieAuthTransport{
Username: "username",
Password: "password",
AuthURL: "https://my.jira.com/rest/auth/1/session",
}

issue, _, err := jiraClient.Issue.Get("SYS-5156", nil)
if err != nil {
panic(err)
}
client, err := jira.NewClient(tp.Client(), "https://my.jira.com")
u, _, err := client.User.Get("admin")

fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress)
}
```

Expand All @@ -164,14 +145,14 @@ import (
)

func main() {
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
if err != nil {
panic(err)
tp := jira.CookieAuthTransport{
Username: "username",
Password: "password",
BaseURL: "https://my.jira.com",
}

res, err := jiraClient.Authentication.AcquireSessionCookie("username", "password")
if err != nil || res == false {
fmt.Printf("Result: %v\n", res)
jiraClient, err := jira.NewClient(tp.Client(), tp.BaseURL)
if err != nil {
panic(err)
}

Expand Down Expand Up @@ -217,7 +198,13 @@ import (
)

func main() {
jiraClient, _ := jira.NewClient(nil, "https://jira.atlassian.com/")
tp := jira.CookieAuthTransport{
Username: "username",
Password: "password",
BaseURL: "https://my.jira.com",
}

jiraClient, _ := jira.NewClient(tp.Client(), tp.BaseURL)
req, _ := jiraClient.NewRequest("GET", "/rest/api/2/project", nil)

projects := new([]jira.Project)
Expand Down Expand Up @@ -271,6 +258,12 @@ If you are new to pull requests, checkout [Collaborating on projects using issue

For adding new dependencies, updating dependencies, and other operations, the [Daily Dep](https://golang.github.io/dep/docs/daily-dep.html) is a good place to start.

### Sandbox environment for testing

Jira offers sandbox test environments at http://go.atlassian.com/cloud-dev.

You can read more about them at https://developer.atlassian.com/blog/2016/04/cloud-ecosystem-dev-env/.

## License

This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License).
7 changes: 7 additions & 0 deletions authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ type Session struct {
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
//
// Deprecated: Use CookieAuthTransport instead
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
apiEndpoint := "rest/auth/1/session"
body := struct {
Expand Down Expand Up @@ -90,6 +92,8 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
}

// SetBasicAuth sets username and password for the basic auth against the JIRA instance.
//
// Deprecated: Use BasicAuthTransport instead
func (s *AuthenticationService) SetBasicAuth(username, password string) {
s.username = username
s.password = password
Expand All @@ -112,6 +116,9 @@ func (s *AuthenticationService) Authenticated() bool {
// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
//
// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the
// client anymore
func (s *AuthenticationService) Logout() error {
if s.authType != authTypeSession || s.client.session == nil {
return fmt.Errorf("No user is authenticated yet.")
Expand Down
47 changes: 47 additions & 0 deletions examples/basicauth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package main

import (
"bufio"
"fmt"
"os"
"strings"
"syscall"

jira "github.com/andygrunwald/go-jira"
"golang.org/x/crypto/ssh/terminal"
)

func main() {
r := bufio.NewReader(os.Stdin)

fmt.Print("Jira URL: ")
jiraURL, _ := r.ReadString('\n')

fmt.Print("Jira Username: ")
username, _ := r.ReadString('\n')

fmt.Print("Jira Password: ")
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
password := string(bytePassword)

tp := jira.BasicAuthTransport{
Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password),
}

client, err := jira.NewClient(tp.Client(), strings.TrimSpace(jiraURL))
if err != nil {
fmt.Printf("\nerror: %v\n", err)
return
}

u, _, err := client.User.Get("admin")

if err != nil {
fmt.Printf("\nerror: %v\n", err)
return
}

fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress)

}
145 changes: 145 additions & 0 deletions jira.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"net/http"
"net/url"
"reflect"
"time"

"github.com/google/go-querystring/query"
"github.com/pkg/errors"
)

// A Client manages communication with the JIRA API.
Expand Down Expand Up @@ -281,3 +283,146 @@ func (r *Response) populatePageValues(v interface{}) {
}
return
}

// BasicAuthTransport is an http.RoundTripper that authenticates all requests
// using HTTP Basic Authentication with the provided username and password.
type BasicAuthTransport struct {
Username string
Password string

// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}

// RoundTrip implements the RoundTripper interface. We just add the
// basic auth and return the RoundTripper for this transport type.
func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract

req2.SetBasicAuth(t.Username, t.Password)
return t.transport().RoundTrip(req2)
}

// Client returns an *http.Client that makes requests that are authenticated
// using HTTP Basic Authentication. This is a nice little bit of sugar
// so we can just get the client instead of creating the client in the calling code.
// If it's necessary to send more information on client init, the calling code can
// always skip this and set the transport itself.
func (t *BasicAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

func (t *BasicAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}

// CookieAuthTransport is an http.RoundTripper that authenticates all requests
// using Jira's cookie-based authentication.
//
// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
type CookieAuthTransport struct {
Username string
Password string
AuthURL string

// SessionObject is the authenticated cookie string.s
// It's passed in each call to prove the client is authenticated.
SessionObject []*http.Cookie

// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}

// RoundTrip adds the session object to the request.
func (t *CookieAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.SessionObject == nil {
err := t.setSessionObject()
if err != nil {
return nil, errors.Wrap(err, "cookieauth: no session object has been set")
}
}

req2 := cloneRequest(req) // per RoundTripper contract
for _, cookie := range t.SessionObject {
req2.AddCookie(cookie)
}

return t.transport().RoundTrip(req2)
}

// Client returns an *http.Client that makes requests that are authenticated
// using cookie authentication
func (t *CookieAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

// setSessionObject attempts to authenticate the user and set
// the session object (e.g. cookie)
func (t *CookieAuthTransport) setSessionObject() error {
req, err := t.buildAuthRequest()
if err != nil {
return err
}

var authClient = &http.Client{
Timeout: time.Second * 60,
}
resp, err := authClient.Do(req)
if err != nil {
return err
}

t.SessionObject = resp.Cookies()
return nil
}

// getAuthRequest assembles the request to get the authenticated cookie
func (t *CookieAuthTransport) buildAuthRequest() (*http.Request, error) {
body := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
t.Username,
t.Password,
}

b := new(bytes.Buffer)
json.NewEncoder(b).Encode(body)

req, err := http.NewRequest("POST", t.AuthURL, b)
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/json")
return req, nil
}

func (t *CookieAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}

// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
return r2
}
Loading

0 comments on commit 78dbcf2

Please sign in to comment.