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)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)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 case OAuthScope.YouTube_Upload:
812 return "https://www.googleapis.com/auth/youtube.upload";
813 case OAuthScope.YouTube_Manage:
814 return "https://www.googleapis.com/auth/youtube";
815
816 // Azure
817 case OAuthScope.Azure_GraphUserRead:
818 return "https://graph.microsoft.com/User.Read";
819 case OAuthScope.Azure_GraphMailRead:
820 return "https://graph.microsoft.com/Mail.Read";
821 case OAuthScope.Azure_Storage:
822 return "https://storage.azure.com/user_impersonation";
823 case OAuthScope.Azure_Management:
824 return "https://management.azure.com/user_impersonation";
825
826 // AWS
827 case OAuthScope.AWS_CognitoOpenId:
828 return "openid";
829 case OAuthScope.AWS_CognitoProfile:
830 return "profile";
831 case OAuthScope.AWS_APIGatewayCustom:
832 return "myapp/read"; // your custom
833
834 default:
835 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unsupported scope: [{scope}]", scope, GPALObjectType.Other);
836 return string.Empty;
837 }
838 }
839
840 // =======================================================
841 // FLUENT interface
842 // =======================================================
849 {
850 OAuthScope = scope;
851 return this;
852 }
853
859 {
860 AuthCode = authCode;
861 return this;
862 }
863
868 public IAllowCredentialSettings WithRefreshToken(string refreshToken)
869 {
870 RefreshToken = refreshToken;
871 return this;
872 }
873
874 internal Credentials(Browser.Browser browser)
875 {
877 Browser = browser;
878 }
879
885 public IAllowCredentialMethod WithService(CredentialServiceType serviceType)
886 {
887 ServiceType = serviceType;
888 return this;
889 }
890
897 {
898 Username = username;
899 return this;
900 }
901
907 public IAllowOAuthClient WithPassword(string password)
908 {
909 Password = password;
910 return this;
911 }
912
920 {
921 WebAuthType = webAuthType;
922 return this;
923 }
924
934 public IAllowOAuthClient WithAuthRedirectUrl(string redirectUrl)
935 {
936 Config.GoogleRestRedirectUri = redirectUrl;
937 return this;
938 }
939
948 public IAllowOAuthClient WithAccessToken(string accessToken)
949 {
950 AccessToken = accessToken;
951 return this;
952 }
953
960 {
961 Domain = domain;
962 return this;
963 }
964
969 public IAllowClientSecret WithClientId(string clientId)
970 {
971 ClientId = clientId;
972 return this;
973 }
974
979 public IAllowCredentialSettings WithClientSecret(string clientSecret)
980 {
981 ClientSecret = clientSecret;
982 ServiceKey = $"{ClientId}:{clientSecret}";
983 return this;
984 }
985
991 {
992 ServiceKey = key;
993 var parts = SafeSplit(key);
994 ClientId = parts.Item1;
995 ClientSecret = parts.Item2;
996 return this;
997 }
998
1005 public IAllowCredentialSettings WithKeyFromEnv(string envVariableName)
1006 {
1007 string key = Environment.GetEnvironmentVariable(envVariableName, EnvironmentVariableTarget.Process)
1008 ?? Environment.GetEnvironmentVariable(envVariableName, EnvironmentVariableTarget.User)
1009 ?? Environment.GetEnvironmentVariable(envVariableName, EnvironmentVariableTarget.Machine);
1010
1011 if (string.IsNullOrEmpty(key))
1012 {
1014 GPALEventType.ERROR,
1015 $"Environment variable [{envVariableName}] is not set or empty.",
1016 null,
1017 GPALObjectType.None);
1018 return this;
1019 }
1020
1021 return WithServiceKey(key);
1022 }
1023
1032 {
1033 _keyFromApiRestClient = restClient;
1034 return this;
1035 }
1036
1044 {
1045 string key = _keyFromApiRestClient
1046 .WithEndpoint(endpoint)
1047 .Execute();
1048
1049 if (string.IsNullOrEmpty(key))
1050 {
1052 GPALEventType.ERROR,
1053 $"REST API returned no key for endpoint [{endpoint}].",
1054 null,
1055 GPALObjectType.None);
1056 return this;
1057 }
1058
1059 return WithServiceKey(key);
1060 }
1061
1068 {
1069 if (serviceAccountKeyFile == null || string.IsNullOrWhiteSpace(serviceAccountKeyFile.Filename) || !File.Exists(serviceAccountKeyFile.Filename))
1070 {
1072 GPALEventType.ERROR,
1073 $"Service account key file missing or not found: [{serviceAccountKeyFile?.Filename ?? "<null>"}]",
1074 null,
1075 GPALObjectType.None);
1076 return this;
1077 }
1078
1079 ServiceAccountKeyJson = File.ReadAllText(serviceAccountKeyFile.Filename);
1080 return this;
1081 }
1082
1088 public IAllowCredentialSettings WithServiceAccountKey(string serviceAccountKey)
1089 {
1090 if (string.IsNullOrWhiteSpace(serviceAccountKey))
1091 {
1093 GPALEventType.ERROR,
1094 "Service account key JSON string is empty.",
1095 null,
1096 GPALObjectType.None);
1097 return this;
1098 }
1099
1100 ServiceAccountKeyJson = serviceAccountKey;
1101 return this;
1102 }
1103
1110 {
1111 if (serviceAccountKey == null)
1112 {
1114 GPALEventType.ERROR,
1115 "Service account key object is null.",
1116 null,
1117 GPALObjectType.None);
1118 return this;
1119 }
1120
1121 try
1122 {
1123 ServiceAccountKeyJson = System.Text.Json.JsonSerializer.Serialize(serviceAccountKey);
1124 }
1125 catch (Exception ex)
1126 {
1128 GPALEventType.EXCEPTION,
1129 $"Failed to serialize service account key object",
1130 null,
1131 GPALObjectType.None, ex);
1132 }
1133 return this;
1134 }
1135
1149 {
1150 if (masterCredentials is Credentials master)
1151 {
1152 Username = master.Username;
1153 Password = master.Password;
1154 ServiceKey = master.ServiceKey;
1155 ClientId = master.ClientId;
1156 ClientSecret = master.ClientSecret;
1157 ServiceAccountKeyJson = master.ServiceAccountKeyJson;
1158 AccessToken = master.AccessToken;
1159 RefreshToken = master.RefreshToken;
1160 OAuthScope = master.OAuthScope;
1161 }
1162
1163 return this;
1164 }
1165
1172 {
1173 Target = target;
1174 if (ServiceType != CredentialServiceType.None)
1175 {
1176 EnsureValidToken(out string accessToken);
1177 }
1178 return this;
1179 }
1180
1187 public IAllowGetCredentials SaveTo(IGPALGrid<string> grid)
1188 {
1189 if (grid == null)
1190 {
1191 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Grid cannot be null in SaveTo.", null, GPALObjectType.None);
1192 return this;
1193 }
1194
1195 var credentials = ServiceType switch
1196 {
1197 CredentialServiceType.Bitwarden => BitwardenHandler.Handle(this),
1198 CredentialServiceType.LastPass => LastPassHandler.Handle(this),
1199 CredentialServiceType.OnePassword => OnePasswordHandler.Handle(this),
1200 CredentialServiceType.Dashlane => DashlaneHandler.Handle(this),
1201 CredentialServiceType.Keeper => KeeperHandler.Handle(this),
1202 CredentialServiceType.None => DirectHandler.Handle(this),
1203
1204 _ => throw new NotSupportedException(
1205 $"SaveTo is not supported for CredentialServiceType.{ServiceType}.")
1206 };
1207
1208 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{credentials.Count()}] credentials from [{ServiceType}] to grid.", null, GPALObjectType.None);
1209
1210 foreach (var credential in credentials)
1211 {
1212 grid.AddRow(new List<string> { credential.Username, credential.Password });
1213 }
1214
1215 return this;
1216 }
1217
1223 {
1224 return this;
1225 }
1226
1231 public void FetchAccessToken(out string accessToken)
1232 {
1233 switch (ServiceType)
1234 {
1235 case CredentialServiceType.Google:
1236 if (!string.IsNullOrEmpty(ServiceAccountKeyJson))
1237 {
1238 AccessToken = GoogleHandler.FetchServiceAccountToken(this);
1239 }
1240 else
1241 {
1242 AccessToken = GoogleHandler.FetchAccessToken(this); // Fallback to OAuth flow
1243 }
1244 break;
1245 case CredentialServiceType.AWS:
1246 AccessToken = AWSHandler.FetchAccessToken(this);
1247 break;
1248 case CredentialServiceType.Azure:
1249 AccessToken = AzureHandler.FetchAccessToken(this);
1250 break;
1251 case CredentialServiceType.Bitwarden:
1252 AccessToken = BitwardenHandler.FetchAccessToken(this);
1253 break;
1254 case CredentialServiceType.Dashlane:
1255 AccessToken = DashlaneHandler.FetchAccessToken(this);
1256 break;
1257 case CredentialServiceType.Keeper:
1258 AccessToken = KeeperHandler.FetchAccessToken(this);
1259 break;
1260 case CredentialServiceType.LastPass:
1261 AccessToken = LastPassHandler.FetchAccessToken(this);
1262 break;
1263 case CredentialServiceType.OnePassword:
1264 AccessToken = OnePasswordHandler.FetchAccessToken(this);
1265 break;
1266 case CredentialServiceType.StaticKey:
1267 // The key supplied via .WithServiceKey/.WithKeyFromEnv/.WithKeyFromApi IS the credential,
1268 // used directly as the Bearer/x-api-key header value - nothing to fetch.
1270 break;
1271 default:
1272 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No access token fetcher for service.", null, GPALObjectType.None);
1273 break;
1274 }
1275 accessToken = AccessToken;
1276 }
1277
1281 private bool IsTokenExpired()
1282 {
1283 if (string.IsNullOrEmpty(AccessToken) || IssueTime == DateTime.MinValue) return true;
1284 return DateTime.UtcNow > ExpiresAt;
1285 }
1286 }
1287
1291 internal class CredentialResult
1292 {
1296 public string Username { get; }
1300 public string Password { get; }
1301
1302 public CredentialResult(string username, string password)
1303 {
1304 Username = username;
1305 Password = password;
1306 }
1307 }
1308
1312 internal static class DirectHandler
1313 {
1319 internal static List<CredentialResult> Handle(Credentials creds)
1320 {
1321 var results = new List<CredentialResult>();
1322
1323 if (string.IsNullOrEmpty(creds.Username) && string.IsNullOrEmpty(creds.Password))
1324 {
1325 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Username and Password are both empty.", null, GPALObjectType.None);
1326 return results;
1327 }
1328
1329 results.Add(new CredentialResult(creds.Username ?? "", creds.Password ?? ""));
1330 return results;
1331 }
1332 }
1333
1337 internal static class AWSHandler
1338 {
1344 internal static string FetchAccessToken(Credentials creds)
1345 {
1346 creds.AccessToken = "";
1347 if (string.IsNullOrEmpty(creds.ClientId) || string.IsNullOrEmpty(creds.ClientSecret))
1348 {
1349 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Client ID or Client Secret is missing.", null, GPALObjectType.None);
1350 return creds.AccessToken;
1351 }
1352
1353 string scopeUrl = Credentials.GetScopeValue(creds.OAuthScope);
1354
1355 if (!string.IsNullOrEmpty(creds.RefreshToken))
1356 {
1357 // Refresh token flow
1358 var parameters = $"grant_type=refresh_token&" +
1359 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1360 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1361 $"refresh_token={Uri.EscapeDataString(creds.RefreshToken)}";
1362
1363 var client = GPAL.RESTClient
1364 .WithAPIBase(creds.Config.AWSAuthBase) // Use config property
1365 .WithEndpoint(creds.Config.AWSAuthEndpoint)
1366 .WithParameters(parameters);
1367 var response = client.Execute();
1368
1369 try
1370 {
1371 var authData = System.Text.Json.JsonSerializer.Deserialize<AWSTokenResponse>(response);
1372
1373 // Note: Cognito may not return a new refresh token
1374 if (authData != null)
1375 {
1376 creds.AccessToken = authData.AccessToken ?? "";
1377
1378 if (string.IsNullOrEmpty(creds.AccessToken))
1379 {
1380 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh AWS access token. Response: [{response}]", null, GPALObjectType.None);
1381 }
1382 else if (!string.IsNullOrEmpty(authData?.RefreshToken))
1383 {
1384 creds.WithRefreshToken(authData.RefreshToken);
1385 }
1386
1387 if (!string.IsNullOrEmpty(creds.AccessToken))
1388 {
1389 creds.IssueTime = DateTime.UtcNow;
1390 creds.ExpiresIn = authData.ExpiresIn;
1391 }
1392 }
1393 }
1394 catch (Exception ex)
1395 {
1396 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1397 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1398 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1399 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh AWS access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1400 }
1401 }
1402 else
1403 {
1404 creds.AuthCode = AuthHandler.GetAuthCode(creds);
1405
1406 if (string.IsNullOrEmpty(creds.AuthCode))
1407 {
1408 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No authorization code provided.", null, GPALObjectType.None);
1409 }
1410 else
1411 {
1412 // Authorization code flow
1413 var parameters = $"grant_type=authorization_code&" +
1414 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1415 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1416 $"code={Uri.EscapeDataString(creds.AuthCode)}&" +
1417 $"redirect_uri={Uri.EscapeDataString(creds.Config.AWSRedirectUri)}"; // Use AWS-specific redirect URI
1418
1419 var client = GPAL.RESTClient
1420 .WithAPIBase(creds.Config.AWSAuthBase) // Use config property
1421 .WithEndpoint(creds.Config.AWSAuthEndpoint)
1422 .WithParameters(parameters);
1423 var response = client.Execute();
1424
1425 try
1426 {
1427 var authData = System.Text.Json.JsonSerializer.Deserialize<AWSTokenResponse>(response);
1428
1429 if (authData != null)
1430 {
1431 creds.AccessToken = authData.AccessToken ?? "";
1432 if (string.IsNullOrEmpty(creds.AccessToken))
1433 {
1434 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch AWS access token. Response: [{response}]", null, GPALObjectType.None);
1435 }
1436 else if (!string.IsNullOrEmpty(authData?.RefreshToken))
1437 {
1438 creds.WithRefreshToken(authData.RefreshToken);
1439 }
1440
1441 if (!string.IsNullOrEmpty(creds.AccessToken))
1442 {
1443 creds.IssueTime = DateTime.UtcNow;
1444 creds.ExpiresIn = authData.ExpiresIn;
1445 }
1446 }
1447 }
1448 catch (Exception ex)
1449 {
1450 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1451 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1452 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1453 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch AWS access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1454 }
1455 }
1456 }
1457
1458 return creds.AccessToken;
1459 }
1460 }
1464 internal static class AzureHandler
1465 {
1471 internal static string FetchAccessToken(Credentials creds)
1472 {
1473 creds.AccessToken = "";
1474 if (string.IsNullOrEmpty(creds.ClientId) || string.IsNullOrEmpty(creds.ClientSecret))
1475 {
1476 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Client ID or Client Secret is missing.", null, GPALObjectType.None);
1477 return creds.AccessToken;
1478 }
1479
1480 string scopeUrl = Credentials.GetScopeValue(creds.OAuthScope);
1481
1482 if (!string.IsNullOrEmpty(creds.RefreshToken))
1483 {
1484 // Refresh token flow
1485 var parameters = $"grant_type=refresh_token&" +
1486 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1487 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1488 $"refresh_token={Uri.EscapeDataString(creds.RefreshToken)}&" +
1489 $"scope={Uri.EscapeDataString(scopeUrl)}";
1490
1491 var client = GPAL.RESTClient
1492 .WithAPIBase(creds.Config.AzureAuthBase) // Use config property
1493 .WithEndpoint(creds.Config.AzureAuthEndpoint)
1494 .WithParameters(parameters);
1495 var response = client.Execute();
1496
1497 try
1498 {
1499 creds.AzureTokenResponse = System.Text.Json.JsonSerializer.Deserialize<AzureTokenResponse>(response);
1500
1501 if (creds.AzureTokenResponse != null)
1502 {
1503 creds.AccessToken = creds.AzureTokenResponse.AccessToken ?? "";
1504
1505 if (!string.IsNullOrEmpty(creds.AccessToken))
1506 {
1507 creds.ExpiresIn = creds.AzureTokenResponse.ExpiresIn;
1508 creds.IssueTime = DateTime.UtcNow;
1509 }
1510
1511 if (!string.IsNullOrEmpty(creds.AzureTokenResponse.RefreshToken))
1512 {
1513 creds.WithRefreshToken(creds.AzureTokenResponse.RefreshToken);
1514 }
1515 }
1516 else if (string.IsNullOrEmpty(creds.AccessToken))
1517 {
1518 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh Azure access token. Response: [{response}]", null, GPALObjectType.None);
1519 }
1520 }
1521 catch (Exception ex)
1522 {
1523 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1524 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1525 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1526 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to refresh Azure access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1527 }
1528 }
1529 else
1530 {
1531 creds.AuthCode = AuthHandler.GetAuthCode(creds);
1532
1533 if (string.IsNullOrEmpty(creds.AuthCode))
1534 {
1535 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No authorization code provided.", null, GPALObjectType.None);
1536 }
1537 else
1538 {
1539 // Authorization code flow
1540 var parameters = $"grant_type=authorization_code&" +
1541 $"client_id={Uri.EscapeDataString(creds.ClientId)}&" +
1542 $"client_secret={Uri.EscapeDataString(creds.ClientSecret)}&" +
1543 $"code={Uri.EscapeDataString(creds.AuthCode)}&" +
1544 $"redirect_uri={Uri.EscapeDataString(creds.Config.AzureRedirectUri)}&" + // Use Azure-specific redirect URI
1545 $"scope={Uri.EscapeDataString(scopeUrl)}";
1546
1547 var client = GPAL.RESTClient
1548 .WithAPIBase(creds.Config.AzureAuthBase) // Use config property
1549 .WithEndpoint(creds.Config.AzureAuthEndpoint)
1550 .WithParameters(parameters);
1551 var response = client.Execute();
1552
1553 try
1554 {
1555 creds.AzureTokenResponse = System.Text.Json.JsonSerializer.Deserialize<AzureTokenResponse>(response);
1556
1557 if (creds.AzureTokenResponse != null)
1558 {
1559 creds.AccessToken = creds.AzureTokenResponse.AccessToken ?? "";
1560
1561 if (string.IsNullOrEmpty(creds.AccessToken))
1562 {
1563 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch Azure access token. Response: [{response}]", null, GPALObjectType.None);
1564 }
1565 else if (!string.IsNullOrEmpty(creds.AzureTokenResponse?.RefreshToken))
1566 {
1567 creds.WithRefreshToken(creds.AzureTokenResponse.RefreshToken);
1568 }
1569
1570 if (!string.IsNullOrEmpty(creds.AccessToken))
1571 {
1572 creds.ExpiresIn = creds.AzureTokenResponse.ExpiresIn;
1573 creds.IssueTime = DateTime.UtcNow;
1574 }
1575 }
1576 }
1577 catch (Exception ex)
1578 {
1579 var errorResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1580 string error = errorResponse?.ContainsKey("error") == true ? errorResponse["error"] : "Unknown error";
1581 string errorDescription = errorResponse?.ContainsKey("error_description") == true ? errorResponse["error_description"] : "No description";
1582 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Failed to fetch Azure access token. Error: [{error}], Description: [{errorDescription}]", null, GPALObjectType.None, ex);
1583 }
1584 }
1585 }
1586
1587 return creds.AccessToken;
1588 }
1589 }
1590
1594 internal static class GoogleHandler
1595 {
1601 internal static string FetchAccessToken(Credentials creds)
1602 {
1603 creds.AccessToken = "";
1604
1605 // the pair Google is called with, whichever way the workflow gave it. WithServiceKey takes both
1606 // as one "id:secret" value and WithClientId and WithClientSecret take them apart, and both flows
1607 // below need the same answer, so it is worked out once here
1608 string clientId = creds.ClientId;
1609 string clientSecret = creds.ClientSecret;
1610
1611 if (!string.IsNullOrEmpty(creds.ServiceKey))
1612 {
1613 var parts = Credentials.SafeSplit(creds.ServiceKey);
1614 clientId = parts.Item1;
1615 clientSecret = parts.Item2;
1616 }
1617
1618 if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
1619 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Google needs a client id and a client secret. Give them to WithClientId and WithClientSecret, or both to WithServiceKey as [id:secret].");
1620
1621 if (!string.IsNullOrEmpty(creds.RefreshToken))
1622 {
1623 var parameters = $"grant_type=refresh_token&" +
1624 $"client_id={Uri.EscapeDataString(clientId)}&" +
1625 $"client_secret={Uri.EscapeDataString(clientSecret)}&" +
1626 $"refresh_token={Uri.EscapeDataString(creds.RefreshToken)}";
1627
1628 var response = GPAL.RESTClient
1629 .WithAPIBase(creds.Config.GoogleAuthBase)
1630 .WithEndpoint(creds.Config.GoogleAuthEndpoint)
1631 .WithParameters(parameters)
1632 .WithEncoding(ContentEncoding.UrlEncoded)
1633 .Execute();
1634
1635 creds.GoogleTokenResponse = System.Text.Json.JsonSerializer.Deserialize<GoogleTokenResponse>(response);
1636 creds.AccessToken = creds.GoogleTokenResponse?.AccessToken ?? "";
1637
1638 if (!string.IsNullOrEmpty(creds.AccessToken))
1639 {
1640 creds.IssueTime = DateTime.UtcNow;
1641 creds.ExpiresIn = creds.GoogleTokenResponse.ExpiresIn;
1642 }
1643 else
1644 {
1645 GPAL.PublishSimpleEvent(
1646 GPALEventType.ERROR,
1647 $"Failed to refresh Google access token. Response: [{response}]",
1648 null,
1649 GPALObjectType.None);
1650 }
1651 }
1652 else
1653 {
1654 creds.AuthCode = AuthHandler.GetAuthCode(creds);
1655
1656 if (string.IsNullOrEmpty(creds.AuthCode))
1657 {
1658 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "No authorization code provided.", null, GPALObjectType.None);
1659 }
1660 else
1661 {
1662 string redirectUri = creds.Config.UseRestRedirect
1663 ? creds.Config.GoogleRestRedirectUri
1664 : creds.Config.GoogleRedirectUri;
1665
1666 var parameters = $"{creds.Config.GrantTypeKey}=authorization_code&" +
1667 $"client_id={Uri.EscapeDataString(clientId)}&" +
1668 $"client_secret={Uri.EscapeDataString(clientSecret)}&" +
1669 $"{creds.Config.GoogleAuthCodeKey}={Uri.EscapeDataString(creds.AuthCode)}&" +
1670 $"redirect_uri={Uri.EscapeDataString(redirectUri)}";
1671
1672 var response = GPAL.RESTClient
1673 .WithAPIBase(creds.Config.GoogleAuthBase)
1674 .WithEndpoint(creds.Config.GoogleAuthEndpoint)
1675 .WithParameters(parameters)
1676 .WithEncoding(ContentEncoding.UrlEncoded)
1677 .Execute();
1678
1679 creds.GoogleTokenResponse = System.Text.Json.JsonSerializer.Deserialize<GoogleTokenResponse>(response);
1680 creds.AccessToken = creds.GoogleTokenResponse?.AccessToken ?? "";
1681
1682 if (!string.IsNullOrEmpty(creds.AccessToken))
1683 {
1684 creds.IssueTime = DateTime.UtcNow;
1685 creds.ExpiresIn = creds.GoogleTokenResponse.ExpiresIn;
1686 }
1687 else
1688 {
1689 GPAL.PublishSimpleEvent(
1690 GPALEventType.ERROR,
1691 $"Failed to fetch Google access token. Response: [{response}]",
1692 null,
1693 GPALObjectType.None);
1694 }
1695
1696 if (!string.IsNullOrEmpty(creds.GoogleTokenResponse?.RefreshToken))
1697 {
1698 creds.WithRefreshToken(creds.GoogleTokenResponse.RefreshToken);
1699 }
1700 }
1701 }
1702
1703 return creds.AccessToken;
1704 }
1705
1711 public static string Base64UrlEncode(byte[] input)
1712 {
1713 return Convert.ToBase64String(input)
1714 .TrimEnd('=') // remove padding
1715 .Replace('+', '-') // URL safe
1716 .Replace('/', '_'); // URL safe
1717 }
1718
1724 private static byte[] Base64UrlDecode(string input)
1725 {
1726 string padded = input
1727 .Replace('-', '+')
1728 .Replace('_', '/');
1729
1730 switch (padded.Length % 4)
1731 {
1732 case 2: padded += "=="; break;
1733 case 3: padded += "="; break;
1734 }
1735
1736 return Convert.FromBase64String(padded);
1737 }
1738
1744 internal static string FetchServiceAccountToken(Credentials creds)
1745 {
1746 creds.AccessToken = "";
1747
1748 if (string.IsNullOrEmpty(creds.ServiceAccountKeyJson))
1749 {
1750 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Service account key JSON is missing.", null, GPALObjectType.None);
1751 return creds.AccessToken;
1752 }
1753
1754 try
1755 {
1756 var serviceAccount = System.Text.Json.JsonSerializer.Deserialize<GoogleServiceAccountKey>(creds.ServiceAccountKeyJson);
1757
1758 var iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
1759 var exp = iat + 3600;
1760
1761 var pemReader = new PemReader(new StringReader(serviceAccount.PrivateKey));
1762 var keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject();
1763 var privateKey = (Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters)keyPair.Private;
1764
1765 var header = new { alg = "RS256", typ = "JWT" };
1766
1767 var payload = new
1768 {
1769 iss = serviceAccount.ClientEmail,
1770 scope = Credentials.GetScopeValue(creds.OAuthScope),
1771 iat,
1772 exp
1773 };
1774
1775 var headerJson = JsonSerializer.Serialize(header);
1776 var payloadJson = JsonSerializer.Serialize(payload);
1777
1778 var headerBytes = Encoding.UTF8.GetBytes(headerJson);
1779 var payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
1780
1781 string headerBase64Url = Base64UrlEncode(headerBytes);
1782 string payloadBase64Url = Base64UrlEncode(payloadBytes);
1783
1784 string unsignedJwt = $"{headerBase64Url}.{payloadBase64Url}";
1785
1786 var signer = SignerUtilities.GetSigner("SHA256withRSA");
1787 signer.Init(true, privateKey);
1788 var bytes = Encoding.UTF8.GetBytes(unsignedJwt);
1789 signer.BlockUpdate(bytes, 0, bytes.Length);
1790
1791 var signatureBytes = signer.GenerateSignature();
1792 var signature = Base64UrlEncode(signatureBytes);
1793
1794 var jwt = $"{unsignedJwt}.{signature}";
1795
1796 var parameters = $"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={Uri.EscapeDataString(jwt)}";
1797
1798 var response = GPAL.RESTClient
1799 .WithAPIBase(creds.Config.GoogleAuthBase)
1800 .WithEndpoint(creds.Config.GoogleServiceAccountTokenUri)
1801 .WithParameters(parameters)
1802 .WithEncoding(ContentEncoding.UrlEncoded)
1803 .WithHttpMethod("POST")
1804 .Execute();
1805
1806 var authData = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(response);
1807
1808 creds.AccessToken = authData?[creds.Config.AccessTokenKey] ?? "";
1809
1810 if (!string.IsNullOrEmpty(creds.AccessToken))
1811 {
1812 creds.IssueTime = DateTime.UtcNow;
1813
1814 if (authData != null &&
1815 authData.TryGetValue("expires_in", out var expStr) &&
1816 int.TryParse(expStr, out int expires))
1817 {
1818 creds.ExpiresIn = expires;
1819 }
1820 else
1821 {
1822 creds.ExpiresIn = 0;
1823 }
1824 }
1825 else
1826 {
1827 GPAL.PublishSimpleEvent(
1828 GPALEventType.ERROR,
1829 $"Failed to fetch service account token. Response: [{response}]",
1830 null,
1831 GPALObjectType.None);
1832 }
1833 }
1834 catch (Exception ex)
1835 {
1836 GPAL.PublishSimpleEvent(
1837 GPALEventType.ERROR,
1838 $"Failed to fetch service account token",
1839 null,
1840 GPALObjectType.None, ex);
1841 }
1842
1843 return creds.AccessToken;
1844 }
1845 }
1846
1850 internal static class BitwardenHandler
1851 {
1857 internal static List<CredentialResult> Handle(Credentials creds)
1858 {
1859 var results = new List<CredentialResult>();
1860
1861 creds.AccessToken = FetchAccessToken(creds);
1862
1863 if (string.IsNullOrEmpty(creds.AccessToken))
1864 return results;
1865
1866 var vaultJson = GPAL.RESTClient
1867 .WithAPIBase(creds.Config.BitwardenVaultBase)
1868 .WithEndpoint(creds.Config.BitwardenVaultEndpoint)
1869 .WithParameters(new Dictionary<string, string>
1870 {
1871 { creds.Config.SearchKey, creds.Target }
1872 })
1873 .WithHeader(creds.Config.AuthorizationHeader, $"Bearer {creds.AccessToken}")
1874 .Execute();
1875
1876 var vaultData = JsonSerializer.Deserialize<BitwardenVaultResponse>(vaultJson);
1877
1878 if (vaultData?.Data != null)
1879 {
1880 foreach (var item in vaultData.Data)
1881 {
1882 if (item.Login != null &&
1883 Credentials.MatchesDomain(item.Login.Uris?.Select(u => u.Uri), creds.Domain))
1884 results.Add(new CredentialResult(item.Login.Username, item.Login.Password));
1885 }
1886 }
1887
1888 return results;
1889 }
1890
1896 internal static string FetchAccessToken(Credentials creds)
1897 {
1898 var (clientId, clientSecret) = Credentials.SafeSplit(creds.ServiceKey);
1899
1900 var response = GPAL.RESTClient
1901 .WithAPIBase(creds.Config.BitwardenAuthBase)
1902 .WithEndpoint(creds.Config.BitwardenAuthEndpoint)
1903 .WithParameters(new
1904 {
1905 grant_type = "password",
1906 username = creds.Username,
1907 password = creds.Password,
1908 client_id = clientId,
1909 client_secret = clientSecret
1910 })
1911 .Execute();
1912
1913 creds.BitwardenTokenResponse = JsonSerializer.Deserialize<BitwardenTokenResponse>(response);
1914 creds.AccessToken = creds.BitwardenTokenResponse?.AccessToken ?? "";
1915
1916 if (!string.IsNullOrEmpty(creds.AccessToken))
1917 {
1918 creds.IssueTime = DateTime.UtcNow;
1919 creds.ExpiresIn = creds.BitwardenTokenResponse.ExpiresIn;
1920 }
1921 else
1922 {
1923 GPAL.PublishSimpleEvent(
1924 GPALEventType.ERROR,
1925 $"Bitwarden auth failed. Response: [{response}]",
1926 null,
1927 GPALObjectType.None);
1928 }
1929
1930 return creds.AccessToken;
1931 }
1932 }
1933
1937 internal static class LastPassHandler
1938 {
1944 internal static List<CredentialResult> Handle(Credentials creds)
1945 {
1946 var results = new List<CredentialResult>();
1947
1948 // Step 1: Ensure we have an active LastPass session
1949 if (!EnsureLoggedIn(creds))
1950 return results;
1951
1952 try
1953 {
1954 // Step 2: Get all items as JSON (most reliable way to search)
1955 var process = new Process
1956 {
1957 StartInfo = new ProcessStartInfo
1958 {
1959 FileName = "lpass",
1960 Arguments = "ls --json",
1961 RedirectStandardOutput = true,
1962 UseShellExecute = false,
1963 CreateNoWindow = true
1964 }
1965 };
1966
1967 process.Start();
1968 string jsonOutput = process.StandardOutput.ReadToEnd();
1969 process.WaitForExit();
1970
1971 if (process.ExitCode != 0)
1972 {
1973 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"LastPass CLI returned exit code [{process.ExitCode}].", null, GPALObjectType.None);
1974 return results;
1975 }
1976
1977 var items = JsonSerializer.Deserialize<List<LastPassItem>>(jsonOutput);
1978
1979 if (items != null)
1980 {
1981 string searchTerm = (creds.Target ?? "").ToLowerInvariant();
1982
1983 foreach (var item in items)
1984 {
1985 if (string.IsNullOrEmpty(item.Username) && string.IsNullOrEmpty(item.Password))
1986 continue;
1987
1988 bool matches = string.IsNullOrEmpty(searchTerm) ||
1989 (item.Name?.ToLowerInvariant().Contains(searchTerm) == true) ||
1990 (item.Url?.ToLowerInvariant().Contains(searchTerm) == true) ||
1991 (item.Username?.ToLowerInvariant().Contains(searchTerm) == true);
1992
1993 if (matches && Credentials.MatchesDomain(item.Url, creds.Domain))
1994 {
1995 results.Add(new CredentialResult(item.Username ?? "", item.Password ?? ""));
1996 }
1997 }
1998 }
1999 }
2000 catch (Exception ex)
2001 {
2002 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"LastPass vault retrieval failed", null, GPALObjectType.None, ex);
2003 }
2004
2005 return results;
2006 }
2007
2013 internal static string FetchAccessToken(Credentials creds)
2014 {
2015 // LastPass CLI doesn't use a traditional "access token" like the others.
2016 // We just ensure the session is valid (this is called by Handle).
2017 EnsureLoggedIn(creds);
2018 return "LastPass-CLI-Session"; // placeholder - not really used
2019 }
2020
2026 private static bool EnsureLoggedIn(Credentials creds)
2027 {
2028 try
2029 {
2030 var statusProcess = new Process
2031 {
2032 StartInfo = new ProcessStartInfo
2033 {
2034 FileName = "lpass",
2035 Arguments = "status",
2036 RedirectStandardOutput = true,
2037 UseShellExecute = false,
2038 CreateNoWindow = true
2039 }
2040 };
2041
2042 statusProcess.Start();
2043 string statusOutput = statusProcess.StandardOutput.ReadToEnd().Trim();
2044 statusProcess.WaitForExit();
2045
2046 if (statusOutput.ToLower().Contains("logged in"))
2047 return true;
2048
2049 // Not logged in > give clear instruction
2050 GPAL.PublishSimpleEvent(
2051 GPALEventType.ERROR,
2052 $"LastPass CLI is not logged in. Please run this command once on the machine:\n" +
2053 $"lpass login [{creds.Username}]\n" +
2054 $"Then re-run your automation.",
2055 null, GPALObjectType.None);
2056
2057 return false;
2058 }
2059 catch (Exception ex)
2060 {
2061 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2062 $"LastPass CLI check failed. Is the LastPass CLI (lpass) installed?",
2063 null, GPALObjectType.None, ex);
2064 return false;
2065 }
2066 }
2067 }
2068
2072 internal static class OnePasswordHandler
2073 {
2079 internal static List<CredentialResult> Handle(Credentials creds)
2080 {
2081 var results = new List<CredentialResult>();
2082
2083 // Ensure we have a valid session
2084 if (!EnsureSession(creds))
2085 return results;
2086
2087 try
2088 {
2089 string search = creds.Target?.Trim() ?? "";
2090
2091 // Build command: list items and get full details in one go where possible
2092 string arguments = string.IsNullOrEmpty(search)
2093 ? "item list --format=json"
2094 : $"item list --search=\"{search}\" --format=json";
2095
2096 var process = new Process
2097 {
2098 StartInfo = new ProcessStartInfo
2099 {
2100 FileName = "op",
2101 Arguments = arguments,
2102 RedirectStandardOutput = true,
2103 UseShellExecute = false,
2104 CreateNoWindow = true
2105 }
2106 };
2107
2108 process.Start();
2109 string jsonOutput = process.StandardOutput.ReadToEnd();
2110 process.WaitForExit();
2111
2112 if (process.ExitCode != 0)
2113 {
2114 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2115 $"1Password CLI returned exit code [{process.ExitCode}]. Make sure you are signed in.",
2116 null, GPALObjectType.None);
2117 return results;
2118 }
2119
2120 var items = JsonSerializer.Deserialize<List<OnePasswordItem>>(jsonOutput);
2121
2122 if (items != null)
2123 {
2124 foreach (var item in items)
2125 {
2126 // Try to extract username and password fields
2127 string username = "";
2128 string password = "";
2129
2130 foreach (var field in item.Fields)
2131 {
2132 if (string.Equals(field.Label, "username", StringComparison.OrdinalIgnoreCase) ||
2133 string.Equals(field.Type, "username", StringComparison.OrdinalIgnoreCase))
2134 {
2135 username = field.Value ?? "";
2136 }
2137 else if (string.Equals(field.Label, "password", StringComparison.OrdinalIgnoreCase) ||
2138 string.Equals(field.Type, "password", StringComparison.OrdinalIgnoreCase))
2139 {
2140 password = field.Value ?? field.Reference ?? "";
2141 }
2142 }
2143
2144 if ((!string.IsNullOrEmpty(username) || !string.IsNullOrEmpty(password)) &&
2145 Credentials.MatchesDomain(item.Urls?.Select(u => u.Href), creds.Domain))
2146 {
2147 results.Add(new CredentialResult(username, password));
2148 }
2149 }
2150 }
2151 }
2152 catch (Exception ex)
2153 {
2154 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2155 $"1Password vault retrieval failed", null, GPALObjectType.None, ex);
2156 }
2157
2158 return results;
2159 }
2160
2166 internal static string FetchAccessToken(Credentials creds)
2167 {
2168 // 1Password uses a session token instead of a traditional access token
2169 EnsureSession(creds);
2170 return "1Password-CLI-Session";
2171 }
2172
2178 private static bool EnsureSession(Credentials creds)
2179 {
2180 try
2181 {
2182 // Simple check if CLI is ready
2183 var process = new Process
2184 {
2185 StartInfo = new ProcessStartInfo
2186 {
2187 FileName = "op",
2188 Arguments = "whoami --format=json",
2189 RedirectStandardOutput = true,
2190 UseShellExecute = false,
2191 CreateNoWindow = true
2192 }
2193 };
2194
2195 process.Start();
2196 string output = process.StandardOutput.ReadToEnd();
2197 process.WaitForExit();
2198
2199 if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
2200 return true;
2201
2202 // Not signed in > give clear guidance
2203 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2204 $"1Password CLI is not signed in. Please run this once on the target machine:\n" +
2205 $"op signin\n" +
2206 $"Then re-run your automation. (Session lasts ~30 minutes of activity)",
2207 null, GPALObjectType.None);
2208
2209 return false;
2210 }
2211 catch (Exception ex)
2212 {
2213 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2214 $"1Password CLI check failed. Is the 1Password CLI (`op`) installed and in PATH?",
2215 null, GPALObjectType.None, ex);
2216 return false;
2217 }
2218 }
2219 }
2220
2224 internal static class DashlaneHandler
2225 {
2231 internal static List<CredentialResult> Handle(Credentials creds)
2232 {
2233 var results = new List<CredentialResult>();
2234
2235 // Ensure CLI session / login
2236 if (!EnsureLoggedIn(creds))
2237 return results;
2238
2239 try
2240 {
2241 string search = creds.Target?.Trim() ?? "";
2242
2243 // Use JSON output for reliable parsing
2244 string arguments = string.IsNullOrEmpty(search)
2245 ? "password list --json"
2246 : $"password list --search \"{search}\" --json";
2247
2248 var process = new Process
2249 {
2250 StartInfo = new ProcessStartInfo
2251 {
2252 FileName = "dcli", // or "dashlane" depending on installation
2253 Arguments = arguments,
2254 RedirectStandardOutput = true,
2255 RedirectStandardError = true,
2256 UseShellExecute = false,
2257 CreateNoWindow = true
2258 }
2259 };
2260
2261 process.Start();
2262 string jsonOutput = process.StandardOutput.ReadToEnd();
2263 string errorOutput = process.StandardError.ReadToEnd();
2264 process.WaitForExit();
2265
2266 if (process.ExitCode != 0)
2267 {
2268 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2269 $"Dashlane CLI failed (exit code [{process.ExitCode}]). Error: [{errorOutput.Trim()}]",
2270 null, GPALObjectType.None);
2271 return results;
2272 }
2273
2274 var items = JsonSerializer.Deserialize<List<DashlaneItem>>(jsonOutput);
2275
2276 if (items != null)
2277 {
2278 foreach (var item in items)
2279 {
2280 if (!string.IsNullOrEmpty(item.Username) || !string.IsNullOrEmpty(item.Password))
2281 {
2282 if (Credentials.MatchesDomain(item.Url, creds.Domain))
2283 results.Add(new CredentialResult(item.Username ?? "", item.Password ?? ""));
2284 }
2285 }
2286 }
2287 }
2288 catch (Exception ex)
2289 {
2290 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2291 $"Dashlane vault retrieval failed", null, GPALObjectType.None, ex);
2292 }
2293
2294 return results;
2295 }
2296
2302 internal static string FetchAccessToken(Credentials creds)
2303 {
2304 // Dashlane CLI uses a local session, not a traditional token
2305 EnsureLoggedIn(creds);
2306 return "Dashlane-CLI-Session";
2307 }
2308
2314 private static bool EnsureLoggedIn(Credentials creds)
2315 {
2316 try
2317 {
2318 var process = new Process
2319 {
2320 StartInfo = new ProcessStartInfo
2321 {
2322 FileName = "dcli",
2323 Arguments = "status --json",
2324 RedirectStandardOutput = true,
2325 UseShellExecute = false,
2326 CreateNoWindow = true
2327 }
2328 };
2329
2330 process.Start();
2331 string output = process.StandardOutput.ReadToEnd();
2332 process.WaitForExit();
2333
2334 if (process.ExitCode == 0 && output.ToLower().Contains("\"logged_in\":true"))
2335 return true;
2336
2337 // Not logged in > clear instruction
2338 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2339 $"Dashlane CLI is not logged in. Please run this once on the target machine:\n" +
2340 $"dcli login\n" +
2341 $"Then re-run your automation.",
2342 null, GPALObjectType.None);
2343
2344 return false;
2345 }
2346 catch (Exception ex)
2347 {
2348 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2349 $"Dashlane CLI check failed. Is the Dashlane CLI (`dcli`) installed and in PATH?",
2350 null, GPALObjectType.None, ex);
2351 return false;
2352 }
2353 }
2354 }
2355
2359 internal static class KeeperHandler
2360 {
2366 internal static List<CredentialResult> Handle(Credentials creds)
2367 {
2368 var results = new List<CredentialResult>();
2369
2370 if (!EnsureLoggedIn(creds))
2371 return results;
2372
2373 try
2374 {
2375 string search = creds.Target?.Trim() ?? "";
2376
2377 // Use "list" or "search" with JSON output
2378 string arguments = string.IsNullOrEmpty(search)
2379 ? "list --format=json"
2380 : $"search \"{search}\" --format=json";
2381
2382 var process = new Process
2383 {
2384 StartInfo = new ProcessStartInfo
2385 {
2386 FileName = "keeper",
2387 Arguments = arguments,
2388 RedirectStandardOutput = true,
2389 RedirectStandardError = true,
2390 UseShellExecute = false,
2391 CreateNoWindow = true
2392 }
2393 };
2394
2395 process.Start();
2396 string jsonOutput = process.StandardOutput.ReadToEnd();
2397 string errorOutput = process.StandardError.ReadToEnd();
2398 process.WaitForExit();
2399
2400 if (process.ExitCode != 0)
2401 {
2402 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2403 $"Keeper Commander returned exit code [{process.ExitCode}]. Error: [{errorOutput.Trim()}]",
2404 null, GPALObjectType.None);
2405 return results;
2406 }
2407
2408 var records = JsonSerializer.Deserialize<List<KeeperRecord>>(jsonOutput);
2409
2410 if (records != null)
2411 {
2412 foreach (var record in records)
2413 {
2414 string username = record.Login?.Username ?? "";
2415 string password = record.Login?.Password ?? "";
2416
2417 if ((!string.IsNullOrEmpty(username) || !string.IsNullOrEmpty(password)) &&
2418 Credentials.MatchesDomain(record.Login?.Url, creds.Domain))
2419 {
2420 results.Add(new CredentialResult(username, password));
2421 }
2422 }
2423 }
2424 }
2425 catch (Exception ex)
2426 {
2427 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2428 $"Keeper vault retrieval failed", null, GPALObjectType.None, ex);
2429 }
2430
2431 return results;
2432 }
2433
2439 internal static string FetchAccessToken(Credentials creds)
2440 {
2441 // Keeper uses a persistent session via Commander
2442 EnsureLoggedIn(creds);
2443 return "Keeper-Commander-Session";
2444 }
2445
2451 private static bool EnsureLoggedIn(Credentials creds)
2452 {
2453 try
2454 {
2455 var process = new Process
2456 {
2457 StartInfo = new ProcessStartInfo
2458 {
2459 FileName = "keeper",
2460 Arguments = "whoami --format=json",
2461 RedirectStandardOutput = true,
2462 UseShellExecute = false,
2463 CreateNoWindow = true
2464 }
2465 };
2466
2467 process.Start();
2468 string output = process.StandardOutput.ReadToEnd();
2469 process.WaitForExit();
2470
2471 if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
2472 return true;
2473
2474 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2475 $"Keeper Commander is not logged in. Please run this once on the target machine:\n" +
2476 $"keeper shell\n" +
2477 $"Then login with your email / master password (or use 'keeper login').\n" +
2478 $"After login, you can use 'this-device' for persistent sessions.",
2479 null, GPALObjectType.None);
2480
2481 return false;
2482 }
2483 catch (Exception ex)
2484 {
2485 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
2486 $"Keeper Commander check failed. Is Keeper Commander (`keeper`) installed and in PATH?",
2487 null, GPALObjectType.None, ex);
2488 return false;
2489 }
2490 }
2491 }
2498 internal static class LocalAuthCallbackServer
2499 {
2500 private static readonly object _lock = new object();
2501 private static HttpListener _listener;
2502 private static string _pendingAuthCode;
2503 private static string _pendingAuthScope;
2504
2510 internal static bool StartIfNeeded(string baseUrl)
2511 {
2512 lock (_lock)
2513 {
2514 if (null != _listener)
2515 return true;
2516
2517 try
2518 {
2519 var listener = new HttpListener();
2520 listener.Prefixes.Add(baseUrl);
2521 listener.Start();
2522 _listener = listener;
2523
2524 new Thread(() => Listen(listener)) { IsBackground = true }.Start();
2525 return true;
2526 }
2527 catch (HttpListenerException)
2528 {
2529 // something else already owns this prefix. whether that something is GPALRestAPI is a
2530 // separate question, and BrowserHelper.RestApiAnswering is how it gets asked
2531 return false;
2532 }
2533 }
2534 }
2535
2536 private static void Listen(HttpListener listener)
2537 {
2538 try
2539 {
2540 while (listener.IsListening)
2541 {
2542 HttpListenerContext context = listener.GetContext();
2543 HandleRequest(context);
2544 }
2545 }
2546 catch (Exception)
2547 {
2548 // listener was stopped
2549 }
2550 }
2551
2552 private static void HandleRequest(HttpListenerContext context)
2553 {
2554 HttpListenerResponse response = context.Response;
2555 string path = context.Request.Url.AbsolutePath.Trim('/');
2556 string json;
2557
2558 switch (path)
2559 {
2560 case "access-token": // Called by the provider's redirect after the user grants consent
2561 NameValueCollection query = ParseQueryString(context.Request.Url.Query);
2562
2563 lock (_lock)
2564 {
2565 _pendingAuthCode = query["code"];
2566 _pendingAuthScope = query["scope"];
2567 }
2568
2569 SendResponse(response, "{\"message\":\"Authorization code received. You can now call /get-access-token.\"}");
2570 break;
2571
2572 case "get-access-token": // Polled by GetAuthCodeFromRest
2573 string code, scope;
2574
2575 lock (_lock)
2576 {
2577 code = _pendingAuthCode ?? string.Empty;
2578 scope = _pendingAuthScope ?? string.Empty;
2579
2580 // single use
2581 _pendingAuthCode = null;
2582 _pendingAuthScope = null;
2583 }
2584
2585 json = System.Text.Json.JsonSerializer.Serialize(new { code, scope });
2586 SendResponse(response, json);
2587 break;
2588
2589 default:
2590 SendResponse(response, "{}");
2591 break;
2592 }
2593 }
2594
2595 private static NameValueCollection ParseQueryString(string query)
2596 {
2597 var result = new NameValueCollection();
2598
2599 if (string.IsNullOrEmpty(query))
2600 return result;
2601
2602 foreach (string pair in query.TrimStart('?').Split('&'))
2603 {
2604 if (string.IsNullOrEmpty(pair))
2605 continue;
2606
2607 string[] parts = pair.Split(new[] { '=' }, 2);
2608 string key = Uri.UnescapeDataString(parts[0]);
2609 string value = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty;
2610 result[key] = value;
2611 }
2612
2613 return result;
2614 }
2615
2616 private static void SendResponse(HttpListenerResponse response, string message)
2617 {
2618 try
2619 {
2620 response.StatusCode = (int)HttpStatusCode.OK;
2621 response.ContentType = "application/json";
2622 response.ContentEncoding = Encoding.UTF8;
2623
2624 byte[] buffer = Encoding.UTF8.GetBytes(message ?? "");
2625 response.OutputStream.Write(buffer, 0, buffer.Length);
2626 }
2627 catch { /* client gone - ignore */ }
2628 finally
2629 {
2630 try { response.Close(); } catch { }
2631 }
2632 }
2633
2637 internal static void Stop()
2638 {
2639 lock (_lock)
2640 {
2641 if (null == _listener)
2642 return;
2643
2644 try { _listener.Stop(); _listener.Close(); } catch { }
2645 _listener = null;
2646 _pendingAuthCode = null;
2647 _pendingAuthScope = null;
2648 }
2649 }
2650 }
2651
2655 internal static class AuthHandler
2656 {
2657 // where a GPALRestAPI serves by default, and the first place looked before binding our own
2658 internal const string DefaultTokenUrl = "http://localhost:3000/";
2666 internal static string GetAuthCodeFromRest(Process browserProcess, int timeoutInSeconds)
2667 {
2668 string authCode = "";
2669
2670 try
2671 {
2672 // NOTE: get the access token from the rest API which is started by OttoMagic
2673 // GPALRestApi gets it because we pass a local url callback creds.Config.GoogleRestRedirectUri
2674 // and then we can access the token
2675 var restClient = GPAL.RESTClient
2676 .WithAPIBase(GPAL.OAuthTokenUrl)
2677 .WithEndpoint("get-access-token");
2678
2679 // Polling parameters
2680 TimeSpan timeout = TimeSpan.FromSeconds(timeoutInSeconds);
2681 TimeSpan interval = TimeSpan.FromSeconds(1);
2682 DateTime start = DateTime.UtcNow;
2683
2684 while ((DateTime.UtcNow - start) < timeout)
2685 {
2686 AuthCodePayload tokenInfo = restClient.Execute<AuthCodePayload>(); // returns AuthCodePayload directly
2687 if (!string.IsNullOrEmpty(tokenInfo?.Code))
2688 {
2689 authCode = tokenInfo.Code;
2690 GPAL.PublishSimpleEvent(
2691 GPALEventType.INFO,
2692 "Authorization code retrieved from REST server.",
2693 null,
2694 GPALObjectType.None
2695 );
2696 break; // stop polling
2697 }
2698
2699 Thread.Sleep(interval);
2700 }
2701
2702 if (string.IsNullOrEmpty(authCode))
2703 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Timeout waiting for authorization code from REST server.", null, GPALObjectType.None);
2704 }
2705 catch (Exception ex)
2706 {
2707 GPAL.PublishSimpleEvent(
2708 GPALEventType.ERROR,
2709 $"REST redirect auth failed",
2710 null,
2711 GPALObjectType.None
2712 , ex);
2713 }
2714 if (null != browserProcess)
2715 BrowserHelper.KillProcess(browserProcess); // only needed once to login
2716
2717 return authCode;
2718 }
2719
2741 internal static bool ResolveTokenUrl(Credentials creds)
2742 {
2743 bool retVal = false;
2744 string configured = creds?.Config?.GoogleRestRedirectUri;
2745
2746 GPAL.OAuthTokenUrl = true == string.IsNullOrWhiteSpace(configured)
2747 ? DefaultTokenUrl
2748 : $"{new Uri(configured).GetLeftPart(UriPartial.Authority)}/";
2749
2750 if (true == LocalAuthCallbackServer.StartIfNeeded(GPAL.OAuthTokenUrl))
2751 {
2752 retVal = true;
2753
2754 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Listening on [{GPAL.OAuthTokenUrl}] to catch the token.", creds, GPALObjectType.Other);
2755 }
2756 else if (true == Browser.BrowserHelper.RestApiAnswering(GPAL.OAuthTokenUrl))
2757 {
2758 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"The GPALRestAPI already on [{GPAL.OAuthTokenUrl}] will catch the token.", creds, GPALObjectType.Other);
2759 }
2760 else
2761 {
2762 // moving to another port would be a redirect the provider never agreed to, so there is no
2763 // recovery here worth making. one machine running two of these was always going to collide
2764 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);
2765 }
2766
2767 return retVal;
2768 }
2769
2770 // whether this program has a console window. Zero for a form, which is a program that cannot be asked
2771 // to type anything
2772 [System.Runtime.InteropServices.DllImport("kernel32.dll")]
2773 private static extern IntPtr GetConsoleWindow();
2774
2775 internal static string GetAuthCode(Credentials creds)
2776 {
2777 string authCode = "";
2778 int retry = 2;
2779 Process browserProcess = null;
2780 bool ownsLocalServer = false;
2781
2782 bool publishToConsole = GPAL.GPALSettings.PublishToConsole;
2783
2784 GPALEventType consoleEvents = GPAL.GPALSettings.ConsoleEvents;
2785
2786 if (false == publishToConsole)
2787 GPAL.WithPublishToConsole();
2788 else if (false == consoleEvents.HasFlag(GPALEventType.INFO))
2789 GPAL.ConsoleEvents |= GPALEventType.INFO;
2790
2791 // REST redirect flow
2792 // workflow : launch browser > user logs in (unknown length of time) > redirect to localhost > listener parses code
2793 // meanwhile, GPAL is polling get-access-token until some value is returned or 1 minute passes
2794 if (creds.Config.UseRestRedirect)
2795 {
2796 // GPALRESTAPI normally serves the redirect target (e.g. http://localhost:3000/access-token) and
2797 // get-access-token. If it isn't running, start a minimal listener on the same base URL ourselves
2798 // so the OAuth redirect still has somewhere to land.
2799 ownsLocalServer = ResolveTokenUrl(creds);
2800
2801 string authUrl = AuthHandler.GetAuthUrl(creds);
2802
2803 browserProcess = creds.Browser.MagicHelper.LaunchBrowser(creds.Browser, authUrl); // launch a new instance of the browser for authentication
2804 authCode = GetAuthCodeFromRest(browserProcess, 60);
2805 }
2806
2807 // Manual fallback flow: either UseRestRedirect is false, or the REST redirect timed out
2808 if (string.IsNullOrEmpty(authCode))
2809 {
2810 string authUrl = AuthHandler.GetAuthUrl(creds);
2811
2812 if (true == creds.Config.UseRestRedirect)
2813 GPAL.PublishSimpleEvent(
2814 GPALEventType.WARNING,
2815 "No authorization code arrived via the local redirect listener. Falling back to manual entry.",
2816 null,
2817 GPALObjectType.None
2818 );
2819
2820 // there has to be somewhere to type the code. A program with a console window can be asked
2821 // for it, and a form has nowhere to answer: reading a console that does not exist never
2822 // returns and never times out, so the whole program stops with no way back. A workflow with a
2823 // form collects the code itself and hands it over with WithAuthCode
2824 if (IntPtr.Zero == GetConsoleWindow())
2825 {
2826 GPAL.PublishSimpleEvent(
2827 GPALEventType.ERROR,
2828 $"An authorization code is needed and this program has no console to type it into. Open this URL, authorize, and hand the code back with WithAuthCode, or supply a refresh token with WithRefreshToken: [{authUrl}]");
2829 }
2830 else
2831 {
2832 while (retry > 0)
2833 {
2834 GPAL.PublishSimpleEvent(
2835 GPALEventType.INFO,
2836 $"Open this URL in a browser, authorize, and provide the code: \n[{authUrl}]",
2837 null,
2838 GPALObjectType.None
2839 );
2840
2841 Console.WriteLine("\nEnter auth code: "); // a prompt for the ReadLine below, not an event
2842 authCode = Console.ReadLine();
2843
2844 if (string.IsNullOrEmpty(authCode))
2845 {
2846 GPAL.PublishSimpleEvent(
2847 GPALEventType.ERROR,
2848 "No authorization code provided.",
2849 null,
2850 GPALObjectType.None
2851 );
2852 retry--;
2853 }
2854 else
2855 {
2856 break;
2857 }
2858 }
2859 }
2860 }
2861
2862 if (ownsLocalServer)
2863 LocalAuthCallbackServer.Stop();
2864
2865 if (false == publishToConsole)
2866 GPAL.WithPublishToConsole(GPALEventType.NONE);
2867 else if (false == consoleEvents.HasFlag(GPALEventType.INFO))
2868 GPAL.ConsoleEvents ^= GPALEventType.INFO;
2869
2870 return authCode;
2871 }
2872
2878 internal static string GetAuthUrl(Credentials creds)
2879 {
2880 switch (creds.ServiceType)
2881 {
2882 case CredentialServiceType.Google:
2883 return $"{creds.Config.GoogleConsentBase}/o/oauth2/v2/auth" +
2884 $"?client_id={Uri.EscapeDataString(creds.ClientId)}" +
2885 $"&redirect_uri={Uri.EscapeDataString(creds.Config.UseRestRedirect ? creds.Config.GoogleRestRedirectUri : creds.Config.GoogleRedirectUri)}" +
2886 $"&response_type=code" +
2887 $"&scope={Uri.EscapeDataString(Credentials.GetScopeValue(creds.OAuthScope))}" +
2888 $"&access_type=offline";
2889
2890 case CredentialServiceType.Azure:
2891 return $"{creds.Config.AzureAuthBase}/oauth2/v2.0/authorize" +
2892 $"?client_id={Uri.EscapeDataString(creds.ClientId)}" +
2893 $"&response_type=code" +
2894 $"&redirect_uri={Uri.EscapeDataString(creds.Config.AzureRedirectUri)}" +
2895 $"&response_mode=query" +
2896 $"&scope={Uri.EscapeDataString(Credentials.GetScopeValue(creds.OAuthScope))}";
2897
2898 case CredentialServiceType.AWS:
2899 return $"{creds.Config.AWSAuthBase}/oauth2/authorize" +
2900 $"?client_id={Uri.EscapeDataString(creds.ClientId)}" +
2901 $"&response_type=code" +
2902 $"&redirect_uri={Uri.EscapeDataString(creds.Config.AWSRedirectUri)}" +
2903 $"&scope={Uri.EscapeDataString(Credentials.GetScopeValue(creds.OAuthScope))}";
2904
2905 case CredentialServiceType.Bitwarden:
2906 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Bitwarden OAuth not implemented.", creds, GPALObjectType.Other);
2907 break;
2908 }
2909
2910 return "";
2911 }
2912 }
2913}
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:7547
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.
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...
IAllowOAuthClient WithWebAuth(WebAuthType webAuthType)
How the site expects to be told who you are. A browser handed this credential acts on it accordingly,...
IAllowOAuthClient WithPassword(string password)
Sets the password, either as a credential to retrieve directly (with CredentialServiceType....
CredentialsConfig Config
Endpoint/protocol configuration for the credential service handlers, loaded from ....
IAllowCredentialSettings WithServiceKey(string key)
Service key is a custom string consisting of your ClientID:ClientSecret.
OAuthScope OAuthScope
OAuth scope requested via .WithScope when fetching an access token.
IAllowCredentialSettings WithDomain(string domain)
Restricts GetCredentialsFor/SaveTo results to vault items whose URL contains this domain....
string Username
Username supplied via .WithUsername, or the username field returned from a vault item.
IAllowKeyFromApiEndpoint WithKeyFromApi(IRESTClient restClient)
Like WithServiceKey, but fetches the key via a GET request made with the supplied,...
IAllowOAuthClient WithAuthRedirectUrl(string redirectUrl)
Where the OAuth redirect lands, and therefore what has to catch the token. This is the one setting th...
string Domain
Optional URL filter supplied via .WithDomain, used to narrow vault matches down to a specific site.
IAllowCredentialSettings WithMasterCredentials(ICredentials masterCredentials)
Supplies the login for the credential service itself, your Bitwarden, LastPass or 1Password account,...
string ClientId
OAuth client ID supplied via .WithClientId or derived from .WithServiceKey.
IAllowClientSecret WithClientId(string clientId)
Sets the OAuth client ID used to authenticate to ServiceType.
IAllowCredentialSettings WithServiceAccountKey(GoogleServiceAccountKey serviceAccountKey)
Supplies a Google Cloud service account key as a deserialized object, used to fetch an access token w...
string AuthCode
OAuth authorization code supplied via .WithAuthCode, or retrieved automatically during the OAuth redi...
IAllowCredentialSettings WithAuthCode(string authCode)
Supplies a previously-obtained OAuth authorization code, skipping the interactive authorization step.
IAllowCredentialMethod WithService(CredentialServiceType serviceType)
Selects which password manager or cloud identity service this credentials request targets.
IAllowCredentialSettings WithScope(OAuthScope scope)
Sets the OAuth scope to request when an access token is fetched.
IAllowCredentialSettings WithEndpoint(string endpoint)
Completes .WithKeyFromApi(restClient) by calling the given endpoint on that RESTClient and using the ...
IAllowCredentialSettings WithKeyFromEnv(string envVariableName)
Like WithServiceKey, but reads the key from an environment variable (checking process,...
IAllowCredentialSettings GetCredentialsFor(string target)
Sets the search term used to find a matching item in the target password manager (e....
string AccessToken
Most recently fetched access token for ServiceType, set by FetchAccessToken/GetCredentialsFor.
string ClientSecret
OAuth client secret supplied via .WithClientSecret or derived from .WithServiceKey.
IAllowCredentialSettings WithServiceAccountKey(GPALFile serviceAccountKeyFile)
Loads a Google Cloud service account key from a JSON key file, used to fetch an access token without ...
IAllowCredentialSettings WithServiceAccountKey(string serviceAccountKey)
Supplies a Google Cloud service account key as a raw JSON string, used to fetch an access token witho...
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 ...
IAllowOAuthClient WithAccessToken(string accessToken)
A token the workflow already holds, for the schemes that present one: Bearer, X-Auth-Token,...
void FetchAccessToken(out string accessToken)
Fetches a new access token for ServiceType using the credentials/configuration supplied so far,...
IAllowCredentialSettings WithRefreshToken(string refreshToken)
Supplies a previously-obtained OAuth refresh token, used to fetch a new access token without re-autho...
IAllowCredentialSettings WithClientSecret(string clientSecret)
Sets the OAuth client secret and derives ServiceKey as "ClientId:ClientSecret".
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:468
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:571
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:2568
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.
The ways a credential can identify itself, which is everything WithService leads to.