Skip to main content

Context:

 

The Endpoint Credential Manager (ECM) Software Development Kit for BeyondTrust Privileged Remote Access (PRA)  and Remote Support (RS)  allows developers to leverage a Credential Provider other than the one included (Vault) to allow Users to inject credentials when for accessing a Jump Client, a Web Jump, etc. This guide covers a specific example:  Password Safe.

 

Important:  There is already an official Password Safe ECM Plugin, and this tutorial’s objective is not to position this Plugin example as a replacement.  Password Safe is just a good example for us to explore some additional Use Cases, specifically for how to leverage session information and credential check-in.

 

 This guide also shows how to create a Custom Source to manage the configuration and credentials needed for the Plugin to authenticate to PRA and RS on one side, and to Password Safe  on the other side.

 

Jump Client example leveraging Password Safe.

 

Note: Secure Remote Access (SRA) is used to refer to both PRA and RS.

 

Disclaimer

The Endpoint Credential Manager (ECM) Software Development Kit

Allows developers to create Custom ECM Plugins. The SDK comes with a Plugin example, which has been used as a starting point to create a new Plugin.

 

Any sample or proof of concept code (“Code”) provided on the Community is provided “as is” and without any express or implied warranties. This means that we do not promise that it will work for your specific needs or that it is error-free. Such Code is community supported and not supported directly by BeyondTrust, and it is not intended to be used in a production environment. BeyondTrust and its contributors are not liable for any damage you or others might experience from using the Code, including but not limited to, loss of data, loss of profits, or any interruptions to your business, no matter what the cause is, even if advised of the possibility of such damage.

 

Capabilities

  • List secrets from Password Safe
  • Inject the selected secret into a Jump session
  • Support for SRA Local and LDAP/AD Service Providers
  • Credential Check-in when Jump session ends
  • Optional: Use Environment Variables for the Configuration for a deployment via Jenkins or another CI/CD automation solution.

 

Configure Password Safe

 

Create an API Registration and configure Authentication Rules to allow Visual Studio and optionally Docker hosts IP addresses.

 

Assign the API Registration to a test Group (Local and/or AD)  and add test Users as members.  Also assign Smart Groups for Managed Accounts.

 

Assign Password Safe Account Management – Read Only Feature.  This is required for the Plugin to retrieve the Managed Account attributes needed for credential injection.

 

For Jump hosts, make sure Password Safe includes Managed Systems with the same names as SRA.  The Plugin is filtering on Computer Names.

 

For Web Jumps, create matchings Managed Systems by name, and add Manage Accounts for credential injection.

 

Obtain and deploy the ECM Runtime and ECM SDK Example Plugin

 

The documentation for the ECM SDK is available here: 

 

https://www.beyondtrust.com/docs/privileged-remote-access/how-to/integrations/ecm-sdk/index.htm

 

Extract the Example Plugin archive into a subdirectory (renamed).

 

Open the Solution/Project with Visual Studio with .NET v8 support.

 

Add NuGet packages required by the Plugin.

 

Here is a quick list of packages that need to be added:

  • Json.NET (Newtonsoft.Json);
  • Microsoft.Bcl.AsyncInterfaces;
  • Microsoft.Bcl.Cryptography;
  • System.Formats.Asn1;
  • System.ComponentModel.Composition;
  • Microsoft.Extensions.Hosting;
  • Microsoft.Extensions.Hosting.Abstractions;
  • Microsoft.Extensions.Hosting.WindowsServices;
  • Microsoft.Extensions.Logging;
  • System.Configuration.ConfigurationManager;
  • System.DirectoryServices;
  • System.Runtime.Caching;
  • System.Security.Cryptography.Pkcs;
  • System.Security.Cryptography.ProtectedData;
  • System.ServiceModel.Primitives;
  • System.ServiceProcess.ServiceController .

 

ExamplePluginConfig.cs:  configure the required config values for Password Safe.

 

namespace MyCompany.Integrations.ExamplePlugin
{
/// <summary>
/// Represents a simple POCO that defines the config values necessary for the plugin to operate
/// These are simply examples of the types of items which might be required for your implementation
/// </summary>
public sealed record ExamplePluginConfig
{
public required string ApiBaseUrl { get; init; }
public required string ApiKey { get; init; }
public required string OAuthClientId { get; init; }
public required string OAuthClientSecret { get; init; }
public bool EnableSpecialCase { get; init; } = false;
public string? Domains { get; init; }
}
}

 

