diff --git a/src/shared/Core/Authentication/StructuredToken.cs b/src/shared/Core/Authentication/StructuredToken.cs new file mode 100644 index 000000000..2d5016471 --- /dev/null +++ b/src/shared/Core/Authentication/StructuredToken.cs @@ -0,0 +1,57 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitCredentialManager.Authentication +{ + public abstract class StructuredToken + { + private class JwtHeader + { + [JsonRequired] + [JsonInclude] + [JsonPropertyName("typ")] + public string Type { get; private set; } + } + private class JwtPayload : StructuredToken + { + [JsonRequired] + [JsonInclude] + [JsonPropertyName("exp")] + public long Expiry { get; private set; } + + public override bool IsExpired + { + get + { + return Expiry < DateTimeOffset.Now.ToUnixTimeSeconds(); + } + } + } + + public abstract bool IsExpired { get; } + + public static bool TryCreate(string value, out StructuredToken token) + { + try + { + // elements of JWT structure "
.." + var parts = value.Split('.'); + if (parts.Length == 3) + { + var header = JsonSerializer.Deserialize(Base64UrlConvert.Decode(parts[0])); + if ("JWT".Equals(header.Type, StringComparison.OrdinalIgnoreCase)) + { + token = JsonSerializer.Deserialize(Base64UrlConvert.Decode(parts[1])); + return true; + } + } + } + catch { } + + // invalid token data on content mismatch or deserializer exception + token = null; + return false; + } + } +} diff --git a/src/shared/Core/Base64UrlConvert.cs b/src/shared/Core/Base64UrlConvert.cs index 7b2fce035..3a0f299f8 100644 --- a/src/shared/Core/Base64UrlConvert.cs +++ b/src/shared/Core/Base64UrlConvert.cs @@ -4,22 +4,43 @@ namespace GitCredentialManager { public static class Base64UrlConvert { + + // The base64url format is the same as regular base64 format except: + // 1. character 62 is "-" (minus) not "+" (plus) + // 2. character 63 is "_" (underscore) not "/" (slash) + // 3. padding is optional + private const char base64PadCharacter = '='; + private const char base64Character62 = '+'; + private const char base64Character63 = '/'; + private const char base64UrlCharacter62 = '-'; + private const char base64UrlCharacter63 = '_'; + public static string Encode(byte[] data, bool includePadding = true) { - const char base64PadCharacter = '='; - const char base64Character62 = '+'; - const char base64Character63 = '/'; - const char base64UrlCharacter62 = '-'; - const char base64UrlCharacter63 = '_'; - - // The base64url format is the same as regular base64 format except: - // 1. character 62 is "-" (minus) not "+" (plus) - // 2. character 63 is "_" (underscore) not "/" (slash) string base64Url = Convert.ToBase64String(data) .Replace(base64Character62, base64UrlCharacter62) .Replace(base64Character63, base64UrlCharacter63); return includePadding ? base64Url : base64Url.TrimEnd(base64PadCharacter); } + + public static byte[] Decode(string data) + { + string base64 = data + .Replace(base64UrlCharacter62, base64Character62) + .Replace(base64UrlCharacter63, base64Character63); + + switch (base64.Length % 4) + { + case 2: + base64 += base64PadCharacter; + goto case 3; + case 3: + base64 += base64PadCharacter; + break; + } + + return Convert.FromBase64String(base64); + } } } diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index a66729a8a..7f19963f2 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -74,17 +74,20 @@ public async Task GetCredentialAsync(InputArguments input) if (credential == null) { _context.Trace.WriteLine("No existing credentials found."); - - // No existing credential was found, create a new one - _context.Trace.WriteLine("Creating new credential..."); - return await GenerateCredentialAsync(input); + } + else if (StructuredToken.TryCreate(credential.Password, out var token) && token.IsExpired) + { + _context.Trace.WriteLine("Credential is expired token."); } else { _context.Trace.WriteLine("Existing credential found."); + return new GetCredentialResult(credential); } - return new GetCredentialResult(credential); + // No valid credential was found, create a new one + _context.Trace.WriteLine("Creating new credential..."); + return await GenerateCredentialAsync(input); } public Task StoreCredentialAsync(InputArguments input)