golang.org/x/oauth2@v0.18.0/jira/jira.go (about) 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 }