appsettings.json: These are the config values for default Sources.

 

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ECMConfig": {
"SRASiteHostname": "",
"SRAClientId": "",
"SRAClientSecret": ""
},
"ExamplePluginConfig": {
"ApiBaseUrl": "https://test.mycompany.com/path/to/api",
"OAuthClientId": "MyClientId-12345-asdfasdf",
"OAuthClientSecret": "super-secret-client-secret",
"ApiKey": "12345",
"EnableSpecialCase": true,
"Domains":
{
"Dn": "dc=btintegrations,dc=cloud",
"Domain": "btintegrations.cloud"
},
{
"Dn": "dc=btlab,dc=cloud",
"Domain": "btlab.cloud"
}
]
}
}

 

In your PRA or RS instance, an API account needs to be created with the Endpoint Credential Manager API/Allow Access permission.

 

Note: While it is still possible to leverage appsettings.json for config values, the option to use Environment Variables is covered in this guide, with the use of a CI/CD Solution like Jenkins in mind, to deploy the plugin as a Container, e.g. using for example a Microsoft provided Docker image.

 

See for example:  https://learn.microsoft.com/en-us/dotnet/architecture/microservices/net-core-net-framework-containers/official-net-docker-images

 

PluginRunner.cs:  Add Custom Source for Password Safe via Environment Variables.

 

//***** Custom Source - Environment Variables - BEGIN
ExamplePluginConfig ExamplePluginConfig = new ExamplePluginConfig
{
ApiBaseUrl = Environment.GetEnvironmentVariable("ApiBaseUrl"),
ApiKey = Environment.GetEnvironmentVariable("ApiKey"),
OAuthClientId = Environment.GetEnvironmentVariable("OAuthClientId"),
OAuthClientSecret = Environment.GetEnvironmentVariable("OAuthClientSecret"),
Domains = Environment.GetEnvironmentVariable("Domains")
};
builder.Services.Configure<ExamplePluginConfig>(builder.Configuration);
//***** Custom Source - Environment Variables - END

 

PluginRunner.cs: Add Custom Source for SRA via Environment Variables.

 

//***** Custom Source - Environment Variables - BEGIN
ECMConfig ECMConfig = new ECMConfig {
SRASiteHostname = Environment.GetEnvironmentVariable("SRASiteHostName"),
SRAClientId = Environment.GetEnvironmentVariable("SRAClientId"),
SRAClientSecret = Environment.GetEnvironmentVariable("SRAClientSecret")
};

builder.Services.Configure<ECMConfig>(builder.Configuration);
//***** Custom Source - Environment Variables – END

 

ExamplePlugin.cs: We need to add our custom reusable functions or classes specific to Password Safe.

 

        // Custom Code - START
private static async Task<string> GetAuthToken(string ApiUrl, string CLIENT_ID, string CLIENT_SECRET)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" }
}
};
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"grant_type", "client_credentials"} ,
{"client_id", CLIENT_ID} ,
{"client_secret", CLIENT_SECRET}
}
);
HttpResponseMessage message = httpClient.PostAsync(ApiUrl + "/psauth/connect/token", content).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Authenticate " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Authenticate " + ex.ToString());
}
}

private static async Task<string> SignAppinOAuth(string ApiUrl, string BEARER_TOKEN)
{
try
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", BEARER_TOKEN);
HttpResponseMessage message = httpClient.PostAsync(ApiUrl + "/BeyondTrust/api/public/v3/Auth/SignAppin", null).Result;
string result = await message.Content.ReadAsStringAsync();
IEnumerable<string> cookieHeaders;
message.Headers.TryGetValues("Set-Cookie", out cookieHeaders);
if (message.IsSuccessStatusCode)
{
return cookieHeaders.First();
}
else
{
throw new Exception("Failed to GET Session Cookie " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to GET Session Cookie " + ex.ToString());
}
}

private static async Task<string> SignAppinApiKey(string ApiUrl, string ApiKey, string Username, string Domains, string DistinguishedName)
{
try
{
var API_userName = Username;
if (Domains != "")
{
dynamic DomainPrefix = JsonConvert.DeserializeObject(Domains);
foreach (var i in DomainPrefix)
{
Console.Write("####### Domain = " + i.ToString() + " Dn = " + i.Dn + " Domain = " + i.Domain + "\n");
string Dn = i.Dn;
if (DistinguishedName.IndexOf(Dn) > 0)
{
API_userName = i.Domain + "\\" + API_userName;
}
}
}
Console.Write("####### API_userName = " + API_userName + " \n");
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Authorization", "PS-Auth key=" + ApiKey + ";runas=" + API_userName}
}
};
HttpResponseMessage message = httpClient.PostAsync(ApiUrl + "/BeyondTrust/api/public/v3/Auth/SignAppin", null).Result;
//string result = await message.Content.ReadAsStringAsync();
string cookie = message.Headers.TryGetValues("Set-Cookie", out var values) ? values.FirstOrDefault() : null;
if (message.IsSuccessStatusCode)
{
return cookie;
}
else
{
throw new Exception("Failed to GET Session Cookie " + cookie);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to GET Session Cookie " + ex.ToString());
}
}

