Developer
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in

Plugin Secrets

The Plugin Secrets Service provides automatic namespace isolation for third-party plugins, ensuring complete secret isolation between plugins without any manual configuration.

Table of contents

Overview

The Plugin Secrets Service allows your plugin to securely store and retrieve sensitive data such as API keys, tokens, and passwords. Each plugin automatically gets its own isolated namespace, ensuring that your secrets remain private and cannot be accessed by other plugins.

Key features:

  • Automatic namespace isolation - Each plugin gets its own isolated secret namespace
  • Zero configuration - Plugin keys are automatically detected via OSGi bundle tracking
  • Simple API - Standard CRUD operations (create, read, update, delete)
  • Batch operations - Efficiently store or retrieve multiple secrets at once
  • Type-safe exceptions - Clear error handling with PluginSecretServiceException

Prerequisites

To use the Plugin Secrets Service, you need:

  • One of the following products:
    • Jira Data Center v11.4
    • Confluence Data Center v10.3
    • Bitbucket Data Center v10.2
  • Basic understanding of OSGi service consumption

Getting started

Follow these steps to start using the Secrets Service in your plugin:

Step 1. Add the dependency

Add the following dependency to your plugin's pom.xml:

1
2
<dependency>
    <groupId>com.atlassian.secrets</groupId>
    <artifactId>atlassian-secrets-public-api</artifactId>
    <version>6.1.0</version>
    <scope>provided</scope>
</dependency>

The provided scope ensures the API classes are loaded from the product instead of being bundled in the plugin.

Step 2. Obtain the service

Import the PluginSecretService OSGi service, for example with @ComponentImport:

1
2
import com.atlassian.secrets.api.plugins.PluginSecretService;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyPluginService {
    private final PluginSecretService secretService;
    
    @Autowired
    public MyPluginService(@ComponentImport PluginSecretService secretService) {
        this.secretService = secretService;
    }
}

The service is automatically scoped to your plugin. You can now store and retrieve secrets without any additional configuration.

Step 3. Store and retrieve secrets

Use the service to store sensitive data:

1
2
public class MyPluginService {
    private final PluginSecretService secretService;
    
    // ... constructor from Step 2
    
    public void configureApiKey(String apiKey) {
        // Store the API key - automatically namespaced to your plugin
        secretService.put("api-key", apiKey);
    }
    
    public Optional<String> getApiKey() {
        // Retrieve the API key - other 3rd party plugins cannot access this key
        return secretService.get("api-key");
    }
    
    public void removeApiKey() {
        // Delete the API key when no longer needed
        secretService.delete("api-key");
    }
}

That's it! Your plugin can now securely store and retrieve secrets.

Usage

Store secrets

Store a single secret:

1
2
secretService.put("api-key", "sk-1234567890abcdef");

Store multiple secrets at once:

1
2
Map<String, String> secrets = Map.of(
    "api-key", "sk-1234567890abcdef",
    "webhook-secret", "whsec_abcdef123456",
    "oauth-token", "oauth_token_xyz"
);
secretService.put(secrets);

Retrieve secrets

Retrieve a single secret:

1
2
Optional<String> apiKey = secretService.get("api-key");
if (apiKey.isPresent()) {
    // Use the API key
    String key = apiKey.get();
}

Retrieve multiple secrets at once:

1
2
Set<String> identifiers = Set.of("api-key", "webhook-secret", "oauth-token");
Map<String, Optional<String>> results = secretService.get(identifiers);

Optional<String> apiKey = results.get("api-key");
Optional<String> webhookSecret = results.get("webhook-secret");

Delete secrets

Delete a single secret:

1
2
secretService.delete("api-key");

Delete multiple secrets at once:

1
2
Set<String> identifiers = Set.of("api-key", "webhook-secret", "oauth-token");
secretService.delete(identifiers);

Handle exceptions

All operations throw PluginSecretServiceException on errors. Handle them appropriately:

1
2
import com.atlassian.secrets.api.plugins.PluginSecretServiceException;

try {
    secretService.put("api-key", apiKey);
} catch (PluginSecretServiceException e) {
    log.error("Failed to store secret", e);
    // Handle the error (e.g., show user message, retry, etc.)
}

How it works

Automatic namespace isolation

When you obtain a reference to PluginSecretService, the system automatically creates an isolated instance just for your plugin. You work with simple identifiers like "api-key", and the system ensures they belong only to your plugin.

Two different plugins can both use the same identifier (for example, "api-key") without any conflicts. Each plugin's secrets are completely isolated - you can only access secrets that your plugin created, and other plugins cannot access yours.

Architecture

The Plugin Secrets Service is built on OSGi's ServiceFactory pattern, which automatically creates a unique service instance for each plugin. This ensures complete isolation without any configuration on your part.

How secrets are isolated

When you obtain a reference to PluginSecretService, the system automatically:

  1. Detects your plugin's unique identifier from your plugin descriptor
  2. Creates a service instance that's bound exclusively to your plugin
  3. Ensures all secrets you store or retrieve belong only to your plugin

Troubleshooting

ClassNotFoundException: PluginSecretService

IllegalStateException: Bundle has no plugin key

Symptom: You see the error IllegalStateException: Bundle has no plugin key when your plugin starts.

