GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
Credentials.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Collections.Specialized;
20using System.Diagnostics;
21using System.Drawing;
22using System.IO;
23using System.Linq;
24using System.Net;
25using System.Security.Cryptography;
26using System.Text;
27using System.Text.Json;
28using System.Text.Json.Serialization;
29using System.Threading;
30using System.Threading.Tasks;
34using Org.BouncyCastle.Crypto;
35using Org.BouncyCastle.OpenSsl;
36using Org.BouncyCastle.Security;
37using static GenerallyPositive.Enums;
38
39
40namespace GenerallyPositive
41{
46 public class CredentialsConfig
47 {
48 private static string ConfigFilePath { get; set; } = "./credentialsConfig.json";
49
53 public bool UseRestRedirect { get; set; } = true; // passthru redirect to GPALRESTAPI vs manual auth url where the user gets an authcode string
54
55 // Google
59 public string GoogleConsentBase { get; set; } = "https://accounts.google.com";
63 public string GoogleAuthBase { get; set; } = "https://oauth2.googleapis.com";
67 public string GoogleAuthEndpoint { get; set; } = "/token";
71 public string GoogleRedirectUri { get; set; } = "urn:ietf:wg:oauth:2.0:oob";
75 public string GoogleRestRedirectUri { get; set; } = "http://localhost:3000/access-token";
79 public string GoogleAuthCodeKey { get; set; } = "code";
83 public string GoogleServiceAccountTokenUri { get; set; } = "https://oauth2.googleapis.com/token";
84
85 // Azure
89 public string AzureAuthBase { get; set; } = "https://login.microsoftonline.com/common";
93 public string AzureAuthEndpoint { get; set; } = "/oauth2/v2.0/token";
97 public string AzureRedirectUri { get; set; } = "urn:ietf:wg:oauth:2.0:oob";
98
99 // AWS Cognito
103 public string AWSAuthBase { get; set; } = "https://your-domain.auth.{region}.amazoncognito.com"; // Replace with your Cognito domain
107 public string AWSAuthEndpoint { get; set; } = "/oauth2/token";
111 public string AWSRedirectUri { get; set; } = "urn:ietf:wg:oauth:2.0:oob";
112
113 // Bitwarden
117 public string BitwardenAuthBase { get; set; } = "https://identity.bitwarden.com";
121 public string BitwardenVaultBase { get; set; } = "https://api.bitwarden.com";
125 public string BitwardenAuthEndpoint { get; set; } = "/connect/token";
129 public string BitwardenVaultEndpoint { get; set; } = "/public/vault/items";
130
131 // Shared
135 public string AuthorizationHeader { get; set; } = "Authorization";
139 public string GrantTypeKey { get; set; } = "grant_type";
143 public string GrantTypeValue { get; set; } = "password";
147 public string UsernameKey { get; set; } = "username";
151 public string PasswordKey { get; set; } = "password";
155 public string ClientIdKey { get; set; } = "client_id";
159 public string ClientSecretKey { get; set; } = "client_secret";
163 public string SearchKey { get; set; } = "search";
167 public string AccessTokenKey { get; set; } = "access_token";
168
173 public static CredentialsConfig Load(GPALFile file = null)
174 {
175 if (file != null) ConfigFilePath = file.Filename;
176 if (true == File.Exists(ConfigFilePath))
177 {
178 try
179 {
180 var loaded = new CredentialsConfig();
182 .WithInput((GPALFile)GPAL.File.WithFileName(ConfigFilePath))
183 .SaveTo(ref loaded);
184 return loaded;
185 }
186 catch (Exception ex)
187 {
188 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Failed to load credentials config from file", null, GPALObjectType.None, ex);
189 }
190 }
191 else
192 Save();
193
194 return new CredentialsConfig();
195 }
196
201 public static void Save(GPALFile file = null)
202 {
203 if (file != null) ConfigFilePath = file.Filename;
205 .WithInput(new CredentialsConfig())
206 .SaveTo((GPALFile)GPAL.File.WithFileName(ConfigFilePath));
207 }
208 }
209
213 public class AuthCodePayload
214 {
218 public string Code { get; set; }
222 public string Scope { get; set; }
223
224 public AuthCodePayload()
225 { }
226 }
227
230 public class GoogleServiceAccountKey
231 {
235 public string Type { get; set; }
239 public string ProjectId { get; set; }
243 public string PrivateKeyId { get; set; }
247 public string PrivateKey { get; set; }
251 public string ClientEmail { get; set; }
255 public string ClientId { get; set; }
259 public string AuthUri { get; set; }
263 public string TokenUri { get; set; }
267 public string AuthProviderX509CertUrl { get; set; }
271 public string ClientX509CertUrl { get; set; }
272
273 public GoogleServiceAccountKey() { }
274 }
275
280 {
284 [JsonPropertyName("access_token")]
285 public string AccessToken { get; set; }
286
290 [JsonPropertyName("expires_in")]
291 public int ExpiresIn { get; set; }
292
296 [JsonPropertyName("refresh_token")]
297 public string RefreshToken { get; set; }
298
302 [JsonPropertyName("token_type")]
303 public string TokenType { get; set; }
304 }
305
306
310 internal class BitwardenVaultResponse
311 {
315 public List<BitwardenVaultItem> Data { get; set; }
316 }
317
321 internal class BitwardenVaultItem
322 {
326 public BitwardenLogin Login { get; set; }
327 }
328
332 internal class BitwardenLogin
333 {
337 public string Username { get; set; }
341 public string Password { get; set; }
345 public List<BitwardenUri> Uris { get; set; }
346 }
347
351 internal class BitwardenUri
352 {
356 public string Uri { get; set; }
357 }
358
363 {
367 [JsonPropertyName("access_token")]
368 public string AccessToken { get; set; }
369
373 [JsonPropertyName("expires_in")]
374 public int ExpiresIn { get; set; }
375
379 [JsonPropertyName("refresh_token")]
380 public string RefreshToken { get; set; }
381
385 [JsonPropertyName("token_type")]
386 public string TokenType { get; set; }
387 }
388
393 {
397 [JsonPropertyName("access_token")]
398 public string AccessToken { get; set; }
399
403 [JsonPropertyName("expires_in")]
404 public int ExpiresIn { get; set; }
405
409 [JsonPropertyName("refresh_token")]
410 public string RefreshToken { get; set; }
411
415 [JsonPropertyName("scope")]
416 public string Scope { get; set; }
417
421 [JsonPropertyName("token_type")]
422 public string TokenType { get; set; }
423 }
424
428 public class AWSTokenResponse
429 {
433 [JsonPropertyName("access_token")]
434 public string AccessToken { get; set; }
435
439 [JsonPropertyName("expires_in")]
440 public int ExpiresIn { get; set; }
441
445 [JsonPropertyName("refresh_token")]
446 public string RefreshToken { get; set; }
447
451 [JsonPropertyName("token_type")]
452 public string TokenType { get; set; }
453
457 [JsonPropertyName("id_token")]
458 public string IdToken { get; set; }
459 }
460
464 public class LastPassItem
465 {
469 public string Id { get; set; }
473 public string Name { get; set; }
477 public string Url { get; set; }
481 public string Username { get; set; }
485 public string Password { get; set; }
489 public string Notes { get; set; }
490 }
491
495 public class OnePasswordItem
496 {
500 public string Id { get; set; }
504 public string Title { get; set; }
508 public string Vault { get; set; } // vault name or id
512 public List<OnePasswordField> Fields { get; set; } = new List<OnePasswordField>();
516 public List<OnePasswordUrl> Urls { get; set; }
517 }
518
522 public class OnePasswordUrl
523 {
527 public string Href { get; set; }
528 }
529
533 public class OnePasswordField
534 {
538 public string Id { get; set; }
542 public string Label { get; set; }
546 public string Type { get; set; }
550 public string Value { get; set; }
554 public string Reference { get; set; } // for secrets that are references
555 }
556
560 public class DashlaneItem
561 {
565 public string Id { get; set; }
569 public string Title { get; set; }
573 public string Url { get; set; }
577 public string Username { get; set; }
581 public string Password { get; set; }
585 public string Otp { get; set; }
589 public string Notes { get; set; }
590 }
591
595 public class KeeperRecord
596 {
600 public string RecordUid { get; set; }
604 public string Title { get; set; }
608 public string RecordType { get; set; }
612 public KeeperLoginData Login { get; set; }
616 public string Notes { get; set; }
617 }
618
622 public class KeeperLoginData
623 {
627 public string Username { get; set; }
631 public string Password { get; set; }
635 public string Url { get; set; }
636 }
637
638 [InProgress("Credential class to get access tokens and credentials from password managers. Untested. 50%")]
639 public class Credentials : ICredentials
640 {
644 internal CredentialServiceType ServiceType { get; private set; }
648 public string Username { get; private set; }
652 internal string Password { get; private set; }
656 public string ServiceKey { get; private set; }
661 public WebAuthType WebAuthType { get; private set; }
665 private IRESTClient _keyFromApiRestClient;
669 public string ClientId { get; internal set; }
673 public string ClientSecret { get; internal set; }
677 public string AccessToken { get; internal set; }
681 public string Target { get; private set; }
685 public string Domain { get; private set; }
689 public CredentialsConfig Config { get; private set; }
693 public string AuthCode { get; set; }
697 public string RefreshToken { get; set; }
701 public OAuthScope OAuthScope { get; set; }
705 internal GoogleTokenResponse GoogleTokenResponse { get; set; }
709 internal AWSTokenResponse AWSTokenResponse { get; set; }
713 internal AzureTokenResponse AzureTokenResponse { get; set; }
717 internal BitwardenTokenResponse BitwardenTokenResponse { get; set; }
721 internal string ServiceAccountKeyJson { get; set; }
725 internal DateTime IssueTime { get; set; }
726 private int _expiresIn = 0;
730 internal int ExpiresIn
731 {
732 get => _expiresIn;
733 set
734 {
735 _expiresIn = value;
736 ExpiresAt = DateTime.UtcNow.AddSeconds(_expiresIn);
737 }
738 }
742 internal DateTime ExpiresAt { get; set; }
746 internal Browser.Browser Browser { get; set; }
747
748 // =======================================================
749 // HELPERS
750 // =======================================================
756 internal static (string, string) SafeSplit(string value)
757 {
758 if (string.IsNullOrEmpty(value)) return ("", "");
759 var parts = value.Split(new[] { ':' }, 2);
760 return (parts[0], parts.Length > 1 ? parts[1] : "");
761 }
762
767 internal static bool MatchesDomain(string url, string domain)
768 {
769 if (string.IsNullOrEmpty(domain)) return true;
770 if (string.IsNullOrEmpty(url)) return false;
771 return url.IndexOf(domain, StringComparison.OrdinalIgnoreCase) >= 0;
772 }
773
777 internal static bool MatchesDomain(IEnumerable<string> urls, string domain)
778 {
779 if (string.IsNullOrEmpty(domain)) return true;
780 if (urls == null) return false;
781 return urls.Any(url => MatchesDomain(url, domain));
782 }
783
788 private void EnsureValidToken(out string accessToken)
789 {
790 if (IsTokenExpired())
791 FetchAccessToken(out accessToken);
792 else
793 accessToken = AccessToken;
794 }
800 internal static string GetScopeValue(OAuthScope scope)
801 {
802 switch (scope)
803 {
804 // Google
805 case OAuthScope.Google_Sheets:
806 return "https://www.googleapis.com/auth/spreadsheets";
807 case OAuthScope.Google_Drive:
808 return "https://www.googleapis.com/auth/drive";
809 case OAuthScope.Google_CloudPlatform:
810 return "https://www.googleapis.com/auth/cloud-platform";
811
812 // Azure
813 case OAuthScope.Azure_GraphUserRead:
814 return "https://graph.microsoft.com/User.Read";
815 case OAuthScope.Azure_GraphMailRead:
816 return "https://graph.microsoft.com/Mail.Read";
817 case OAuthScope.Azure_Storage:
818 return "https://storage.azure.com/user_impersonation";
819 case OAuthScope.Azure_Management:
820 return "https://management.azure.com/user_impersonation";
821
822 // AWS
823 case OAuthScope.AWS_CognitoOpenId:
824 return "openid";
825 case OAuthScope.AWS_CognitoProfile:
826 return "profile";
827 case OAuthScope.AWS_APIGatewayCustom:
828 return "myapp/read"; // your custom
829
830 default:
831 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unsupported scope: [{scope}]", scope, GPALObjectType.Other);
832 return string.Empty;
833 }
834 }
835
836 // =======================================================
837 // FLUENT interface
838 // =======================================================
845 {
846 OAuthScope = scope;
847 return this;
848 }
849
855 {
856 AuthCode = authCode;
857 return this;
858 }
859
865 {
866 RefreshToken = refreshToken;
867 return this;
868 }
869
870 internal Credentials(Browser.Browser browser)
871 {
873 Browser = browser;
874 }
875
881 public IAllowCredentialsUsername WithService(CredentialServiceType serviceType)
882 {
883 ServiceType = serviceType;
884 return this;
885 }
886
893 {
894 Username = username;
895 return this;
896 }
897
904 {
905 Password = password;
906 return this;
907 }
908
916 {
917 WebAuthType = webAuthType;
918 return this;
919 }
920
931 {
932 Config.GoogleRestRedirectUri = redirectUrl;
933 return this;
934 }
935
945 {
946 AccessToken = accessToken;
947 return this;
948 }
949
956 {
957 Domain = domain;
958 return this;
959 }
960
965 public IAllowClientSecret WithClientId(string clientId)
966 {
967 ClientId = clientId;
968 return this;
969 }
970
976 {
977 ClientSecret = clientSecret;
978 ServiceKey = $"{ClientId}:{clientSecret}";
979 return this;
980 }
981
987 {
988 ServiceKey = key;
989 var parts = SafeSplit(key);
990 ClientId = parts.Item1;
991 ClientSecret = parts.Item2;
992 return this;
993 }
994
1002 {
1003 string key = Environment.GetEnvironmentVariable(envVariableName, EnvironmentVariableTarget.Process)
1004 ?? Environment.GetEnvironmentVariable(envVariableName, EnvironmentVariableTarget.User)
1005 ?? Environment.GetEnvironmentVariable(envVariableName, EnvironmentVariableTarget.Machine);
1006
1007 if (string.IsNullOrEmpty(key))
1008 {
1010 GPALEventType.ERROR,
1011 $"Environment variable [{envVariableName}] is not set or empty.",
1012 null,
1013 GPALObjectType.None);
1014 return this;
1015 }
1016
1017 return WithServiceKey(key);
1018 }
1019
1028 {
1029 _keyFromApiRestClient = restClient;
1030 return this;
1031 }
1032
1040 {
1041 string key = _keyFromApiRestClient
1042 .WithEndpoint(endpoint)
1043 .Execute();
1044
1045 if (string.IsNullOrEmpty(key))
1046 {
1048 GPALEventType.ERROR,
1049 $"REST API returned no key for endpoint [{endpoint}].",
1050 null,
1051 GPALObjectType.None);
1052 return this;
1053 }
1054
1055 return WithServiceKey(key);
1056 }
1057
1064 {
1065 if (serviceAccountKeyFile == null || string.IsNullOrWhiteSpace(serviceAccountKeyFile.Filename) || !File.Exists(serviceAccountKeyFile.Filename))
1066 {
1068 GPALEventType.ERROR,
1069 $"Service account key file missing or not found: [{serviceAccountKeyFile?.Filename ?? "<null>"}]",
1070 null,
1071 GPALObjectType.None);
1072 return this;
1073 }
1074
1075 ServiceAccountKeyJson = File.ReadAllText(serviceAccountKeyFile.Filename);
1076 return this;
1077 }
1078
1085 {
1086 if (string.IsNullOrWhiteSpace(serviceAccountKey))
1087 {
1089 GPALEventType.ERROR,
1090 "Service account key JSON string is empty.",
1091 null,
1092 GPALObjectType.None);
1093 return this;
1094 }
1095
1096 ServiceAccountKeyJson = serviceAccountKey;
1097 return this;
1098 }
1099
1106 {
1107 if (serviceAccountKey == null)
1108 {
1110 GPALEventType.ERROR,
1111 "Service account key object is null.",
1112 null,
1113 GPALObjectType.None);
1114 return this;
1115 }
1116
1117 try
1118 {
1119 ServiceAccountKeyJson = System.Text.Json.JsonSerializer.Serialize(serviceAccountKey);
1120 }
1121 catch (Exception ex)
1122 {
1124 GPALEventType.EXCEPTION,
1125 $"Failed to serialize service account key object",
1126 null,
1127 GPALObjectType.None, ex);
1128 }
1129 return this;
1130 }
1131
1138 {
1139 Target = target;
1140 if (ServiceType != CredentialServiceType.None)
1141 {
1142 EnsureValidToken(out string accessToken);
1143 }
1144 return this;
1145 }
1146
1157 {
1158 if (credentials is Credentials masterCredentials)
1159 {
1160 Username = masterCredentials.Username;
1161 Password = masterCredentials.Password;
1162 ServiceKey = masterCredentials.ServiceKey;
1163 ClientId = masterCredentials.ClientId;
1164 ClientSecret = masterCredentials.ClientSecret;
1165 ServiceAccountKeyJson = masterCredentials.ServiceAccountKeyJson;
1166 AccessToken = masterCredentials.AccessToken;
1167 RefreshToken = masterCredentials.RefreshToken;
1168 OAuthScope = masterCredentials.OAuthScope;
1169 }
1170 return this;
1171 }
1172
1179 public IAllowGetCredentials SaveTo(IGPALGrid<string> grid)
1180 {
1181 if (grid == null)
1182 {
1183 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Grid cannot be null in SaveTo.", null, GPALObjectType.None);
1184 return this;
1185 }
1186
1187 var credentials = ServiceType switch
1188 {
1189 CredentialServiceType.Bitwarden => BitwardenHandler.Handle(this),
1190 CredentialServiceType.LastPass => LastPassHandler.Handle(this),
1191 CredentialServiceType.OnePassword => OnePasswordHandler.Handle(this),
1192 CredentialServiceType.Dashlane => DashlaneHandler.Handle(this),
1193 CredentialServiceType.Keeper => KeeperHandler.Handle(this),
1194 CredentialServiceType.None => DirectHandler.Handle(this),
1195
1196 _ => throw new NotSupportedException(
1197 $"SaveTo is not supported for CredentialServiceType.{ServiceType}.")
1198 };
1199
1200 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{credentials.Count()}] credentials from [{ServiceType}] to grid.", null, GPALObjectType.None);
1201
1202 foreach (var credential in credentials)
1203 {
1204 grid.AddRow(new List<string> { credential.Username, credential.Password });
1205 }
1206
1207 return this;
1208 }
1209
1215 {
1216 return this;
1217 }
1218
1223 public void FetchAccessToken(out string accessToken)
1224 {
1225 switch (ServiceType)
1226 {
1227 case CredentialServiceType.Google:
1228 if (!string.IsNullOrEmpty(ServiceAccountKeyJson))
1229 {
1230 AccessToken = GoogleHandler.FetchServiceAccountToken(this);
1231 }
1232 else
1233 {
1234 AccessToken = GoogleHandler.FetchAccessToken(this); // Fallback to OAuth flow
1235 }
1236 break;
1237 case CredentialServiceType.AWS:
1238 AccessToken = AWSHandler.FetchAccessToken(this);
1239 break;
1240 case CredentialServiceType.Azure:
1241 AccessToken = AzureHandler.FetchAccessToken(this);
1242 break;
1243 case CredentialServiceType.Bitwarden:
1244 AccessToken = BitwardenHandler.FetchAccessToken(this);
1245 break;
1246 case CredentialServiceType.Dashlane:
1247 AccessToken = DashlaneHandler.FetchAccessToken(this);
1248 break;
1249 case CredentialServiceType.Keeper:
1250 AccessToken = KeeperHandler.FetchAccessToken(this);
1251 break;
1252 case CredentialServiceType.LastPass:
1253 AccessToken = LastPassHandler.FetchAccessToken(this);
1254 break;
1255 case CredentialServiceType.OnePassword:
1256 AccessToken = OnePasswordHandler.FetchAccessToken(this);
1257 break;
1258 case CredentialServiceType.StaticKey:
1259 // The key supplied via .WithServiceKey/.WithKeyFromEnv/.WithKeyFromApi IS the credential,
1260 // used directly as the Bearer/x-api-key header value - nothing to fetch.
1262 break;
1263 default:
1264 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No access token fetcher for service.", null, GPALObjectType.None);
1265 break;
1266 }
1267 accessToken = AccessToken;
1268 }
1269
1273 private bool IsTokenExpired()
1274 {
1275 if (string.IsNullOrEmpty(AccessToken) || IssueTime == DateTime.MinValue) return true;
1276 return DateTime.UtcNow > ExpiresAt;
1277 }
1278 }
1279
1283 internal class CredentialResult
1284 {
1288 public string Username { get; }
1292 public string Password { get; }
1293
1294 public CredentialResult(string username, string password)
1295 {
1296 Username = username;
1297 Password = password;
1298 }
1299 }
1300
1304 internal static class DirectHandler
1305 {
1311 internal static List<CredentialResult> Handle(Credentials creds)
1312 {
1313 var results = new List<CredentialResult>();
1314
1315 if (string.IsNullOrEmpty(creds.Username) && string.IsNullOrEmpty(creds.Password))
1316 {
1317 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Username and Password are both empty.", null, GPALObjectType.None);
1318 return results;
1319 }
1320
1321 results.Add(new CredentialResult(creds.Username ?? "", creds.Password ?? ""));
1322 return results;
1323 }
1324 }
1325
1329 internal static class AWSHandler
1330 {
1336 internal static string FetchAccessToken(Credentials creds)
1337 {
1338 creds.AccessToken = "";
1339 if (string.IsNullOrEmpty(creds.ClientId) || string.IsNullOrEmpty(creds.ClientSecret))
1340 {
1341 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Client ID or Client Secret is missing.", null, GPALObjectType.None);
1342 return creds.AccessToken;
1343 }
1344
1345 string scopeUrl = Credentials.GetScopeValue(creds.OAuthScope);
1346
1347 if (!string.IsNullOrEmpty(creds.RefreshToken))
1348 {
1349 // Refresh token flow
1350 var parameters = $"grant_type=refresh_token&" +
1351 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1352 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1353 $"refresh_token={Uri.EscapeDataString(creds.RefreshToken)}";
1354
1355 var client = GPAL.RESTClient
1356 .WithAPIBase(creds.Config.AWSAuthBase) // Use config property
1357 .WithEndpoint(creds.Config.AWSAuthEndpoint)
1358 .WithParameters(parameters);
1359 var response = client.Execute();
1360
1361 try
1362 {
1363 var authData = System.Text.Json.JsonSerializer.Deserialize<AWSTokenResponse>(response);
1364
1365 // Note: Cognito may not return a new refresh token
1366 if (authData != null)
1367 {
1368 creds.AccessToken = authData.AccessToken ?? "";
1369
1370 if (string.IsNullOrEmpty(creds.AccessToken))
1371 {
1372 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh AWS access token. Response: [{response}]", null, GPALObjectType.None);
1373 }
1374 else if (!string.IsNullOrEmpty(authData?.RefreshToken))
1375 {
1376 creds.WithRefreshToken(authData.RefreshToken);
1377 }
1378
1379 if (!string.IsNullOrEmpty(creds.AccessToken))
1380 {
1381 creds.IssueTime = DateTime.UtcNow;
1382 creds.ExpiresIn = authData.ExpiresIn;
1383 }
1384 }
1385 }
1386 catch (Exception ex)
1387 {
1388 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1389 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1390 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1391 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh AWS access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1392 }
1393 }
1394 else
1395 {
1396 creds.AuthCode = AuthHandler.GetAuthCode(creds);
1397
1398 if (string.IsNullOrEmpty(creds.AuthCode))
1399 {
1400 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No authorization code provided.", null, GPALObjectType.None);
1401 }
1402 else
1403 {
1404 // Authorization code flow
1405 var parameters = $"grant_type=authorization_code&" +
1406 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1407 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1408 $"code={Uri.EscapeDataString(creds.AuthCode)}&" +
1409 $"redirect_uri={Uri.EscapeDataString(creds.Config.AWSRedirectUri)}"; // Use AWS-specific redirect URI
1410
1411 var client = GPAL.RESTClient
1412 .WithAPIBase(creds.Config.AWSAuthBase) // Use config property
1413 .WithEndpoint(creds.Config.AWSAuthEndpoint)
1414 .WithParameters(parameters);
1415 var response = client.Execute();
1416
1417 try
1418 {
1419 var authData = System.Text.Json.JsonSerializer.Deserialize<AWSTokenResponse>(response);
1420
1421 if (authData != null)
1422 {
1423 creds.AccessToken = authData.AccessToken ?? "";
1424 if (string.IsNullOrEmpty(creds.AccessToken))
1425 {
1426 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch AWS access token. Response: [{response}]", null, GPALObjectType.None);
1427 }
1428 else if (!string.IsNullOrEmpty(authData?.RefreshToken))
1429 {
1430 creds.WithRefreshToken(authData.RefreshToken);
1431 }
1432
1433 if (!string.IsNullOrEmpty(creds.AccessToken))
1434 {
1435 creds.IssueTime = DateTime.UtcNow;
1436 creds.ExpiresIn = authData.ExpiresIn;
1437 }
1438 }
1439 }
1440 catch (Exception ex)
1441 {
1442 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1443 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1444 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1445 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch AWS access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1446 }
1447 }
1448 }
1449
1450 return creds.AccessToken;
1451 }
1452 }
1456 internal static class AzureHandler
1457 {
1463 internal static string FetchAccessToken(Credentials creds)
1464 {
1465 creds.AccessToken = "";
1466 if (string.IsNullOrEmpty(creds.ClientId) || string.IsNullOrEmpty(creds.ClientSecret))
1467 {
1468 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Client ID or Client Secret is missing.", null, GPALObjectType.None);
1469 return creds.AccessToken;
1470 }
1471
1472 string scopeUrl = Credentials.GetScopeValue(creds.OAuthScope);
1473
1474 if (!string.IsNullOrEmpty(creds.RefreshToken))
1475 {
1476 // Refresh token flow
1477 var parameters = $"grant_type=refresh_token&" +
1478 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1479 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1480 $"refresh_token={Uri.EscapeDataString(creds.RefreshToken)}&" +
1481 $"scope={Uri.EscapeDataString(scopeUrl)}";
1482
1483 var client = GPAL.RESTClient
1484 .WithAPIBase(creds.Config.AzureAuthBase) // Use config property
1485 .WithEndpoint(creds.Config.AzureAuthEndpoint)
1486 .WithParameters(parameters);
1487 var response = client.Execute();
1488
1489 try
1490 {
1491 creds.AzureTokenResponse = System.Text.Json.JsonSerializer.Deserialize<AzureTokenResponse>(response);
1492
1493 if (creds.AzureTokenResponse != null)
1494 {
1495 creds.AccessToken = creds.AzureTokenResponse.AccessToken ?? "";
1496
1497 if (!string.IsNullOrEmpty(creds.AccessToken))
1498 {
1499 creds.ExpiresIn = creds.AzureTokenResponse.ExpiresIn;
1500 creds.IssueTime = DateTime.UtcNow;
1501 }
1502
1503 if (!string.IsNullOrEmpty(creds.AzureTokenResponse.RefreshToken))
1504 {
1505 creds.WithRefreshToken(creds.AzureTokenResponse.RefreshToken);
1506 }
1507 }
1508 else if (string.IsNullOrEmpty(creds.AccessToken))
1509 {
1510 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh Azure access token. Response: [{response}]", null, GPALObjectType.None);
1511 }
1512 }
1513 catch (Exception ex)
1514 {
1515 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1516 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1517 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1518 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh Azure access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1519 }
1520 }
1521 else
1522 {
1523 creds.AuthCode = AuthHandler.GetAuthCode(creds);
1524
1525 if (string.IsNullOrEmpty(creds.AuthCode))
1526 {
1527 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No authorization code provided.", null, GPALObjectType.None);
1528 }
1529 else
1530 {
1531 // Authorization code flow
1532 var parameters = $"grant_type=authorization_code&" +
1533 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1534 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1535 $"code={Uri.EscapeDataString(creds.AuthCode)}&" +
1536 $"redirect_uri={Uri.EscapeDataString(creds.Config.AzureRedirectUri)}&" + // Use Azure-specific redirect URI
1537 $"scope={Uri.EscapeDataString(scopeUrl)}";
1538
1539 var client = GPAL.RESTClient
1540 .WithAPIBase(creds.Config.AzureAuthBase) // Use config property
1541 .WithEndpoint(creds.Config.AzureAuthEndpoint)
1542 .WithParameters(parameters);
1543 var response = client.Execute();
1544
1545 try
1546 {
1547 creds.AzureTokenResponse = System.Text.Json.JsonSerializer.Deserialize<AzureTokenResponse>(response);
1548
1549 if (creds.AzureTokenResponse != null)
1550 {
1551 creds.AccessToken = creds.AzureTokenResponse.AccessToken ?? "";
1552
1553 if (string.IsNullOrEmpty(creds.AccessToken))
1554 {
1555 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch Azure access token. Response: [{response}]", null, GPALObjectType.None);
1556 }
1557 else if (!string.IsNullOrEmpty(creds.AzureTokenResponse?.RefreshToken))
1558 {
1559 creds.WithRefreshToken(creds.AzureTokenResponse.RefreshToken);
1560 }
1561
1562 if (!string.IsNullOrEmpty(creds.AccessToken))
1563 {
1564 creds.ExpiresIn = creds.AzureTokenResponse.ExpiresIn;
1565 creds.IssueTime = DateTime.UtcNow;
1566 }
1567 }
1568 }
1569 catch (Exception ex)
1570 {
1571 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1572 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1573 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1574 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch Azure access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1575 }
1576 }
1577 }
1578
1579 return creds.AccessToken;
1580 }
1581 }
1582
1586 internal static class GoogleHandler
1587 {
1593 internal static string FetchAccessToken(Credentials creds)
1594 {
1595 creds.AccessToken = "";
1596
1597 if (!string.IsNullOrEmpty(creds.RefreshToken))
1598 {
1599 string clientId, clientSecret;
1600
1601 if (!string.IsNullOrEmpty(creds.ServiceKey))
1602 {
1603 var parts = Credentials.SafeSplit(creds.ServiceKey);
1604 clientId = parts.Item1;
1605 clientSecret = parts.Item2;
1606 }
1607 else
1608 {
1609 clientId = creds.ClientId;
1610 clientSecret = creds.ClientSecret;
1611 }
1612
1613 var parameters = $"grant_type=refresh_token&" +
1614 $"client_id={Uri.EscapeDataString(clientId)}&" +
1615 $"client_secret={Uri.EscapeDataString(clientSecret)}&" +
1616 $"refresh_token={Uri.EscapeDataString(creds.RefreshToken)}";
1617
1618 var response = GPAL.RESTClient
1619 .WithAPIBase(creds.Config.GoogleAuthBase)
1620 .WithEndpoint(creds.Config.GoogleAuthEndpoint)
1621 .WithParameters(parameters)
1622 .WithEncoding(ContentEncoding.UrlEncoded)
1623 .Execute();
1624
1625 creds.GoogleTokenResponse = System.Text.Json.JsonSerializer.Deserialize<GoogleTokenResponse>(response);
1626 creds.AccessToken = creds.GoogleTokenResponse?.AccessToken ?? "";
1627
1628 if (!string.IsNullOrEmpty(creds.AccessToken))
1629 {
1630 creds.IssueTime = DateTime.UtcNow;
1631 creds.ExpiresIn = creds.GoogleTokenResponse.ExpiresIn;
1632 }
1633 else
1634 {
1635 GPAL.PublishSimpleEvent(
1636 GPALEventType.ERROR,
1637 $"Failed to refresh Google access token. Response: [{response}]",
1638 null,
1639 GPALObjectType.None);
1640 }
1641 }
1642 else
1643 {
1644 creds.AuthCode = AuthHandler.GetAuthCode(creds);
1645
1646 if (string.IsNullOrEmpty(creds.AuthCode))
1647 {
1648 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No authorization code provided.", null, GPALObjectType.None);
1649 }
1650 else
1651 {
1652 string redirectUri = creds.Config.UseRestRedirect
1653 ? creds.Config.GoogleRestRedirectUri
1654 : creds.Config.GoogleRedirectUri;
1655
1656 var parameters = $"{creds.Config.GrantTypeKey}=authorization_code&" +
1657 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1658 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1659 $"{creds.Config.GoogleAuthCodeKey}={Uri.EscapeDataString(creds.AuthCode)}&" +
1660 $"redirect_uri={Uri.EscapeDataString(redirectUri)}";
1661
1662 var response = GPAL.RESTClient
1663 .WithAPIBase(creds.Config.GoogleAuthBase)
1664 .WithEndpoint(creds.Config.GoogleAuthEndpoint)
1665 .WithParameters(parameters)
1666 .WithEncoding(ContentEncoding.UrlEncoded)
1667 .Execute();
1668
1669 creds.GoogleTokenResponse = System.Text.Json.JsonSerializer.Deserialize<GoogleTokenResponse>(response);
1670 creds.AccessToken = creds.GoogleTokenResponse?.AccessToken ?? "";
1671
1672 if (!string.IsNullOrEmpty(creds.AccessToken))
1673 {
1674 creds.IssueTime = DateTime.UtcNow;
1675 creds.ExpiresIn = creds.GoogleTokenResponse.ExpiresIn;
1676 }
1677 else
1678 {
1679 GPAL.PublishSimpleEvent(
1680 GPALEventType.ERROR,
1681 $"Failed to fetch Google access token. Response: [{response}]",
1682 null,
1683 GPALObjectType.None);
1684 }
1685
1686 if (!string.IsNullOrEmpty(creds.GoogleTokenResponse?.RefreshToken))
1687 {
1688 creds.WithRefreshToken(creds.GoogleTokenResponse.RefreshToken);
1689 }
1690 }
1691 }
1692
1693 return creds.AccessToken;
1694 }
1695
1701 public static string Base64UrlEncode(byte[] input)
1702 {
1703 return Convert.ToBase64String(input)
1704 .TrimEnd('=') // remove padding
1705 .Replace('+', '-') // URL safe
1706 .Replace('/', '_'); // URL safe
1707 }
1708
1714 private static byte[] Base64UrlDecode(string input)
1715 {
1716 string padded = input
1717 .Replace('-', '+')
1718 .Replace('_', '/');
1719
1720 switch (padded.Length % 4)
1721 {
1722 case 2: padded += "=="; break;
1723 case 3: padded += "="; break;
1724 }
1725
1726 return Convert.FromBase64String(padded);
1727 }
1728
1734 internal static string FetchServiceAccountToken(Credentials creds)
1735 {
1736 creds.AccessToken = "";
1737
1738 if (string.IsNullOrEmpty(creds.ServiceAccountKeyJson))
1739 {
1740 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Service account key JSON is missing.", null, GPALObjectType.None);
1741 return creds.AccessToken;
1742 }
1743
1744 try
1745 {
1746 var serviceAccount = System.Text.Json.JsonSerializer.Deserialize<GoogleServiceAccountKey>(creds.ServiceAccountKeyJson);
1747
1748 var iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
1749 var exp = iat + 3600;
1750
1751 var pemReader = new PemReader(new StringReader(serviceAccount.PrivateKey));
1752 var keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
1753 var privateKey = (Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters)keyPair.Private;
1754
1755 var header = new { alg = "RS256", typ = "JWT" };
1756
1757 var payload = new
1758 {
1759 iss = serviceAccount.ClientEmail,
1760 scope = Credentials.GetScopeValue(creds.OAuthScope),
1761 iat,
1762 exp
1763 };
1764
1765 var headerJson = JsonSerializer.Serialize(header);
1766 var payloadJson = JsonSerializer.Serialize(payload);
1767
1768 var headerBytes = Encoding.UTF8.GetBytes(headerJson);
1769 var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
1770
1771 string headerBase64Url = Base64UrlEncode(headerBytes);
1772 string payloadBase64Url = Base64UrlEncode(payloadBytes);
1773
1774 string unsignedJwt = $"{headerBase64Url}.{payloadBase64Url}";
1775
1776 var signer = SignerUtilities.GetSigner("SHA256withRSA");
1777 signer.Init(true, privateKey);
1778 var bytes = Encoding.UTF8.GetBytes(unsignedJwt);
1779 signer.BlockUpdate(bytes, 0, bytes.Length);
1780
1781 var signatureBytes = signer.GenerateSignature();
1782 var signature = Base64UrlEncode(signatureBytes);
1783
1784 var jwt = $"{unsignedJwt}.{signature}";
1785
1786 var parameters = $"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={Uri.EscapeDataString(jwt)}";
1787
1788 var response = GPAL.RESTClient
1789 .WithAPIBase(creds.Config.GoogleAuthBase)
1790 .WithEndpoint(creds.Config.GoogleServiceAccountTokenUri)
1791 .WithParameters(parameters)
1792 .WithEncoding(ContentEncoding.UrlEncoded)
1793 .WithHttpMethod("POST")
1794 .Execute();
1795
1796 var authData = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1797
1798 creds.AccessToken = authData?[creds.Config.AccessTokenKey] ?? "";
1799
1800 if (!string.IsNullOrEmpty(creds.AccessToken))
1801 {
1802 creds.IssueTime = DateTime.UtcNow;
1803
1804 if (authData != null &&
1805 authData.TryGetValue("expires_in", out var expStr) &&
1806 int.TryParse(expStr, out int expires))
1807 {
1808 creds.ExpiresIn = expires;
1809 }
1810 else
1811 {
1812 creds.ExpiresIn = 0;
1813 }
1814 }
1815 else
1816 {
1817 GPAL.PublishSimpleEvent(
1818 GPALEventType.ERROR,
1819 $"Failed to fetch service account token. Response: [{response}]",
1820 null,
1821 GPALObjectType.None);
1822 }
1823 }
1824 catch (Exception ex)
1825 {
1826 GPAL.PublishSimpleEvent(
1827 GPALEventType.ERROR,
1828 $"Failed to fetch service account token",
1829 null,
1830 GPALObjectType.None, ex);
1831 }
1832
1833 return creds.AccessToken;
1834 }
1835 }
1836
1840 internal static class BitwardenHandler
1841 {
1847 internal static List<CredentialResult> Handle(Credentials creds)
1848 {
1849 var results = new List<CredentialResult>();
1850
1851 creds.AccessToken = FetchAccessToken(creds);
1852
1853 if (string.IsNullOrEmpty(creds.AccessToken))
1854 return results;
1855
1856 var vaultJson = GPAL.RESTClient
1857 .WithAPIBase(creds.Config.BitwardenVaultBase)
1858 .WithEndpoint(creds.Config.BitwardenVaultEndpoint)
1859 .WithParameters(new Dictionary<string, string>
1860 {
1861 { creds.Config.SearchKey, creds.Target }
1862 })
1863 .WithHeader(creds.Config.AuthorizationHeader, $"Bearer {creds.AccessToken}")
1864 .Execute();
1865
1866 var vaultData = JsonSerializer.Deserialize<BitwardenVaultResponse>(vaultJson);
1867
1868 if (vaultData?.Data != null)
1869 {
1870 foreach (var item in vaultData.Data)
1871 {
1872 if (item.Login != null &&
1873 Credentials.MatchesDomain(item.Login.Uris?.Select(u => u.Uri), creds.Domain))
1874 results.Add(new CredentialResult(item.Login.Username, item.Login.Password));
1875 }
1876 }
1877
1878 return results;
1879 }
1880
1886 internal static string FetchAccessToken(Credentials creds)
1887 {
1888 var (clientId, clientSecret) = Credentials.SafeSplit(creds.ServiceKey);
1889
1890 var response = GPAL.RESTClient
1891 .WithAPIBase(creds.Config.BitwardenAuthBase)
1892 .WithEndpoint(creds.Config.BitwardenAuthEndpoint)
1893 .WithParameters(new
1894 {
1895 grant_type = "password",
1896 username = creds.Username,
1897 password = creds.Password,
1898 client_id = clientId,
1899 client_secret = clientSecret
1900 })
1901 .Execute();
1902
1903 creds.BitwardenTokenResponse = JsonSerializer.Deserialize<BitwardenTokenResponse>(response);
1904 creds.AccessToken = creds.BitwardenTokenResponse?.AccessToken ?? "";
1905
1906 if (!string.IsNullOrEmpty(creds.AccessToken))
1907 {
1908 creds.IssueTime = DateTime.UtcNow;
1909 creds.ExpiresIn = creds.BitwardenTokenResponse.ExpiresIn;
1910 }
1911 else
1912 {
1913 GPAL.PublishSimpleEvent(
1914 GPALEventType.ERROR,
1915 $"Bitwarden auth failed. Response: [{response}]",
1916 null,
1917 GPALObjectType.None);
1918 }
1919
1920 return creds.AccessToken;
1921 }
1922 }
1923
1927 internal static class LastPassHandler
1928 {
1934 internal static List<CredentialResult> Handle(Credentials creds)
1935 {
1936 var results = new List<CredentialResult>();
1937
1938 // Step 1: Ensure we have an active LastPass session
1939 if (!EnsureLoggedIn(creds))
1940 return results;
1941
1942 try
1943 {
1944 // Step 2: Get all items as JSON (most reliable way to search)
1945 var process = new Process
1946 {
1947 StartInfo = new ProcessStartInfo
1948 {
1949 FileName = "lpass",
1950 Arguments = "ls --json",
1951 RedirectStandardOutput = true,
1952 UseShellExecute = false,
1953 CreateNoWindow = true
1954 }
1955 };
1956
1957 process.Start();
1958 string jsonOutput = process.StandardOutput.ReadToEnd();
1959 process.WaitForExit();
1960
1961 if (process.ExitCode != 0)
1962 {
1963 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"LastPass CLI returned exit code [{process.ExitCode}].", null, GPALObjectType.None);
1964 return results;
1965 }
1966
1967 var items = JsonSerializer.Deserialize<List<LastPassItem>>(jsonOutput);
1968
1969 if (items != null)
1970 {
1971 string searchTerm = (creds.Target ?? "").ToLowerInvariant();
1972
1973 foreach (var item in items)
1974 {
1975 if (string.IsNullOrEmpty(item.Username) && string.IsNullOrEmpty(item.Password))
1976 continue;
1977
1978 bool matches = string.IsNullOrEmpty(searchTerm) ||
1979 (item.Name?.ToLowerInvariant().Contains(searchTerm) == true) ||
1980 (item.Url?.ToLowerInvariant().Contains(searchTerm) == true) ||
1981 (item.Username?.ToLowerInvariant().Contains(searchTerm) == true);
1982
1983 if (matches && Credentials.MatchesDomain(item.Url, creds.Domain))
1984 {
1985 results.Add(new CredentialResult(item.Username ?? "", item.Password ?? ""));
1986 }
1987 }
1988 }
1989 }
1990 catch (Exception ex)
1991 {
1992 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"LastPass vault retrieval failed", null, GPALObjectType.None, ex);
1993 }
1994
1995 return results;
1996 }
1997
2003 internal static string FetchAccessToken(Credentials creds)
2004 {
2005 // LastPass CLI doesn't use a traditional "access token" like the others.
2006 // We just ensure the session is valid (this is called by Handle).
2007 EnsureLoggedIn(creds);
2008 return "LastPass-CLI-Session"; // placeholder - not really used
2009 }
2010
2016 private static bool EnsureLoggedIn(Credentials creds)
2017 {
2018 try
2019 {
2020 var statusProcess = new Process
2021 {
2022 StartInfo = new ProcessStartInfo
2023 {
2024 FileName = "lpass",
2025 Arguments = "status",
2026 RedirectStandardOutput = true,
2027 UseShellExecute = false,
2028 CreateNoWindow = true
2029 }
2030 };
2031
2032 statusProcess.Start();
2033 string statusOutput = statusProcess.StandardOutput.ReadToEnd().Trim();
2034 statusProcess.WaitForExit();
2035
2036 if (statusOutput.ToLower().Contains("logged in"))
2037 return true;
2038
2039 // Not logged in > give clear instruction
2040 GPAL.PublishSimpleEvent(
2041 GPALEventType.ERROR,
2042 $"LastPass CLI is not logged in. Please run this command once on the machine:\n" +
2043 $"lpass login [{creds.Username}]\n" +
2044 $"Then re-run your automation.",
2045 null, GPALObjectType.None);
2046
2047 return false;
2048 }
2049 catch (Exception ex)
2050 {
2051 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2052 $"LastPass CLI check failed. Is the LastPass CLI (lpass) installed?",
2053 null, GPALObjectType.None, ex);
2054 return false;
2055 }
2056 }
2057 }
2058
2062 internal static class OnePasswordHandler
2063 {
2069 internal static List<CredentialResult> Handle(Credentials creds)
2070 {
2071 var results = new List<CredentialResult>();
2072
2073 // Ensure we have a valid session
2074 if (!EnsureSession(creds))
2075 return results;
2076
2077 try
2078 {
2079 string search = creds.Target?.Trim() ?? "";
2080
2081 // Build command: list items and get full details in one go where possible
2082 string arguments = string.IsNullOrEmpty(search)
2083 ? "item list --format=json"
2084 : $"item list --search=\"{search}\" --format=json";
2085
2086 var process = new Process
2087 {
2088 StartInfo = new ProcessStartInfo
2089 {
2090 FileName = "op",
2091 Arguments = arguments,
2092 RedirectStandardOutput = true,
2093 UseShellExecute = false,
2094 CreateNoWindow = true
2095 }
2096 };
2097
2098 process.Start();
2099 string jsonOutput = process.StandardOutput.ReadToEnd();
2100 process.WaitForExit();
2101
2102 if (process.ExitCode != 0)
2103 {
2104 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2105 $"1Password CLI returned exit code [{process.ExitCode}]. Make sure you are signed in.",
2106 null, GPALObjectType.None);
2107 return results;
2108 }
2109
2110 var items = JsonSerializer.Deserialize<List<OnePasswordItem>>(jsonOutput);
2111
2112 if (items != null)
2113 {
2114 foreach (var item in items)
2115 {
2116 // Try to extract username and password fields
2117 string username = "";
2118 string password = "";
2119
2120 foreach (var field in item.Fields)
2121 {
2122 if (string.Equals(field.Label, "username", StringComparison.OrdinalIgnoreCase) ||
2123 string.Equals(field.Type, "username", StringComparison.OrdinalIgnoreCase))
2124 {
2125 username = field.Value ?? "";
2126 }
2127 else if (string.Equals(field.Label, "password", StringComparison.OrdinalIgnoreCase) ||
2128 string.Equals(field.Type, "password", StringComparison.OrdinalIgnoreCase))
2129 {
2130 password = field.Value ?? field.Reference ?? "";
2131 }
2132 }
2133
2134 if ((!string.IsNullOrEmpty(username) || !string.IsNullOrEmpty(password)) &&
2135 Credentials.MatchesDomain(item.Urls?.Select(u => u.Href), creds.Domain))
2136 {
2137 results.Add(new CredentialResult(username, password));
2138 }
2139 }
2140 }
2141 }
2142 catch (Exception ex)
2143 {
2144 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2145 $"1Password vault retrieval failed", null, GPALObjectType.None, ex);
2146 }
2147
2148 return results;
2149 }
2150
2156 internal static string FetchAccessToken(Credentials creds)
2157 {
2158 // 1Password uses a session token instead of a traditional access token
2159 EnsureSession(creds);
2160 return "1Password-CLI-Session";
2161 }
2162
2168 private static bool EnsureSession(Credentials creds)
2169 {
2170 try
2171 {
2172 // Simple check if CLI is ready
2173 var process = new Process
2174 {
2175 StartInfo = new ProcessStartInfo
2176 {
2177 FileName = "op",
2178 Arguments = "whoami --format=json",
2179 RedirectStandardOutput = true,
2180 UseShellExecute = false,
2181 CreateNoWindow = true
2182 }
2183 };
2184
2185 process.Start();
2186 string output = process.StandardOutput.ReadToEnd();
2187 process.WaitForExit();
2188
2189 if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
2190 return true;
2191
2192 // Not signed in > give clear guidance
2193 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2194 $"1Password CLI is not signed in. Please run this once on the target machine:\n" +
2195 $"op signin\n" +
2196 $"Then re-run your automation. (Session lasts ~30 minutes of activity)",
2197 null, GPALObjectType.None);
2198
2199 return false;
2200 }
2201 catch (Exception ex)
2202 {
2203 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2204 $"1Password CLI check failed. Is the 1Password CLI (`op`) installed and in PATH?",
2205 null, GPALObjectType.None, ex);
2206 return false;
2207 }
2208 }
2209 }
2210
2214 internal static class DashlaneHandler
2215 {
2221 internal static List<CredentialResult> Handle(Credentials creds)
2222 {
2223 var results = new List<CredentialResult>();
2224
2225 // Ensure CLI session / login
2226 if (!EnsureLoggedIn(creds))
2227 return results;
2228
2229 try
2230 {
2231 string search = creds.Target?.Trim() ?? "";
2232
2233 // Use JSON output for reliable parsing
2234 string arguments = string.IsNullOrEmpty(search)
2235 ? "password list --json"
2236 : $"password list --search \"{search}\" --json";
2237
2238 var process = new Process
2239 {
2240 StartInfo = new ProcessStartInfo
2241 {
2242 FileName = "dcli", // or "dashlane" depending on installation
2243 Arguments = arguments,
2244 RedirectStandardOutput = true,
2245 RedirectStandardError = true,
2246 UseShellExecute = false,
2247 CreateNoWindow = true
2248 }
2249 };
2250
2251 process.Start();
2252 string jsonOutput = process.StandardOutput.ReadToEnd();
2253 string errorOutput = process.StandardError.ReadToEnd();
2254 process.WaitForExit();
2255
2256 if (process.ExitCode != 0)
2257 {
2258 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2259 $"Dashlane CLI failed (exit code [{process.ExitCode}]). Error: [{errorOutput.Trim()}]",
2260 null, GPALObjectType.None);
2261 return results;
2262 }
2263
2264 var items = JsonSerializer.Deserialize<List<DashlaneItem>>(jsonOutput);
2265
2266 if (items != null)
2267 {
2268 foreach (var item in items)
2269 {
2270 if (!string.IsNullOrEmpty(item.Username) || !string.IsNullOrEmpty(item.Password))
2271 {
2272 if (Credentials.MatchesDomain(item.Url, creds.Domain))
2273 results.Add(new CredentialResult(item.Username ?? "", item.Password ?? ""));
2274 }
2275 }
2276 }
2277 }
2278 catch (Exception ex)
2279 {
2280 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2281 $"Dashlane vault retrieval failed", null, GPALObjectType.None, ex);
2282 }
2283
2284 return results;
2285 }
2286
2292 internal static string FetchAccessToken(Credentials creds)
2293 {
2294 // Dashlane CLI uses a local session, not a traditional token
2295 EnsureLoggedIn(creds);
2296 return "Dashlane-CLI-Session";
2297 }
2298
2304 private static bool EnsureLoggedIn(Credentials creds)
2305 {
2306 try
2307 {
2308 var process = new Process
2309 {
2310 StartInfo = new ProcessStartInfo
2311 {
2312 FileName = "dcli",
2313 Arguments = "status --json",
2314 RedirectStandardOutput = true,
2315 UseShellExecute = false,
2316 CreateNoWindow = true
2317 }
2318 };
2319
2320 process.Start();
2321 string output = process.StandardOutput.ReadToEnd();
2322 process.WaitForExit();
2323
2324 if (process.ExitCode == 0 && output.ToLower().Contains("\"logged_in\":true"))
2325 return true;
2326
2327 // Not logged in > clear instruction
2328 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2329 $"Dashlane CLI is not logged in. Please run this once on the target machine:\n" +
2330 $"dcli login\n" +
2331 $"Then re-run your automation.",
2332 null, GPALObjectType.None);
2333
2334 return false;
2335 }
2336 catch (Exception ex)
2337 {
2338 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2339 $"Dashlane CLI check failed. Is the Dashlane CLI (`dcli`) installed and in PATH?",
2340 null, GPALObjectType.None, ex);
2341 return false;
2342 }
2343 }
2344 }
2345
2349 internal static class KeeperHandler
2350 {
2356 internal static List<CredentialResult> Handle(Credentials creds)
2357 {
2358 var results = new List<CredentialResult>();
2359
2360 if (!EnsureLoggedIn(creds))
2361 return results;
2362
2363 try
2364 {
2365 string search = creds.Target?.Trim() ?? "";
2366
2367 // Use "list" or "search" with JSON output
2368 string arguments = string.IsNullOrEmpty(search)
2369 ? "list --format=json"
2370 : $"search \"{search}\" --format=json";
2371
2372 var process = new Process
2373 {
2374 StartInfo = new ProcessStartInfo
2375 {
2376 FileName = "keeper",
2377 Arguments = arguments,
2378 RedirectStandardOutput = true,
2379 RedirectStandardError = true,
2380 UseShellExecute = false,
2381 CreateNoWindow = true
2382 }
2383 };
2384
2385 process.Start();
2386 string jsonOutput = process.StandardOutput.ReadToEnd();
2387 string errorOutput = process.StandardError.ReadToEnd();
2388 process.WaitForExit();
2389
2390 if (process.ExitCode != 0)
2391 {
2392 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2393 $"Keeper Commander returned exit code [{process.ExitCode}]. Error: [{errorOutput.Trim()}]",
2394 null, GPALObjectType.None);
2395 return results;
2396 }
2397
2398 var records = JsonSerializer.Deserialize<List<KeeperRecord>>(jsonOutput);
2399
2400 if (records != null)
2401 {
2402 foreach (var record in records)
2403 {
2404 string username = record.Login?.Username ?? "";
2405 string password = record.Login?.Password ?? "";
2406
2407 if ((!string.IsNullOrEmpty(username) || !string.IsNullOrEmpty(password)) &&
2408 Credentials.MatchesDomain(record.Login?.Url, creds.Domain))
2409 {
2410 results.Add(new CredentialResult(username, password));
2411 }
2412 }
2413 }
2414 }
2415 catch (Exception ex)
2416 {
2417 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2418 $"Keeper vault retrieval failed", null, GPALObjectType.None, ex);
2419 }
2420
2421 return results;
2422 }
2423
2429 internal static string FetchAccessToken(Credentials creds)
2430 {
2431 // Keeper uses a persistent session via Commander
2432 EnsureLoggedIn(creds);
2433 return "Keeper-Commander-Session";
2434 }
2435
2441 private static bool EnsureLoggedIn(Credentials creds)
2442 {
2443 try
2444 {
2445 var process = new Process
2446 {
2447 StartInfo = new ProcessStartInfo
2448 {
2449 FileName = "keeper",
2450 Arguments = "whoami --format=json",
2451 RedirectStandardOutput = true,
2452 UseShellExecute = false,
2453 CreateNoWindow = true
2454 }
2455 };
2456
2457 process.Start();
2458 string output = process.StandardOutput.ReadToEnd();
2459 process.WaitForExit();
2460
2461 if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
2462 return true;
2463
2464 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2465 $"Keeper Commander is not logged in. Please run this once on the target machine:\n" +
2466 $"keeper shell\n" +
2467 $"Then login with your email / master password (or use 'keeper login').\n" +
2468 $"After login, you can use 'this-device' for persistent sessions.",
2469 null, GPALObjectType.None);
2470
2471 return false;
2472 }
2473 catch (Exception ex)
2474 {
2475 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2476 $"Keeper Commander check failed. Is Keeper Commander (`keeper`) installed and in PATH?",
2477 null, GPALObjectType.None, ex);
2478 return false;
2479 }
2480 }
2481 }
2488 internal static class LocalAuthCallbackServer
2489 {
2490 private static readonly object _lock = new object();
2491 private static HttpListener _listener;
2492 private static string _pendingAuthCode;
2493 private static string _pendingAuthScope;
2494
2500 internal static bool StartIfNeeded(string baseUrl)
2501 {
2502 lock (_lock)
2503 {
2504 if (null != _listener)
2505 return true;
2506
2507 try
2508 {
2509 var listener = new HttpListener();
2510 listener.Prefixes.Add(baseUrl);
2511 listener.Start();
2512 _listener = listener;
2513
2514 new Thread(() => Listen(listener)) { IsBackground = true }.Start();
2515 return true;
2516 }
2517 catch (HttpListenerException)
2518 {
2519 // something else already owns this prefix. whether that something is GPALRestAPI is a
2520 // separate question, and BrowserHelper.RestApiAnswering is how it gets asked
2521 return false;
2522 }
2523 }
2524 }
2525
2526 private static void Listen(HttpListener listener)
2527 {
2528 try
2529 {
2530 while (listener.IsListening)
2531 {
2532 HttpListenerContext context = listener.GetContext();
2533 HandleRequest(context);
2534 }
2535 }
2536 catch (Exception)
2537 {
2538 // listener was stopped
2539 }
2540 }
2541
2542 private static void HandleRequest(HttpListenerContext context)
2543 {
2544 HttpListenerResponse response = context.Response;
2545 string path = context.Request.Url.AbsolutePath.Trim('/');
2546 string json;
2547
2548 switch (path)
2549 {
2550 case "access-token": // Called by the provider's redirect after the user grants consent
2551 NameValueCollection query = ParseQueryString(context.Request.Url.Query);
2552
2553 lock (_lock)
2554 {
2555 _pendingAuthCode = query["code"];
2556 _pendingAuthScope = query["scope"];
2557 }
2558
2559 SendResponse(response, "{\"message\":\"Authorization code received. You can now call /get-access-token.\"}");
2560 break;
2561
2562 case "get-access-token": // Polled by GetAuthCodeFromRest
2563 string code, scope;
2564
2565 lock (_lock)
2566 {
2567 code = _pendingAuthCode ?? string.Empty;
2568 scope = _pendingAuthScope ?? string.Empty;
2569
2570 // single use
2571 _pendingAuthCode = null;
2572 _pendingAuthScope = null;
2573 }
2574
2575 json = System.Text.Json.JsonSerializer.Serialize(new { code, scope });
2576 SendResponse(response, json);
2577 break;
2578
2579 default:
2580 SendResponse(response, "{}");
2581 break;
2582 }
2583 }
2584
2585 private static NameValueCollection ParseQueryString(string query)
2586 {
2587 var result = new NameValueCollection();
2588
2589 if (string.IsNullOrEmpty(query))
2590 return result;
2591
2592 foreach (string pair in query.TrimStart('?').Split('&'))
2593 {
2594 if (string.IsNullOrEmpty(pair))
2595 continue;
2596
2597 string[] parts = pair.Split(new[] { '=' }, 2);
2598 string key = Uri.UnescapeDataString(parts[0]);
2599 string value = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty;
2600 result[key] = value;
2601 }
2602
2603 return result;
2604 }
2605
2606 private static void SendResponse(HttpListenerResponse response, string message)
2607 {
2608 try
2609 {
2610 response.StatusCode = (int)HttpStatusCode.OK;
2611 response.ContentType = "application/json";
2612 response.ContentEncoding = Encoding.UTF8;
2613
2614 byte[] buffer = Encoding.UTF8.GetBytes(message ?? "");
2615 response.OutputStream.Write(buffer, 0, buffer.Length);
2616 }
2617 catch { /* client gone - ignore */ }
2618 finally
2619 {
2620 try { response.Close(); } catch { }
2621 }
2622 }
2623
2627 internal static void Stop()
2628 {
2629 lock (_lock)
2630 {
2631 if (null == _listener)
2632 return;
2633
2634 try { _listener.Stop(); _listener.Close(); } catch { }
2635 _listener = null;
2636 _pendingAuthCode = null;
2637 _pendingAuthScope = null;
2638 }
2639 }
2640 }
2641
2645 internal static class AuthHandler
2646 {
2647 // where a GPALRestAPI serves by default, and the first place looked before binding our own
2648 internal const string DefaultTokenUrl = "http://localhost:3000/";
2656 internal static string GetAuthCodeFromRest(Process browserProcess, int timeoutInSeconds)
2657 {
2658 string authCode = "";
2659
2660 try
2661 {
2662 // NOTE: get the access token from the rest API which is started by OttoMagic
2663 // GPALRestApi gets it because we pass a local url callback creds.Config.GoogleRestRedirectUri
2664 // and then we can access the token
2665 var restClient = GPAL.RESTClient
2666 .WithAPIBase(GPAL.OAuthTokenUrl)
2667 .WithEndpoint("get-access-token");
2668
2669 // Polling parameters
2670 TimeSpan timeout = TimeSpan.FromSeconds(timeoutInSeconds);
2671 TimeSpan interval = TimeSpan.FromSeconds(1);
2672 DateTime start = DateTime.UtcNow;
2673
2674 while ((DateTime.UtcNow - start) < timeout)
2675 {
2676 AuthCodePayload tokenInfo = restClient.Execute<AuthCodePayload>(); // returns AuthCodePayload directly
2677 if (!string.IsNullOrEmpty(tokenInfo?.Code))
2678 {
2679 authCode = tokenInfo.Code;
2680 GPAL.PublishSimpleEvent(
2681 GPALEventType.INFO,
2682 "Authorization code retrieved from REST server.",
2683 null,
2684 GPALObjectType.None
2685 );
2686 break; // stop polling
2687 }
2688
2689 Thread.Sleep(interval);
2690 }
2691
2692 if (string.IsNullOrEmpty(authCode))
2693 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Timeout waiting for authorization code from REST server.", null, GPALObjectType.None);
2694 }
2695 catch (Exception ex)
2696 {
2697 GPAL.PublishSimpleEvent(
2698 GPALEventType.ERROR,
2699 $"REST redirect auth failed",
2700 null,
2701 GPALObjectType.None
2702 , ex);
2703 }
2704 if (null != browserProcess)
2705 BrowserHelper.KillProcess(browserProcess); // only needed once to login
2706
2707 return authCode;
2708 }
2709
2731 internal static bool ResolveTokenUrl(Credentials creds)
2732 {
2733 bool retVal = false;
2734 string configured = creds?.Config?.GoogleRestRedirectUri;
2735
2736 GPAL.OAuthTokenUrl = true == string.IsNullOrWhiteSpace(configured)
2737 ? DefaultTokenUrl
2738 : $"{new Uri(configured).GetLeftPart(UriPartial.Authority)}/";
2739
2740 if (true == LocalAuthCallbackServer.StartIfNeeded(GPAL.OAuthTokenUrl))
2741 {
2742 retVal = true;
2743
2744 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Listening on [{GPAL.OAuthTokenUrl}] to catch the token.", creds, GPALObjectType.Other);
2745 }
2746 else if (true == Browser.BrowserHelper.RestApiAnswering(GPAL.OAuthTokenUrl))
2747 {
2748 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"The GPALRestAPI already on [{GPAL.OAuthTokenUrl}] will catch the token.", creds, GPALObjectType.Other);
2749 }
2750 else
2751 {
2752 // moving to another port would be a redirect the provider never agreed to, so there is no
2753 // recovery here worth making. one machine running two of these was always going to collide
2754 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Something that is not GPALRestAPI is on [{GPAL.OAuthTokenUrl}], and the redirect registered with the provider cannot point anywhere else", creds, GPALObjectType.Other);
2755 }
2756
2757 return retVal;
2758 }
2759
2760 internal static string GetAuthCode(Credentials creds)
2761 {
2762 string authCode = "";
2763 int retry = 2;
2764 Process browserProcess = null;
2765 bool ownsLocalServer = false;
2766
2767 bool publishToConsole = GPAL.GPALSettings.PublishToConsole;
2768
2769 GPALEventType consoleEvents = GPAL.GPALSettings.ConsoleEvents;
2770
2771 if (false == publishToConsole)
2772 GPAL.WithPublishToConsole();
2773 else if (false == consoleEvents.HasFlag(GPALEventType.INFO))
2774 GPAL.ConsoleEvents |= GPALEventType.INFO;
2775
2776 // REST redirect flow
2777 // workflow : launch browser > user logs in (unknown length of time) > redirect to localhost > listener parses code
2778 // meanwhile, GPAL is polling get-access-token until some value is returned or 1 minute passes
2779 if (creds.Config.UseRestRedirect)
2780 {
2781 // GPALRESTAPI normally serves the redirect target (e.g. http://localhost:3000/access-token) and
2782 // get-access-token. If it isn't running, start a minimal listener on the same base URL ourselves
2783 // so the OAuth redirect still has somewhere to land.
2784 ownsLocalServer = ResolveTokenUrl(creds);
2785
2786 string authUrl = AuthHandler.GetAuthUrl(creds);
2787
2788 browserProcess = creds.Browser.MagicHelper.LaunchBrowser(creds.Browser, authUrl); // launch a new instance of the browser for authentication
2789 authCode = GetAuthCodeFromRest(browserProcess, 60);
2790 }
2791
2792 // Manual fallback flow: either UseRestRedirect is false, or the REST redirect timed out
2793 if (string.IsNullOrEmpty(authCode))
2794 {
2795 string authUrl = AuthHandler.GetAuthUrl(creds);
2796
2797 if (true == creds.Config.UseRestRedirect)
2798 GPAL.PublishSimpleEvent(
2799 GPALEventType.WARNING,
2800 "No authorization code arrived via the local redirect listener. Falling back to manual entry.",
2801 null,
2802 GPALObjectType.None
2803 );
2804
2805 while (retry > 0)
2806 {
2807
2808 GPAL.PublishSimpleEvent(
2809 GPALEventType.INFO,
2810 $"Open this URL in a browser, authorize, and provide the code: \n[{authUrl}]",
2811 null,
2812 GPALObjectType.None
2813 );
2814
2815 Console.WriteLine("\nEnter auth code: "); // a prompt for the ReadLine below, not an event
2816 authCode = Console.ReadLine();
2817
2818 if (string.IsNullOrEmpty(authCode))
2819 {
2820 GPAL.PublishSimpleEvent(
2821 GPALEventType.ERROR,
2822 "No authorization code provided.",
2823 null,
2824 GPALObjectType.None
2825 );
2826 retry--;
2827 }
2828 else
2829 {
2830 break;
2831 }
2832 }
2833 }
2834
2835 if (ownsLocalServer)
2836 LocalAuthCallbackServer.Stop();
2837
2838 if (false == publishToConsole)
2839 GPAL.WithPublishToConsole(GPALEventType.NONE);
2840 else if (false == consoleEvents.HasFlag(GPALEventType.INFO))
2841 GPAL.ConsoleEvents ^= GPALEventType.INFO;
2842
2843 return authCode;
2844 }
2845
2851 internal static string GetAuthUrl(Credentials creds)
2852 {
2853 switch (creds.ServiceType)
2854 {
2855 case CredentialServiceType.Google:
2856 return $"{creds.Config.GoogleConsentBase}/o/oauth2/v2/auth" +
2857 $"?client_id={Uri.EscapeDataString(creds.ClientId)}" +
2858 $"&redirect_uri={Uri.EscapeDataString(creds.Config.UseRestRedirect ? creds.Config.GoogleRestRedirectUri : creds.Config.GoogleRedirectUri)}" +
2859 $"&response_type=code" +
2860 $"&scope={Uri.EscapeDataString(Credentials.GetScopeValue(creds.OAuthScope))}" +
2861 $"&access_type=offline";
2862
2863 case CredentialServiceType.Azure:
2864 return $"{creds.Config.AzureAuthBase}/oauth2/v2.0/authorize" +
2865 $"?client_id={Uri.EscapeDataString(creds.ClientId)}" +
2866 $"&response_type=code" +
2867 $"&redirect_uri={Uri.EscapeDataString(creds.Config.AzureRedirectUri)}" +
2868 $"&response_mode=query" +
2869 $"&scope={Uri.EscapeDataString(Credentials.GetScopeValue(creds.OAuthScope))}";
2870
2871 case CredentialServiceType.AWS:
2872 return $"{creds.Config.AWSAuthBase}/oauth2/authorize" +
2873 $"?client_id={Uri.EscapeDataString(creds.ClientId)}" +
2874 $"&response_type=code" +
2875 $"&redirect_uri={Uri.EscapeDataString(creds.Config.AWSRedirectUri)}" +
2876 $"&scope={Uri.EscapeDataString(Credentials.GetScopeValue(creds.OAuthScope))}";
2877
2878 case CredentialServiceType.Bitwarden:
2879 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Bitwarden OAuth not implemented.", creds, GPALObjectType.Other);
2880 break;
2881 }
2882
2883 return "";
2884 }
2885 }
2886}
JSON response from the AWS Cognito OAuth token endpoint.
string TokenType
OAuth token type, normally "Bearer".
string RefreshToken
Token used to obtain a new access token once it expires. Cognito may not always return one.
string AccessToken
Bearer access token for calling AWS APIs.
int ExpiresIn
Number of seconds until AccessToken expires.
string IdToken
OpenID Connect ID token containing identity claims for the authenticated user.
Authorization code (and granted scope) returned by GPALRESTAPI's get-access-token endpoint after a us...
string Code
OAuth authorization code to exchange for an access token.
string Scope
OAuth scope(s) granted along with the authorization code.
JSON response from the Azure AD (Microsoft Entra ID) OAuth token endpoint.
string RefreshToken
Token used to obtain a new access token once it expires.
string Scope
Space-delimited list of scopes granted to AccessToken.
string AccessToken
Bearer access token for calling Microsoft Graph or other Azure APIs.
string TokenType
OAuth token type, normally "Bearer".
int ExpiresIn
Number of seconds until AccessToken expires.
JSON response from Bitwarden's OAuth token endpoint.
string RefreshToken
Token used to obtain a new access token once it expires.
int ExpiresIn
Number of seconds until AccessToken expires.
string AccessToken
Bearer access token used to call the Bitwarden vault API.
string TokenType
OAuth token type, normally "Bearer".
Browser object that contains the fluent methods to create your Browser workflow. Instatiated using G...
Definition Browser.cs:68
MagicHelper MagicHelper
The MagicHelper used to issue commands when this Browser is using the OttoMagic engine.
Definition Browser.cs:7109
Endpoint URLs, redirect URIs, and protocol field names used by the credential service handlers....
string GrantTypeValue
Default OAuth grant_type value (resource owner password credentials).
string AzureAuthEndpoint
Path of the Azure AD OAuth token endpoint, appended to AzureAuthBase.
string AWSAuthBase
Base URL of your AWS Cognito hosted UI domain. Must be replaced with your actual Cognito domain.
bool UseRestRedirect
When true, the OAuth authorization code is retrieved by polling GPALRESTAPI's get-access-token endpoi...
string AccessTokenKey
JSON field key under which an OAuth token response returns the access token.
string PasswordKey
Form field key for the password parameter.
string GoogleAuthCodeKey
Query string key under which Google returns the authorization code.
string AWSRedirectUri
Redirect URI used for the manual (out-of-band) AWS Cognito OAuth flow.
string ClientIdKey
Form field key for the OAuth client_id parameter.
static void Save(GPALFile file=null)
Saves the default credentials configuration to the config file as a starter file for editing....
string SearchKey
Query parameter key used when searching a vault for an item.
string BitwardenVaultBase
Base URL for the Bitwarden public vault API.
string GoogleAuthEndpoint
Path of Google's OAuth token endpoint, appended to GoogleAuthBase.
string GoogleRedirectUri
Redirect URI used for the manual (out-of-band) Google OAuth flow.
string BitwardenAuthBase
Base URL for the Bitwarden identity (token) service.
string GoogleAuthBase
Base URL for Google's OAuth token endpoint.
string AuthorizationHeader
HTTP header name used to send the bearer access token.
string ClientSecretKey
Form field key for the OAuth client_secret parameter.
string GoogleRestRedirectUri
Redirect URI used when UseRestRedirect is true; GPALRESTAPI listens here for the OAuth callback.
string BitwardenVaultEndpoint
Path of the Bitwarden vault items endpoint, appended to BitwardenVaultBase.
string AzureRedirectUri
Redirect URI used for the manual (out-of-band) Azure OAuth flow.
string BitwardenAuthEndpoint
Path of the Bitwarden OAuth token endpoint, appended to BitwardenAuthBase.
static CredentialsConfig Load(GPALFile file=null)
Loads the credentials config, falling back to defaults if the file is missing or invalid....
string GoogleConsentBase
Base URL for Google's OAuth consent/authorization page.
string AWSAuthEndpoint
Path of the AWS Cognito OAuth token endpoint, appended to AWSAuthBase.
string GrantTypeKey
Form field key for the OAuth grant_type parameter.
string AzureAuthBase
Base URL for the Azure AD (Microsoft Entra ID) OAuth token endpoint.
string UsernameKey
Form field key for the username parameter.
string GoogleServiceAccountTokenUri
Token endpoint used when exchanging a Google service-account JWT for an access token.
ICredentials ToGPALObject()
Fluent interface to return this credentials object as an ICredentials.
IAllowCredentialsServiceKeyWithCredentials WithAuthRedirectUrl(string redirectUrl)
Where the OAuth redirect lands, and therefore what has to catch the token. This is the one setting th...
string ServiceKey
Combined "ClientId:ClientSecret" service key supplied via .WithServiceKey or derived from ....
string RefreshToken
OAuth refresh token supplied via .WithRefreshToken, or returned by a token exchange and used to renew...
IAllowCredentialsServiceKeyWithCredentials WithAccessToken(string accessToken)
A token the workflow already holds, for the schemes that present one: Bearer, X-Auth-Token,...
IAllowCredentialsIntoCredentials WithServiceAccountKey(GoogleServiceAccountKey serviceAccountKey)
Supplies a Google Cloud service account key as a deserialized object, used to fetch an access token w...
CredentialsConfig Config
Endpoint/protocol configuration for the credential service handlers, loaded from ....
OAuthScope OAuthScope
OAuth scope requested via .WithScope when fetching an access token.
string Username
Username supplied via .WithUsername, or the username field returned from a vault item.
IAllowCredentialsIntoCredentials WithServiceAccountKey(GPALFile serviceAccountKeyFile)
Loads a Google Cloud service account key from a JSON key file, used to fetch an access token without ...
IAllowKeyFromApiEndpoint WithKeyFromApi(IRESTClient restClient)
Like WithServiceKey, but fetches the key via a GET request made with the supplied,...
IAllowCredentialsIntoCredentials WithDomain(string domain)
Restricts GetCredentialsFor/SaveTo results to vault items whose URL contains this domain....
IAllowCredentialsIntoCredentials WithRefreshToken(string refreshToken)
Supplies a previously-obtained OAuth refresh token, used to fetch a new access token without re-autho...
IAllowCredentialsIntoCredentials WithClientSecret(string clientSecret)
Sets the OAuth client secret and derives ServiceKey as "ClientId:ClientSecret".
IAllowCredentialsIntoCredentials GetCredentialsFor(string target)
Sets the search term used to find a matching item in the target password manager (e....
string Domain
Optional URL filter supplied via .WithDomain, used to narrow vault matches down to a specific site.
string ClientId
OAuth client ID supplied via .WithClientId or derived from .WithServiceKey.
IAllowCredentialsIntoCredentials WithServiceAccountKey(string serviceAccountKey)
Supplies a Google Cloud service account key as a raw JSON string, used to fetch an access token witho...
IAllowCredentialsServiceKeyWithCredentials WithWebAuth(WebAuthType webAuthType)
How the site expects to be told who you are. A browser handed this credential acts on it accordingly,...
IAllowClientSecret WithClientId(string clientId)
Sets the OAuth client ID used to authenticate to ServiceType.
string AuthCode
OAuth authorization code supplied via .WithAuthCode, or retrieved automatically during the OAuth redi...
IAllowCredentialsIntoCredentials WithCredentials(ICredentials credentials)
Supplies the login (master) credentials used to authenticate to the credential service itself (e....
IAllowCredentialsIntoCredentials WithServiceKey(string key)
Service key is a custom string consisting of your ClientID:ClientSecret.
IAllowCredentialsIntoCredentials WithScope(OAuthScope scope)
Sets the OAuth scope to request when an access token is fetched.
string AccessToken
Most recently fetched access token for ServiceType, set by FetchAccessToken/GetCredentialsFor.
IAllowCredentialsIntoCredentials WithEndpoint(string endpoint)
Completes .WithKeyFromApi(restClient) by calling the given endpoint on that RESTClient and using the ...
string ClientSecret
OAuth client secret supplied via .WithClientSecret or derived from .WithServiceKey.
IAllowCredentialsUsername WithService(CredentialServiceType serviceType)
Selects which password manager or cloud identity service this credentials request targets.
IAllowCredentialsServiceKeyWithCredentials WithPassword(string password)
Sets the password, either as a credential to retrieve directly (with CredentialServiceType....
IAllowCredentialsIntoCredentials WithAuthCode(string authCode)
Supplies a previously-obtained OAuth authorization code, skipping the interactive authorization step.
WebAuthType WebAuthType
How the site expects to be told who you are, supplied via .WithWebAuth. Read by a browser this creden...
string Target
Search term supplied via GetCredentialsFor, used by the password manager handlers to find a matching ...
void FetchAccessToken(out string accessToken)
Fetches a new access token for ServiceType using the credentials/configuration supplied so far,...
IAllowCredentialsIntoCredentials WithKeyFromEnv(string envVariableName)
Like WithServiceKey, but reads the key from an environment variable (checking process,...
IAllowGetCredentials SaveTo(IGPALGrid< string > grid)
Looks up the credential matching .GetCredentialsFor's target (filtered by .WithDomain if set) and app...
IAllowCredentialsPassword WithUsername(string username)
Sets the username, either as a credential to retrieve directly (with CredentialServiceType....
A single vault item returned by the Dashlane CLI (dcli password list --json).
string Username
Stored username/login.
string Title
Display title of the vault item.
string Id
Unique identifier of the vault item.
string Url
URL associated with the vault item, used for matching against .WithDomain.
string Notes
Free-form notes attached to the vault item.
string Otp
One-time-password/TOTP secret, if configured for the item.
string Password
Stored password.
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
string Filename
We have only one file, accessing it.
Definition GPALFile.cs:474
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static IAllowConverterInput Converter
New GPAL Convertor.
Definition GPAL.cs:560
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2406
static IAllowFileName File
Instantiates a new fluent File SETTINGS object. This data object defines settings for use by the ....
Definition GPAL.cs:508
Deserialized contents of a Google Cloud service account JSON key file, used by GoogleHandler....
string AuthUri
Google OAuth authorization endpoint URL.
string AuthProviderX509CertUrl
URL of Google's X.509 certificate provider for verifying tokens.
string PrivateKeyId
Identifier of the private key within the service account.
string TokenUri
Google OAuth token endpoint URL used to exchange the signed JWT for an access token.
string ClientId
OAuth client ID associated with the service account.
string ClientEmail
Email address of the service account; used as the JWT issuer (iss claim).
string ClientX509CertUrl
URL of this service account's X.509 certificate.
string PrivateKey
PEM-encoded RSA private key used to sign the JWT assertion.
string ProjectId
Google Cloud project ID that owns this service account.
string Type
Key type, normally "service_account".
JSON response from Google's OAuth token endpoint.
string TokenType
OAuth token type, normally "Bearer".
string AccessToken
Bearer access token for calling Google APIs.
int ExpiresIn
Number of seconds until AccessToken expires.
string RefreshToken
Token used to obtain a new access token once it expires. Only returned on the initial authorization c...
Login fields (username, password, and URL) of a Keeper record.
string Url
URL associated with the login, used for matching against .WithDomain.
string Password
Stored password.
string Username
Stored username/login.
A single record returned by Keeper Commander (keeper list/keeper search --format=json).
KeeperLoginData Login
Login fields (username, password, URL) of the record, if it is a login record.
string Notes
Free-form notes attached to the record.
string RecordType
Keeper record type (e.g. "login").
string Title
Display title of the record.
string RecordUid
Unique identifier of the Keeper record.
A single vault item returned by the LastPass CLI (lpass ls --json).
string Username
Stored username/login.
string Id
Unique identifier of the vault item.
string Password
Stored password.
string Url
URL associated with the vault item, used for matching against .WithDomain.
string Notes
Free-form notes attached to the vault item.
string Name
Display name of the vault item.
A single custom field of a 1Password item, such as a username or password field.
string Value
Field value, when not a secret reference.
string Label
Display label of the field (e.g. "username", "password").
string Reference
secret reference URI (op://...) used to resolve the value for secret fields.
string Type
Field type as reported by the 1Password CLI (e.g. "STRING", "CONCEALED").
string Id
Unique identifier of the field within the item.
A single vault item returned by the 1Password CLI (op item list --format=json).
List< OnePasswordUrl > Urls
URLs associated with this item, used for matching against .WithDomain.
string Id
Unique identifier of the vault item.
string Title
Display title of the vault item.
List< OnePasswordField > Fields
Custom fields (e.g. username, password) belonging to this item.
string Vault
Name or ID of the 1Password vault containing this item.
A single URL associated with a 1Password item.
string Href
The URL string.