OAuth2 client implementation for Go with support for authorization code flow, client credentials, device authorization, and JWT flows
Specialized OAuth2 support for JIRA and Confluence with JWT bearer token authentication using HMAC-SHA256 signing.
import "golang.org/x/oauth2/jira"The jira package provides JWT-based OAuth2 authentication specifically designed for Atlassian JIRA and Confluence. It implements the JWT Bearer Token Authorization Grant Type with HMAC-SHA256 signing.
Documentation: https://developer.atlassian.com/cloud/jira/software/oauth-2-jwt-bearer-token-authorization-grant-type/
type Config struct {
// BaseURL for your app
BaseURL string
// Subject is the userkey as defined by Atlassian
// Different than username (fetch from: /rest/api/2/user?username=alex)
Subject string
// Embedded oauth2.Config
oauth2.Config
}func (c *Config) Client(ctx context.Context) *http.ClientReturns an HTTP client that automatically handles JWT token generation and adds Authorization headers. The returned client and its Transport should not be modified.
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSourceReturns a JWT TokenSource using the configuration and HTTP client from the context. Most users should use Client instead.
type ClaimSet struct {
// Issuer in format: urn:atlassian:connect:clientid:{ClientID}
Issuer string `json:"iss"`
// Subject in format: urn:atlassian:connect:useraccountid:{Subject}
Subject string `json:"sub"`
// InstalledURL is the URL of the installed app
InstalledURL string `json:"tnt"`
// AuthURL is the URL of the auth server
AuthURL string `json:"aud"`
// ExpiresIn must be no later than 60 seconds in the future
ExpiresIn int64 `json:"exp"`
// IssuedAt is the timestamp when the token was issued
IssuedAt int64 `json:"iat"`
}package main
import (
"context"
"fmt"
"io"
"log"
"golang.org/x/oauth2"
"golang.org/x/oauth2/jira"
)
func main() {
ctx := context.Background()
// Configure JIRA OAuth
config := &jira.Config{
BaseURL: "https://your-domain.atlassian.net",
Subject: "user-account-id", // Get from /rest/api/2/user?username=alex
Config: oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret", // Shared secret
Endpoint: oauth2.Endpoint{
AuthURL: "https://your-domain.atlassian.net",
TokenURL: "https://your-domain.atlassian.net/rest/oauth2/token",
},
Scopes: []string{"read:jira-user", "write:jira-work"},
},
}
// Create authenticated client
client := config.Client(ctx)
// Use client to access JIRA API
resp, err := client.Get("https://your-domain.atlassian.net/rest/api/2/myself")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("User info: %s\n", body)
}The Subject field requires the user account ID, not the username:
// Make a request to get the user account ID
resp, err := http.Get("https://your-domain.atlassian.net/rest/api/2/user?username=alex")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
var user struct {
AccountID string `json:"accountId"`
}
json.NewDecoder(resp.Body).Decode(&user)
// Use this accountId as the Subject
config.Subject = user.AccountIDScopes are automatically converted to uppercase and joined with +:
config.Scopes = []string{
"read:jira-user",
"read:jira-work",
"write:jira-work",
"manage:jira-project",
"manage:jira-configuration",
}Common scopes:
read:jira-user - Read user informationread:jira-work - Read issues, projects, etc.write:jira-work - Create and update issues, commentsmanage:jira-project - Manage projectsmanage:jira-configuration - Manage Jira configurationmanage:jira-webhook - Manage webhooksThe same package works for Confluence:
config := &jira.Config{
BaseURL: "https://your-domain.atlassian.net/wiki",
Subject: "user-account-id",
Config: oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: "https://your-domain.atlassian.net/wiki",
TokenURL: "https://your-domain.atlassian.net/wiki/rest/oauth2/token",
},
Scopes: []string{"read:confluence-content.all", "write:confluence-content"},
},
}
client := config.Client(ctx)
resp, _ := client.Get("https://your-domain.atlassian.net/wiki/rest/api/content")package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"golang.org/x/oauth2"
"golang.org/x/oauth2/jira"
)
type JiraIssue struct {
Key string `json:"key"`
Fields struct {
Summary string `json:"summary"`
Description string `json:"description"`
} `json:"fields"`
}
func main() {
ctx := context.Background()
config := &jira.Config{
BaseURL: "https://mycompany.atlassian.net",
Subject: "557058:12345678-1234-1234-1234-123456789abc",
Config: oauth2.Config{
ClientID: "oauth-client-id",
ClientSecret: "shared-secret-key",
Endpoint: oauth2.Endpoint{
AuthURL: "https://mycompany.atlassian.net",
TokenURL: "https://mycompany.atlassian.net/rest/oauth2/token",
},
Scopes: []string{
"read:jira-work",
"write:jira-work",
},
},
}
// Get token source
ts := config.TokenSource(ctx)
// Get a token to verify configuration
token, err := ts.Token()
if err != nil {
log.Fatalf("Failed to get token: %v", err)
}
fmt.Printf("Got access token (expires: %v)\n", token.Expiry)
// Create client
client := config.Client(ctx)
// Fetch an issue
resp, err := client.Get("https://mycompany.atlassian.net/rest/api/2/issue/PROJ-123")
if err != nil {
log.Fatalf("Failed to fetch issue: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Fatalf("API error: %s - %s", resp.Status, body)
}
var issue JiraIssue
if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil {
log.Fatalf("Failed to decode response: %v", err)
}
fmt.Printf("Issue: %s\n", issue.Key)
fmt.Printf("Summary: %s\n", issue.Fields.Summary)
// Create a new issue
newIssue := map[string]any{
"fields": map[string]any{
"project": map[string]string{
"key": "PROJ",
},
"summary": "New issue created via API",
"description": "This issue was created using the JIRA OAuth2 package",
"issuetype": map[string]string{
"name": "Task",
},
},
}
issueJSON, _ := json.Marshal(newIssue)
createResp, err := client.Post(
"https://mycompany.atlassian.net/rest/api/2/issue",
"application/json",
bytes.NewReader(issueJSON),
)
if err != nil {
log.Fatalf("Failed to create issue: %v", err)
}
defer createResp.Body.Close()
if createResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(createResp.Body)
log.Fatalf("Failed to create issue: %s - %s", createResp.Status, body)
}
var created struct {
Key string `json:"key"`
}
json.NewDecoder(createResp.Body).Decode(&created)
fmt.Printf("Created issue: %s\n", created.Key)
}urn:ietf:params:oauth:grant-type:jwt-bearer| Feature | JIRA OAuth | Standard OAuth2 |
|---|---|---|
| Signing | HMAC-SHA256 | RSA (typically) |
| Token lifetime | 59 seconds | Configurable |
| Grant type | JWT Bearer | Various |
| User identifier | Account ID | Email/username |
| Scope format | Uppercase + joined | Space-separated |
Ensure you're using the account ID, not username:
// Wrong
config.Subject = "alex"
// Correct
config.Subject = "557058:12345678-1234-1234-1234-123456789abc"The 59-second limit is intentional. The client automatically refreshes:
// Don't manually refresh - the client handles it
client := config.Client(ctx)
// Each request automatically gets a fresh token if neededScopes are automatically converted to uppercase:
// Both work the same
config.Scopes = []string{"read:jira-work"}
config.Scopes = []string{"READ:JIRA-WORK"}Common JIRA REST API endpoints:
// User info
GET /rest/api/2/myself
// Get issue
GET /rest/api/2/issue/{issueKey}
// Search issues
POST /rest/api/2/search
Body: {"jql": "project = PROJ"}
// Create issue
POST /rest/api/2/issue
Body: {"fields": {...}}
// Update issue
PUT /rest/api/2/issue/{issueKey}
// Add comment
POST /rest/api/2/issue/{issueKey}/comment
Body: {"body": "Comment text"}Install with Tessl CLI
npx tessl i tessl/golang-golang-org-x--oauth2