aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/golang.org/x/oauth2/jira/jira.go
blob: 34415607c3f507dbcf9a185ba71744639947a83b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package jira provides claims and JWT signing for OAuth2 to access JIRA/Confluence.
package jira

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"time"

	"golang.org/x/oauth2"
)

// ClaimSet contains information about the JWT signature according
// to Atlassian's documentation
// https://developer.atlassian.com/cloud/jira/software/oauth-2-jwt-bearer-token-authorization-grant-type/
type ClaimSet struct {
	Issuer       string `json:"iss"`
	Subject      string `json:"sub"`
	InstalledURL string `json:"tnt"` // URL of installed app
	AuthURL      string `json:"aud"` // URL of auth server
	ExpiresIn    int64  `json:"exp"` // Must be no later that 60 seconds in the future
	IssuedAt     int64  `json:"iat"`
}

var (
	defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
	defaultHeader    = map[string]string{
		"typ": "JWT",
		"alg": "HS256",
	}
)

// Config is the configuration for using JWT to fetch tokens,
// commonly known as "two-legged OAuth 2.0".
type Config struct {
	// BaseURL for your app
	BaseURL string

	// Subject is the userkey as defined by Atlassian
	// Different than username (ex: /rest/api/2/user?username=alex)
	Subject string

	oauth2.Config
}

// TokenSource returns a JWT TokenSource using the configuration
// in c and the HTTP client from the provided context.
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
	return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
}

// Client returns an HTTP client wrapping the context's
// HTTP transport and adding Authorization headers with tokens
// obtained from c.
//
// The returned client and its Transport should not be modified.
func (c *Config) Client(ctx context.Context) *http.Client {
	return oauth2.NewClient(ctx, c.TokenSource(ctx))
}

// jwtSource is a source that always does a signed JWT request for a token.
// It should typically be wrapped with a reuseTokenSource.
type jwtSource struct {
	ctx  context.Context
	conf *Config
}

func (js jwtSource) Token() (*oauth2.Token, error) {
	exp := time.Duration(59) * time.Second
	claimSet := &ClaimSet{
		Issuer:       fmt.Sprintf("urn:atlassian:connect:clientid:%s", js.conf.ClientID),
		Subject:      fmt.Sprintf("urn:atlassian:connect:userkey:%s", js.conf.Subject),
		InstalledURL: js.conf.BaseURL,
		AuthURL:      js.conf.Endpoint.AuthURL,
		IssuedAt:     time.Now().Unix(),
		ExpiresIn:    time.Now().Add(exp).Unix(),
	}

	v := url.Values{}
	v.Set("grant_type", defaultGrantType)

	// Add scopes if they exist;  If not, it defaults to app scopes
	if scopes := js.conf.Scopes; scopes != nil {
		upperScopes := make([]string, len(scopes))
		for i, k := range scopes {
			upperScopes[i] = strings.ToUpper(k)
		}
		v.Set("scope", strings.Join(upperScopes, "+"))
	}

	// Sign claims for assertion
	assertion, err := sign(js.conf.ClientSecret, claimSet)
	if err != nil {
		return nil, err
	}
	v.Set("assertion", string(assertion))

	// Fetch access token from auth server
	hc := oauth2.NewClient(js.ctx, nil)
	resp, err := hc.PostForm(js.conf.Endpoint.TokenURL, v)
	if err != nil {
		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
	}
	if c := resp.StatusCode; c < 200 || c > 299 {
		return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body)
	}

	// tokenRes is the JSON response body.
	var tokenRes struct {
		AccessToken string `json:"access_token"`
		TokenType   string `json:"token_type"`
		ExpiresIn   int64  `json:"expires_in"` // relative seconds from now
	}
	if err := json.Unmarshal(body, &tokenRes); err != nil {
		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
	}
	token := &oauth2.Token{
		AccessToken: tokenRes.AccessToken,
		TokenType:   tokenRes.TokenType,
	}

	if secs := tokenRes.ExpiresIn; secs > 0 {
		token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
	}
	return token, nil
}

// Sign the claim set with the shared secret
// Result to be sent as assertion
func sign(key string, claims *ClaimSet) (string, error) {
	b, err := json.Marshal(defaultHeader)
	if err != nil {
		return "", err
	}
	header := base64.RawURLEncoding.EncodeToString(b)

	jsonClaims, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}
	encodedClaims := strings.TrimRight(base64.URLEncoding.EncodeToString(jsonClaims), "=")

	ss := fmt.Sprintf("%s.%s", header, encodedClaims)

	mac := hmac.New(sha256.New, []byte(key))
	mac.Write([]byte(ss))
	signature := mac.Sum(nil)

	return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(signature)), nil
}