private static async Task<string> GetManagedAccounts(string ApiUrl, string cookie, string SystemName)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Cookie", cookie }
}
};
HttpResponseMessage message = httpClient.GetAsync(ApiUrl + "/BeyondTrust/api/public/v3/ManagedAccounts?systemName=" + SystemName).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Discover Managed Accounts " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Discover Managed Accounts " + ex.ToString());
}
}

private static async Task<string> GetManagedAccount(string ApiUrl, string cookie, string AccountID)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Cookie", cookie }
}
};
HttpResponseMessage message = httpClient.GetAsync(ApiUrl + "/BeyondTrust/api/public/v3/ManagedAccounts/" + AccountID).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Get Account " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Get Account " + ex.ToString());
}
}

private static async Task<string> PostRequest(string ApiUrl, string cookie, string SystemID, string AccountID)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Cookie", cookie }
}
};
string myJson = "{\"AccessType\":\"view\",\"SystemID\": \"" + SystemID + "\",\"AccountID\":\"" + AccountID + "\",\"DurationMinutes\": 10,\"Reason\": \"ECM Plugin\",\"ConflictOption\":\"reuse\"}";
Console.Write("####### create myJson = " + myJson + "\n");
var httpContent = new StringContent(myJson, Encoding.UTF8, "application/json");
HttpResponseMessage message = httpClient.PostAsync(ApiUrl + "/BeyondTrust/api/public/v3/Requests", httpContent).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Post Request " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Post Request " + ex.ToString());
}
}

private static async Task<string> GetCredentials(string ApiUrl, string cookie, string RequestID)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Cookie", cookie }
}
};
HttpResponseMessage message = httpClient.GetAsync(ApiUrl + "/BeyondTrust/api/public/v3/Credentials/" + RequestID + "?type=password").Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Get Credentials " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Get Credentials " + ex.ToString());
}
}

private static async Task<string> GetRequests(string ApiUrl, string cookie)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Cookie", cookie }
}
};
HttpResponseMessage message = httpClient.GetAsync(ApiUrl + "/BeyondTrust/api/public/v3/Requests").Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to GET Requests" + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to GET Requests " + ex.ToString());
}
}

private static async Task<string> CheckinRequest(string ApiUrl, string cookie, string RequestID)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Cookie", cookie }
}
};
string myJson = "{\"Reason\": \"ECM Plugin\"}";
Console.Write("####### create myJson = " + myJson + "\n");
var httpContent = new StringContent(myJson, Encoding.UTF8, "application/json");
HttpResponseMessage message = httpClient.PutAsync(ApiUrl + "/BeyondTrust/api/public/v3/Requests/" + RequestID + "/Checkin", httpContent).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Post Request " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Post Request " + ex.ToString());
}
}

private static async Task<string> Signout(string ApiUrl, string BEARER_TOKEN, string cookie)
{
try
{
var httpClient = new HttpClient
{
DefaultRequestHeaders = {
{ "User-Agent", "BeyondTrust Secure Remote Access" },
{"Cookie", cookie }
}
};
HttpResponseMessage message = httpClient.PostAsync(ApiUrl + "/BeyondTrust/api/public/v3/Auth/Signout", null).Result;
string result = await message.Content.ReadAsStringAsync();
if (message.IsSuccessStatusCode)
{
return result;
}
else
{
throw new Exception("Failed to Sign Out " + result);
}
}
catch (Exception ex)
{
Console.Write("####### Error in method = " + ex.ToString());
throw new Exception("Failed to Sign Out " + ex.ToString());
}
}
// Custom Code - END

 

Note:  Even if the methods above include Authentication with OAuth, we use the API Key methods instead, so the Plugin can impersonate the end-user and only show Managed Accounts for which the end-user is authorized.

 

ExamplePlugin.cs: We need to make our Example Plugin Config available.

 

        private readonly ExamplePluginConfig examplePluginConfig;

