Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,34 @@ https://your-site.atlassian.net/_edge/tenant_info

The response contains a `cloudId` field. Copy it into `--cloud-id` or `JIRA_CLOUD_ID`.

### mTLS Authentication (Client Certificate)

For self-hosted or reverse-proxied JIRA deployments that authenticate at the TLS layer with client certificates:

#### Command Line Configuration
```bash
jira config --server https://jira.example.com \
--auth-type mtls \
--tls-client-cert ~/.certs/client.pem \
--tls-client-key ~/.certs/client.key \
--tls-ca-cert ~/.certs/ca-chain.pem
```

#### Environment Variables
```bash
export JIRA_HOST="jira.example.com"
export JIRA_AUTH_TYPE="mtls"
export JIRA_TLS_CLIENT_CERT="~/.certs/client.pem"
export JIRA_TLS_CLIENT_KEY="~/.certs/client.key"
export JIRA_TLS_CA_CERT="~/.certs/ca-chain.pem" # optional
```

**Notes:**
- mTLS mode does not send an `Authorization` header; authentication happens at the TLS layer
- The CA certificate is optional if your client certificate is signed by a well-known CA
- mTLS is commonly used in enterprise environments with private certificate authorities
- Paths beginning with `~/` are expanded to your home directory; certificate files are read at startup, so update the cert paths if they change

### Getting Your API Token

1. Go to [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens)
Expand Down
87 changes: 77 additions & 10 deletions bin/commands/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { Command } = require('commander');
const fs = require('fs');
const { expandHomePath } = require('../../lib/utils');

function createConfigCommand(factory) {
const command = new Command('config')
Expand All @@ -9,21 +11,34 @@ function createConfigCommand(factory) {
.option('--username <username>', 'set username')
.option('--token <token>', 'set API token')
.option('--cloud-id <cloudId>', 'set Atlassian Cloud ID for scoped API tokens')
.option('--auth-type <type>', 'authentication type (basic, bearer, or mtls)')
.option('--tls-client-cert <path>', 'client certificate for mTLS authentication')
.option('--tls-client-key <path>', 'client private key for mTLS authentication')
.option('--tls-ca-cert <path>', 'CA certificate for mTLS authentication (optional)')
.action(async (options) => {
const io = factory.getIOStreams();
const config = factory.getConfig();
const analytics = factory.getAnalytics();

try {
await analytics.track('config', { action: getConfigAction(options) });

if (options.show) {
// Show current configuration
config.displayConfig();
return;
}

if (options.server || options.username || options.token || options.cloudId) {
if (
options.server ||
options.username ||
options.token ||
options.cloudId ||
options.authType ||
options.tlsClientCert ||
options.tlsClientKey ||
options.tlsCaCert
) {
// Set individual configuration values
if (options.server) {
config.set('server', options.server.replace(/\/$/, ''));
Expand All @@ -45,6 +60,40 @@ function createConfigCommand(factory) {
io.success(`Cloud ID set to: ${options.cloudId} (requests will route via Atlassian Platform API Gateway)`);
}

if (options.authType) {
const authType = options.authType.toLowerCase();
if (!['basic', 'bearer', 'mtls'].includes(authType)) {
throw new Error('--auth-type must be "basic", "bearer", or "mtls"');
}
config.set('authType', authType);
io.success(`Auth type set to: ${authType}`);
}

// mTLS certificate configuration
if (options.tlsClientCert) {
if (!fs.existsSync(expandHomePath(options.tlsClientCert))) {
throw new Error(`Client certificate file not found: ${options.tlsClientCert}`);
}
config.set('tlsClientCert', options.tlsClientCert);
io.success('TLS client certificate configured');
}

if (options.tlsClientKey) {
if (!fs.existsSync(expandHomePath(options.tlsClientKey))) {
throw new Error(`Client key file not found: ${options.tlsClientKey}`);
}
config.set('tlsClientKey', options.tlsClientKey);
io.success('TLS client key configured');
}

if (options.tlsCaCert) {
if (!fs.existsSync(expandHomePath(options.tlsCaCert))) {
throw new Error(`CA certificate file not found: ${options.tlsCaCert}`);
}
config.set('tlsCaCert', options.tlsCaCert);
io.success('TLS CA certificate configured');
}

// Test connection if all required fields are present
if (config.isConfigured()) {
io.info('Testing connection...');
Expand All @@ -67,6 +116,11 @@ function createConfigCommand(factory) {
' jira config --server <url> --username <email> --token <token>\n\n' +
'Scoped API token (Atlassian Cloud, recommended for new tokens):\n' +
' jira config --server <url> --username <email> --token <scoped-token> --cloud-id <cloudId>\n\n' +
'mTLS authentication (for self-hosted/reverse-proxied Jira):\n' +
' jira config --server <url> --auth-type mtls \\\n' +
' --tls-client-cert /path/to/client.pem \\\n' +
' --tls-client-key /path/to/client.key \\\n' +
' --tls-ca-cert /path/to/ca.pem\n\n' +
'Or set using individual commands:\n' +
' jira config set server <url>\n' +
' jira config set token <token>\n' +
Expand All @@ -75,7 +129,9 @@ function createConfigCommand(factory) {
'Or use environment variables:\n' +
' Bearer auth: export JIRA_HOST=<url> JIRA_API_TOKEN=<token>\n' +
' Basic auth: export JIRA_HOST=<url> JIRA_API_TOKEN=<token> JIRA_USERNAME=<email>\n' +
' Scoped token: also export JIRA_CLOUD_ID=<cloudId>'
' Scoped token: also export JIRA_CLOUD_ID=<cloudId>\n' +
' mTLS auth: export JIRA_HOST=<url> JIRA_AUTH_TYPE=mtls \\\n' +
' JIRA_TLS_CLIENT_CERT=<path> JIRA_TLS_CLIENT_KEY=<path>'
);
}

Expand All @@ -92,7 +148,7 @@ function createConfigCommand(factory) {
.action(async (key) => {
const io = factory.getIOStreams();
const config = factory.getConfig();

try {
if (key) {
const value = config.get(key);
Expand All @@ -116,16 +172,16 @@ function createConfigCommand(factory) {
.action(async (key, value) => {
const io = factory.getIOStreams();
const config = factory.getConfig();

try {
config.set(key, value);
io.success(`${key} set successfully`);

// Test connection if setting critical values
if (['server', 'username', 'token', 'cloudId'].includes(key) && config.isConfigured()) {
io.info('Testing connection...');
const testResult = await config.testConfig();

if (testResult.success) {
io.success('Connection verified');
} else {
Expand All @@ -144,7 +200,7 @@ function createConfigCommand(factory) {
.action(async (key) => {
const io = factory.getIOStreams();
const config = factory.getConfig();

try {
config.delete(key);
io.success(`${key} unset successfully`);
Expand All @@ -159,7 +215,18 @@ function createConfigCommand(factory) {

function getConfigAction(options) {
if (options.show) return 'show';
if (options.server || options.username || options.token || options.cloudId) return 'set';
if (
options.server ||
options.username ||
options.token ||
options.cloudId ||
options.authType ||
options.tlsClientCert ||
options.tlsClientKey ||
options.tlsCaCert
) {
return 'set';
}
return 'interactive';
}

Expand Down
Loading
Loading