@@ -2,15 +2,19 @@ package jira
22
33import (
44 "bytes"
5+ "crypto/sha256"
6+ "encoding/hex"
57 "encoding/json"
68 "fmt"
79 "io"
810 "net/http"
911 "net/url"
1012 "reflect"
13+ "sort"
1114 "strings"
1215 "time"
1316
17+ "github.com/dgrijalva/jwt-go"
1418 "github.com/google/go-querystring/query"
1519 "github.com/pkg/errors"
1620)
@@ -360,7 +364,7 @@ func (t *BasicAuthTransport) transport() http.RoundTripper {
360364// CookieAuthTransport is an http.RoundTripper that authenticates all requests
361365// using Jira's cookie-based authentication.
362366//
363- // Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
367+ // Note that it is generally preferable to use HTTP BASIC authentication with the REST API.
364368// 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).
365369//
366370// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
@@ -453,6 +457,78 @@ func (t *CookieAuthTransport) transport() http.RoundTripper {
453457 return http .DefaultTransport
454458}
455459
460+ // JWTAuthTransport is an http.RoundTripper that authenticates all requests
461+ // using Jira's JWT based authentication.
462+ //
463+ // NOTE: this form of auth should be used by add-ons installed from the Atlassian marketplace.
464+ //
465+ // JIRA docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt
466+ // Examples in other languages:
467+ // https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/d44a8e7a4649e4f23edaa784402655fda7c816ea/lib/atlassian/jwt.rb
468+ // https://bitbucket.org/atlassian/atlassian-jwt-py/src/master/atlassian_jwt/url_utils.py
469+ type JWTAuthTransport struct {
470+ Secret []byte
471+ Issuer string
472+
473+ // Transport is the underlying HTTP transport to use when making requests.
474+ // It will default to http.DefaultTransport if nil.
475+ Transport http.RoundTripper
476+ }
477+
478+ func (t * JWTAuthTransport ) Client () * http.Client {
479+ return & http.Client {Transport : t }
480+ }
481+
482+ func (t * JWTAuthTransport ) transport () http.RoundTripper {
483+ if t .Transport != nil {
484+ return t .Transport
485+ }
486+ return http .DefaultTransport
487+ }
488+
489+ // RoundTrip adds the session object to the request.
490+ func (t * JWTAuthTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
491+ req2 := cloneRequest (req ) // per RoundTripper contract
492+ exp := time .Duration (59 ) * time .Second
493+ qsh := t .createQueryStringHash (req .Method , req2 .URL )
494+ token := jwt .NewWithClaims (jwt .SigningMethodHS256 , jwt.MapClaims {
495+ "iss" : t .Issuer ,
496+ "iat" : time .Now ().Unix (),
497+ "exp" : time .Now ().Add (exp ).Unix (),
498+ "qsh" : qsh ,
499+ })
500+
501+ jwtStr , err := token .SignedString (t .Secret )
502+ if err != nil {
503+ return nil , errors .Wrap (err , "jwtAuth: error signing JWT" )
504+ }
505+
506+ req2 .Header .Set ("Authorization" , fmt .Sprintf ("JWT %s" , jwtStr ))
507+ return t .transport ().RoundTrip (req2 )
508+ }
509+
510+ func (t * JWTAuthTransport ) createQueryStringHash (httpMethod string , jiraURL * url.URL ) string {
511+ canonicalRequest := t .canonicalizeRequest (httpMethod , jiraURL )
512+ h := sha256 .Sum256 ([]byte (canonicalRequest ))
513+ return hex .EncodeToString (h [:])
514+ }
515+
516+ func (t * JWTAuthTransport ) canonicalizeRequest (httpMethod string , jiraURL * url.URL ) string {
517+ path := "/" + strings .Replace (strings .Trim (jiraURL .Path , "/" ), "&" , "%26" , - 1 )
518+
519+ var canonicalQueryString []string
520+ for k , v := range jiraURL .Query () {
521+ if k == "jwt" {
522+ continue
523+ }
524+ param := url .QueryEscape (k )
525+ value := url .QueryEscape (strings .Join (v , "" ))
526+ canonicalQueryString = append (canonicalQueryString , strings .Replace (strings .Join ([]string {param , value }, "=" ), "+" , "%20" , - 1 ))
527+ }
528+ sort .Strings (canonicalQueryString )
529+ return fmt .Sprintf ("%s&%s&%s" , strings .ToUpper (httpMethod ), path , strings .Join (canonicalQueryString , "&" ))
530+ }
531+
456532// cloneRequest returns a clone of the provided *http.Request.
457533// The clone is a shallow copy of the struct and its Header map.
458534func cloneRequest (r * http.Request ) * http.Request {
0 commit comments