@@ -2,15 +2,19 @@ package jira
2
2
3
3
import (
4
4
"bytes"
5
+ "crypto/sha256"
6
+ "encoding/hex"
5
7
"encoding/json"
6
8
"fmt"
7
9
"io"
8
10
"net/http"
9
11
"net/url"
10
12
"reflect"
13
+ "sort"
11
14
"strings"
12
15
"time"
13
16
17
+ "github.com/dgrijalva/jwt-go"
14
18
"github.com/google/go-querystring/query"
15
19
"github.com/pkg/errors"
16
20
)
@@ -360,7 +364,7 @@ func (t *BasicAuthTransport) transport() http.RoundTripper {
360
364
// CookieAuthTransport is an http.RoundTripper that authenticates all requests
361
365
// using Jira's cookie-based authentication.
362
366
//
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.
364
368
// 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).
365
369
//
366
370
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
@@ -453,6 +457,78 @@ func (t *CookieAuthTransport) transport() http.RoundTripper {
453
457
return http .DefaultTransport
454
458
}
455
459
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
+
456
532
// cloneRequest returns a clone of the provided *http.Request.
457
533
// The clone is a shallow copy of the struct and its Header map.
458
534
func cloneRequest (r * http.Request ) * http.Request {
0 commit comments