Cause: The plugin key cannot be determined from your OSGi manifest.

Solution: Ensure your plugin descriptor has a valid key attribute:

1
2
<atlassian-plugin key="com.yourcompany.yourplugin" ...>

The key must be:

  • Non-empty
  • Not equal to "null" or "-null"
  • Unique across all installed plugins

Secrets are not persisting

Symptom: Secrets disappear after the product restarts.

Cause: This typically indicates the underlying secret storage is not configured correctly in the host product.

Solution: This is a product configuration issue, not a plugin issue. Contact your Atlassian product administrator to ensure secret storage is properly configured.

Unable to obtain PluginSecretService

Symptom: Your plugin cannot obtain a reference to PluginSecretService.

Cause: Service not available or incorrect dependency configuration.

Solution:

  1. Verify the dependency scope is provided in pom.xml:

    1
    2
    <scope>provided</scope>
    
  2. If using @ComponentImport, ensure it's on the parameter:

    1
    2
    public MyService(@ComponentImport PluginSecretService secretService) {
    
  3. Check that your plugin's minimum product version supports the Secrets API (Data Center 8.0+)

API reference

PluginSecretService interface

The PluginSecretService interface extends SecretOperations and provides the following methods:

Single secret operations

Store a secret:

1
2
void put(String identifier, String secretData)
  • identifier - Unique identifier for the secret (e.g., "api-key")
  • secretData - The secret value to store
  • Throws - PluginSecretServiceException on error

Retrieve a secret:

1
2
Optional<String> get(String identifier)
  • identifier - Unique identifier for the secret
  • Returns - Optional<String> containing the secret if found, or empty Optional if not found
  • Throws - PluginSecretServiceException on error

Delete a secret:

1
2
void delete(String identifier)
  • identifier - Unique identifier for the secret to delete
  • Throws - PluginSecretServiceException on error

Batch operations

Store multiple secrets:

1
2
void put(Map<String, String> secrets)
  • secrets - Map of identifiers to secret values
  • Throws - PluginSecretServiceException on error

Retrieve multiple secrets:

1
2
Map<String, Optional<String>> get(Set<String> identifiers)
  • identifiers - Set of identifiers to retrieve
  • Returns - Map where each key is an identifier and value is Optional<String> containing the secret if found
  • Throws - PluginSecretServiceException on error

Delete multiple secrets:

1
2
void delete(Set<String> identifiers)
  • identifiers - Set of identifiers to delete
  • Throws - PluginSecretServiceException on error

Exception hierarchy

All operations throw PluginSecretServiceException, which extends SecretOperationsException. This is an unchecked exception (extends RuntimeException).

1
2
RuntimeException
  └── SecretOperationsException
        └── PluginSecretServiceException

Best practices

Use descriptive identifiers

Choose clear, meaningful identifiers that describe what the secret is for:

1
2
// Good
secretService.put("github-api-token", token);
secretService.put("oauth-client-secret", clientSecret);
secretService.put("webhook-signing-key", signingKey);

// Avoid
secretService.put("key1", token);
secretService.put("secret", clientSecret);
secretService.put("x", signingKey);

Use batch operations for multiple secrets

When working with multiple secrets, use batch operations for better performance:

1
2
// Good - single operation
Map<String, String> secrets = Map.of(
    "api-key", apiKey,
    "api-secret", apiSecret,
    "webhook-url", webhookUrl
);
secretService.put(secrets);

// Avoid - multiple round trips
secretService.put("api-key", apiKey);
secretService.put("api-secret", apiSecret);
secretService.put("webhook-url", webhookUrl);

Always handle exceptions

Always catch and handle PluginSecretServiceException to provide good user experience:

1
2
try {
    secretService.put("api-key", apiKey);
    return Response.ok("API key saved successfully").build();
} catch (PluginSecretServiceException e) {
    log.error("Failed to save API key", e);
    return Response.serverError()
        .entity("Failed to save configuration. Please try again.")
        .build();
}

Delete secrets when no longer needed

Clean up secrets when they're no longer needed to minimize exposure:

1
2
public void disconnectService() {
    // Remove credentials when user disconnects the integration
    secretService.delete(Set.of(
        "api-key",
        "api-secret",
        "oauth-token"
    ));
}

Never log secret values

Never log the actual secret values. Log identifiers only:

1
2
// Good
log.info("Storing secret: {}", identifier);

// NEVER do this
log.info("Storing secret: {} = {}", identifier, secretValue);

Don't expose secrets in REST APIs

Never return actual secret values in your REST API responses:

1
2
// Good - return only whether secret exists
@GET
@Path("/config")
public ConfigResponse getConfig() {
    return new ConfigResponse(
        secretService.get("api-key").isPresent(),  // Only return boolean
        otherConfig
    );
}

// NEVER do this
@GET
@Path("/config")
public ConfigResponse getConfig() {
    return new ConfigResponse(
        secretService.get("api-key").orElse(null),  // Exposing secret!
        otherConfig
    );
}

Support

If you need help with the Plugin Secrets Service:

  1. Check the Troubleshooting section above
  2. Ask questions on the Atlassian Developer Community
  3. Report bugs through Atlassian Support

Rate this page: