The Plugin Secrets Service provides automatic namespace isolation for third-party plugins, ensuring complete secret isolation between plugins without any manual configuration.
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:
PluginSecretServiceExceptionTo use the Plugin Secrets Service, you need:
Follow these steps to start using the Secrets Service in your plugin:
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.
Import the PluginSecretService OSGi service, for example with @ComponentImport:
1 2import 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.
Use the service to store sensitive data:
1 2public 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.
Store a single secret:
1 2secretService.put("api-key", "sk-1234567890abcdef");
Store multiple secrets at once:
1 2Map<String, String> secrets = Map.of( "api-key", "sk-1234567890abcdef", "webhook-secret", "whsec_abcdef123456", "oauth-token", "oauth_token_xyz" ); secretService.put(secrets);
Retrieve a single secret:
1 2Optional<String> apiKey = secretService.get("api-key"); if (apiKey.isPresent()) { // Use the API key String key = apiKey.get(); }
Retrieve multiple secrets at once:
1 2Set<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 a single secret:
1 2secretService.delete("api-key");
Delete multiple secrets at once:
1 2Set<String> identifiers = Set.of("api-key", "webhook-secret", "oauth-token"); secretService.delete(identifiers);
All operations throw PluginSecretServiceException on errors. Handle them appropriately:
1 2import 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.) }
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.
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.
When you obtain a reference to PluginSecretService, the system automatically:
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:
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.
Symptom: Your plugin cannot obtain a reference to PluginSecretService.
Cause: Service not available or incorrect dependency configuration.
Solution:
Verify the dependency scope is provided in pom.xml:
1 2<scope>provided</scope>
If using @ComponentImport, ensure it's on the parameter:
1 2public MyService(@ComponentImport PluginSecretService secretService) {
Check that your plugin's minimum product version supports the Secrets API (Data Center 8.0+)
The PluginSecretService interface extends SecretOperations and provides the following methods:
Store a secret:
1 2void put(String identifier, String secretData)
PluginSecretServiceException on errorRetrieve a secret:
1 2Optional<String> get(String identifier)
Optional<String> containing the secret if found, or empty Optional if not foundPluginSecretServiceException on errorDelete a secret:
1 2void delete(String identifier)
PluginSecretServiceException on errorStore multiple secrets:
1 2void put(Map<String, String> secrets)
PluginSecretServiceException on errorRetrieve multiple secrets:
1 2Map<String, Optional<String>> get(Set<String> identifiers)
Optional<String> containing the secret if foundPluginSecretServiceException on errorDelete multiple secrets:
1 2void delete(Set<String> identifiers)
PluginSecretServiceException on errorAll operations throw PluginSecretServiceException, which extends SecretOperationsException. This is an unchecked exception (extends RuntimeException).
1 2RuntimeException └── SecretOperationsException └── PluginSecretServiceException
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);
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 catch and handle PluginSecretServiceException to provide good user experience:
1 2try { 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(); }
Clean up secrets when they're no longer needed to minimize exposure:
1 2public void disconnectService() { // Remove credentials when user disconnects the integration secretService.delete(Set.of( "api-key", "api-secret", "oauth-token" )); }
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);
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 ); }
If you need help with the Plugin Secrets Service:
Rate this page: