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"}