1
2
3
4
5
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
26
27
28 type ClaimSet struct {
29 Issuer string `json:"iss"`
30 Subject string `json:"sub"`
31 InstalledURL string `json:"tnt"`
32 AuthURL string `json:"aud"`
33 ExpiresIn int64 `json:"exp"`
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
46
47 type Config struct {
48
49 BaseURL string
50
51
52
53 Subject string
54
55 oauth2.Config
56 }
57
58
59
60 func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
61 return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
62 }
63
64
65
66
67
68
69 func (c *Config) Client(ctx context.Context) *http.Client {
70 return oauth2.NewClient(ctx, c.TokenSource(ctx))
71 }
72
73
74
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
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
104 assertion, err := sign(js.conf.ClientSecret, claimSet)
105 if err != nil {
106 return nil, err
107 }
108 v.Set("assertion", assertion)
109
110
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
126 var tokenRes struct {
127 AccessToken string `json:"access_token"`
128 TokenType string `json:"token_type"`
129 ExpiresIn int64 `json:"expires_in"`
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
146
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