public ExamplePlugin(IOptions<ExamplePluginConfig> config)
{
// Possible tasks to handle when the service is instantiated:
// - validate configuration
// - initialize any sort of API client or other types that will be required for servicing requests
// - pre-load any information that may be required for servicing requests (files, API / product version info, etc.)
examplePluginConfig = config.Value;
}

 

ExamplePlugin.cs: This is our code for FindCredentialsForSessionAsync required Action.

 

        public async Task<ActionResult<IList<CredentialSummary>>> FindCredentialsForSessionAsync(SRASession session)
{
// At this point the user has chosen a Jump Item in the console and attempted to initiate a session to the target system.
// The plugin should retrieve a list of credentials that are either queried or filtered based on the supplied
// information about the user and system to which the user is connecting.
// NOTE: While the operation is async, the SRA site will only wait for a certain period of time (20 seconds by default)
// for the list to be returned before it proceeds without presenting any creds for selection.

try
{
// GET Accounts List
// Authenticate - GET Session Cookie using API Key
Console.Write("####### session.JumpItem = " + session.JumpItem.Value.ToString() + " \n");
Console.Write("####### session.User = " + session.User.Value.ToString() + " \n");
Console.Write("####### User = " + session.User.Value.Username + " \n");
Console.Write("####### Domains = " + examplePluginConfig.Domains + " \n");
var cookie = await SignAppinApiKey(examplePluginConfig.ApiBaseUrl, examplePluginConfig.ApiKey, session.User.Value.Username, examplePluginConfig.Domains, session.User.Value.DistinguishedName);
Console.Write("We got cookie" + "\n");
// Get Managed Accounts for ComputerName/SystemName
string SystemName = session.JumpItem.Value.ComputerName;
Console.Write("####### session.JumpItem.Value.ComputerName = " + SystemName + " \n");
var accounts = await GetManagedAccounts(examplePluginConfig.ApiBaseUrl, cookie, SystemName);
Console.Write("We got Managed Accounts \n");
dynamic jsonString_accounts = JsonConvert.DeserializeObject(accounts);
List<CredentialSummary> credentials = new List<CredentialSummary>();
foreach (var i in jsonString_accounts)
{
Console.Write("####### Discovered Account = " + i.AccountName + " id = " + i.AccountId + " SystemdId = " + i.SystemId + " userName = " + i.AccountName + "\n");
Console.Write("####### Discovered Domain = " + i.DomainName + "\n");
string userName = "";
if (i.DomainName != null)
{
userName = i.DomainName + "\\" + i.AccountName;
}
else {
userName = i.AccountName;
}

CredentialSummary credential = new CredentialSummary { CredentialId = i.AccountId + ":" + i.SystemId, DisplayValue = "Password Safe : " + userName };
credentials.Add(credential);
}

// Then put that all in an ActionResult object to be returned
return new ActionResult<IList<CredentialSummary>> { ResultValue = credentials, IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get list of Credentials");
return new ActionResult<IList<CredentialSummary>> { ResultValue = null, FailureReason = ex.Message, IsSuccess = false };
}

// COMMENTED - throw new NotImplementedException($"The {Name} definition must implement {nameof(FindCredentialsForSessionAsync)} as defined by {nameof(ICredentialActions)}");
}

 

ExamplePlugin.cs: This is our code for the GetCredentialForInjectionAction required Action.

 

        public async Task<ActionResult<CredentialPackage>> GetCredentialForInjectionAsync(SRASession session, string credentialId)
{
// The user has been presented with a list of credentials (supplied by FindCredentialsForSessionAsync above) and has
// selected one from the list. The plugin should use the information supplied about the session as well as the ID of
// the selected credential to attempt to retrieve / check-out that credential and return it for injection into the session.
// NOTE: As above, it is important to note that while the operation is async, the same timeout applies.

try
{
// Authenticate - GET Session Cookie using API Key
Console.Write("####### User = " + session.User.ToString() + " \n");
var cookie = await SignAppinApiKey(examplePluginConfig.ApiBaseUrl, examplePluginConfig.ApiKey, session.User.Value.Username, examplePluginConfig.Domains, session.User.Value.DistinguishedName);
Console.Write("We got cookie" + "\n");
// Extract AccountId and SystemId from credentialId
stringx] Ids = credentialId.Split(':');
string AccountId = Ids 0];
string SystemId = Ids 1];
// GET userName from Account details
var account = await GetManagedAccount(examplePluginConfig.ApiBaseUrl, cookie, AccountId);
dynamic jsonString_account = JsonConvert.DeserializeObject(account);
string DomainName = jsonString_account.DomainName;
string AccountName = jsonString_account.AccountName;
string userName = "";
if (DomainName.Length != 0)
{
userName = DomainName + "\\" + AccountName;
}
else
{
userName = AccountName;
}
Console.Write("####### Retrieved userName = " + userName + "\n");
// Get Secret
Console.Write("####### Get secret AccountId = " + AccountId + " SystemId = " + SystemId + " username = " + userName + "\n");
// Create Request
var request = await PostRequest(examplePluginConfig.ApiBaseUrl, cookie, SystemId, AccountId);
string RequestID = request;
Console.Write("####### RequestID = " + RequestID + "\n");
// Get Credentials
var credentials = await GetCredentials(examplePluginConfig.ApiBaseUrl, cookie, RequestID);

