...

Source file src/golang.org/x/oauth2/jira/jira.go

Documentation: golang.org/x/oauth2/jira

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package jira provides claims and JWT signing for OAuth2 to access JIRA/Confluence.
     6  package jira
     7  
     8  import (
     9  	"context"
    10  	"crypto/hmac"
    11  	"crypto/sha256"
    12  	"encoding/base64"
    13  	"encoding/json"
    14  	"fmt"
    15  	"io"
    16  	"io/ioutil"
    17  	"net/http"
    18  	"net/url"
    19  	"strings"
    20  	"time"
    21  
    22  	"golang.org/x/oauth2"
    23  )
    24  
    25  // ClaimSet contains information about the JWT signature according
    26  // to Atlassian's documentation
    27  // https://developer.atlassian.com/cloud/jira/software/oauth-2-jwt-bearer-token-authorization-grant-type/
    28  type ClaimSet struct {
    29  	Issuer       string `json:"iss"`
    30  	Subject      string `json:"sub"`
    31  	InstalledURL string `json:"tnt"` // URL of installed app
    32  	AuthURL      string `json:"aud"` // URL of auth server
    33  	ExpiresIn    int64  `json:"exp"` // Must be no later that 60 seconds in the future
    34  	IssuedAt     int64  `json:"iat"`
    35  }
    36  
    37  var (
    38  	defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
    39  	defaultHeader    = map[string]string{
    40  		"typ": "JWT",
    41  		"alg": "HS256",
    42  	}
    43  )
    44  
    45  // Config is the configuration for using JWT to fetch tokens,
    46  // commonly known as "two-legged OAuth 2.0".
    47  type Config struct {
    48  	// BaseURL for your app
    49  	BaseURL string
    50  
    51  	// Subject is the userkey as defined by Atlassian
    52  	// Different than username (ex: /rest/api/2/user?username=alex)
    53  	Subject string
    54  
    55  	oauth2.Config
    56  }
    57  
    58  // TokenSource returns a JWT TokenSource using the configuration
    59  // in c and the HTTP client from the provided context.
    60  func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
    61  	return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
    62  }
    63  
    64  // Client returns an HTTP client wrapping the context's
    65  // HTTP transport and adding Authorization headers with tokens
    66  // obtained from c.
    67  //
    68  // The returned client and its Transport should not be modified.
    69  func (c *Config) Client(ctx context.Context) *http.Client {
    70  	return oauth2.NewClient(ctx, c.TokenSource(ctx))
    71  }
    72  
    73  // jwtSource is a source that always does a signed JWT request for a token.
    74  // It should typically be wrapped with a reuseTokenSource.
    75  type jwtSource struct {
    76  	ctx  context.Context
    77  	conf *Config
    78  }
    79  
    80  func (js jwtSource) Token() (*oauth2.Token, error) {
    81  	exp := time.Duration(59) * time.Second
    82  	claimSet := &ClaimSet{
    83  		Issuer:       fmt.Sprintf("urn:atlassian:connect:clientid:%s", js.conf.ClientID),
    84  		Subject:      fmt.Sprintf("urn:atlassian:connect:useraccountid:%s", js.conf.Subject),
    85  		InstalledURL: js.conf.BaseURL,
    86  		AuthURL:      js.conf.Endpoint.AuthURL,
    87  		IssuedAt:     time.Now().Unix(),
    88  		ExpiresIn:    time.Now().Add(exp).Unix(),
    89  	}
    90  
    91  	v := url.Values{}
    92  	v.Set("grant_type", defaultGrantType)
    93  
    94  	// Add scopes if they exist;  If not, it defaults to app scopes
    95  	if scopes := js.conf.Scopes; scopes != nil {
    96  		upperScopes := make([]string, len(scopes))
    97  		for i, k := range scopes {
    98  			upperScopes[i] = strings.ToUpper(k)
    99  		}
   100  		v.Set("scope", strings.Join(upperScopes, "+"))
   101  	}
   102  
   103  	// Sign claims for assertion
   104  	assertion, err := sign(js.conf.ClientSecret, claimSet)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	v.Set("assertion", assertion)
   109  
   110  	// Fetch access token from auth server
   111  	hc := oauth2.NewClient(js.ctx, nil)
   112  	resp, err := hc.PostForm(js.conf.Endpoint.TokenURL, v)
   113  	if err != nil {
   114  		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
   115  	}
   116  	defer resp.Body.Close()
   117  	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
   118  	if err != nil {
   119  		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
   120  	}
   121  	if c := resp.StatusCode; c < 200 || c > 299 {
   122  		return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body)
   123  	}
   124  
   125  	// tokenRes is the JSON response body.
   126  	var tokenRes struct {
   127  		AccessToken string `json:"access_token"`
   128  		TokenType   string `json:"token_type"`
   129  		ExpiresIn   int64  `json:"expires_in"` // relative seconds from now
   130  	}
   131  	if err := json.Unmarshal(body, &tokenRes); err != nil {
   132  		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
   133  	}
   134  	token := &oauth2.Token{
   135  		AccessToken: tokenRes.AccessToken,
   136  		TokenType:   tokenRes.TokenType,
   137  	}
   138  
   139  	if secs := tokenRes.ExpiresIn; secs > 0 {
   140  		token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
   141  	}
   142  	return token, nil
   143  }
   144  
   145  // Sign the claim set with the shared secret
   146  // Result to be sent as assertion
   147  func sign(key string, claims *ClaimSet) (string, error) {
   148  	b, err := json.Marshal(defaultHeader)
   149  	if err != nil {
   150  		return "", err
   151  	}
   152  	header := base64.RawURLEncoding.EncodeToString(b)
   153  
   154  	jsonClaims, err := json.Marshal(claims)
   155  	if err != nil {
   156  		return "", err
   157  	}
   158  	encodedClaims := strings.TrimRight(base64.URLEncoding.EncodeToString(jsonClaims), "=")
   159  
   160  	ss := fmt.Sprintf("%s.%s", header, encodedClaims)
   161  
   162  	mac := hmac.New(sha256.New, []byte(key))
   163  	mac.Write([]byte(ss))
   164  	signature := mac.Sum(nil)
   165  
   166  	return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(signature)), nil
   167  }
   168  

View as plain text