mirror of
https://github.com/golang/oauth2.git
synced 2025-07-21 00:00:09 +08:00
With https://go.dev/issue/61417 implemented, we can use the token type directly to unmarshal the JSON fields for the wire format. While here, remove all uses of the deprecated ioutil package as suggested by gopls while making these changes. Change-Id: I79d82374643007a21b5b3d9a8117bed81273eca5 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/614415 Reviewed-by: Sean Liao <sean@liao.dev> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Michael Pratt <mpratt@google.com>
163 lines
4.7 KiB
Go
163 lines
4.7 KiB
Go
// 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"
|
|
"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:useraccountid:%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", 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 := io.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 oauth2.Token
|
|
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
|
|
}
|