dynamic secretValue = credentials.Replace("\"","");

var result = CredentialPackage.BuildUsernamePasswordPackage(credentialId, userName, secretValue);
return new ActionResult<CredentialPackage> { ResultValue = result, IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to inject Credentials");
return new ActionResult<CredentialPackage> { ResultValue = null, FailureReason = ex.Message, IsSuccess = false };
}

// COMMENTED - throw new NotImplementedException($"The {Name} definition must implement {nameof(GetCredentialForInjectionAsync)} as defined by {nameof(ICredentialActions)}");
}

 

This is our code for the Release Credential action.

 

        public async Task<ActionResult> ReleaseCredentialFromSessionAsync(SRASession session, string credentialId, long sessionEndedTimestamp)
{
// And at this point the session has ended and the SRA site is notifying the ECM / plugin in case any clean-up or check-in actions
// need to be performed in order to release the credential so other users can access it.
// NOTE: Any responses that return an ActionResult indicating failure will result in the SRA site retrying the request until it is met
// with a successful response or the request eventually times out of the queue. If it is unlikely that a given error scenario will
// ever result in success, then it is best to return a successful response here but adequately log anything that went wrong.
try {
// Authenticate - GET Session Cookie using API Key
Console.Write("####### User = " + session.User.ToString() + " \n");
var cookie = await SignAppinApiKey(examplePluginConfig.ApiBaseUrl, examplePluginConfig.ApiKey, session.User.Value.Username, examplePluginConfig.Domains, session.User.Value.DistinguishedName);
Console.Write("We got cookie" + "\n");
// GET Requests
var requests = await GetRequests(examplePluginConfig.ApiBaseUrl, cookie);
Console.Write("We got Requests = " + requests.ToString() + "\n");
dynamic jsonString_requests = JsonConvert.DeserializeObject(requests);
// Extract AccountId and SystemId from credentialId
Console.Write("####### We are looking into Requests response for a match for credentialId = " + credentialId + "\n");
if(credentialId.IndexOf(":") > 0) {
string ] Ids = credentialId.Split(':');
string AccountId = Ids[0];
string SystemId = Ids 1];
foreach (var i in jsonString_requests)
{
if (i.SystemID == SystemId && i.AccountID == AccountId)
{
Console.Write("####### We found a Request to Check In for SystemID = " + SystemId + " and AccountID = " + AccountId + "\n");
Console.Write("####### Check In RequestID = " + i.RequestID + "\n");
string RequestID = i.RequestID;
var checkin = await CheckinRequest(examplePluginConfig.ApiBaseUrl, cookie, RequestID);
Console.Write("We Checked In RequestID = " + i.RequestID + "\n");
}
}
}
return new ActionResult { IsSuccess = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to Check In Request");
return new ActionResult {FailureReason = ex.Message, IsSuccess = false };
}
//throw new NotImplementedException($"The {Name} definition must implement {nameof(ReleaseCredentialFromSessionAsync)} as defined by {nameof(ICredentialActions)}");
}

 

Project Properties: We need to add our Environment Variables to be able to use Debug within Visual Studio. This is for testing only, not required for publishing the App.

 

Build and Debug

 

We should be able to see the connection established with SRA, and discovered Secrets from Password Safe when Jumping into a Client.

 

The drop-down list of available credentials should now include our Password Safe  secrets.

 

Next Steps

 

Once we can successfully Build and test the Plugin App via Debug in Visual Studio, the App and all its dependencies can be deployed.  It is not recommended to use appsettings.json to store sensitive configuration information including credentials, so another config Source should be used. Several examples are available on the web, and covering them is out of scope for this guide.  However, this guide does include an example using Environment Variables, which can make sense for when the Plugin App is containerized and a CI/CD solution like Jenkins is used to deploy the container to Docker or Kubernetes.

 

See part 2 for how to build a Docker image:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Be the first to reply!

Reply