diff --git a/src/Core/Authentication/JsonWebToken.cs b/src/Core/Authentication/JsonWebToken.cs new file mode 100644 index 000000000..21b9fca28 --- /dev/null +++ b/src/Core/Authentication/JsonWebToken.cs @@ -0,0 +1,68 @@ +using System; +using System.Buffers.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitCredentialManager.Authentication +{ + public partial class JsonWebToken + { + public static readonly string Type = "JWT"; + + public long? Expiry { get; } + public string Value { get; } + + class Header + { + [JsonRequired] + [JsonInclude] + [JsonPropertyName("typ")] + public string Type { get; internal set; } + } + + class Payload + { + [JsonInclude] + [JsonPropertyName("exp")] + public long? Expiry { get; internal set; } + } + + JsonWebToken(long? expiry, string value) + { + Expiry = expiry; + Value = value; + } + + + [JsonSerializable(typeof(Header))] + private partial class HeaderDto : JsonSerializerContext { } + + [JsonSerializable(typeof(Payload))] + private partial class PayloadDto : JsonSerializerContext { } + + + public static bool TryCreate(string value, out JsonWebToken token) + { + try + { + // elements of JWT structure "
.." + var parts = value.Split('.'); + if (parts.Length == 2 || parts.Length == 3) + { + var header = JsonSerializer.Deserialize(Base64Url.DecodeFromChars(parts[0]), HeaderDto.Default.Header); + if (Type.Equals(header.Type, StringComparison.OrdinalIgnoreCase)) + { + var payload = JsonSerializer.Deserialize(Base64Url.DecodeFromChars(parts[1]), PayloadDto.Default.Payload); + token = new JsonWebToken(payload.Expiry, value); + return true; + } + } + } + catch { } + + // invalid token data on content mismatch or deserializer exception + token = null; + return false; + } + } +} diff --git a/src/Core/Authentication/Token.cs b/src/Core/Authentication/Token.cs new file mode 100644 index 000000000..5ab4340b0 --- /dev/null +++ b/src/Core/Authentication/Token.cs @@ -0,0 +1,32 @@ +namespace GitCredentialManager.Authentication +{ + public interface IToken + { + string Type { get; } + string Value { get; } + long? Expiry { get; } + } + + public static class Token + { + protected class Jwt(string value, long? expiry) : IToken + { + public string Type => JsonWebToken.Type; + public string Value => value; + public long? Expiry => expiry; + + } + + public static bool TryCreate(string value, out IToken token) + { + if (JsonWebToken.TryCreate(value, out JsonWebToken jwt)) + { + token = new Jwt(value, jwt.Expiry); + return true; + } + + token = null; + return false; + } + } +} diff --git a/src/Core/GenericHostProvider.cs b/src/Core/GenericHostProvider.cs index 5fb16027b..e3aff6cab 100644 --- a/src/Core/GenericHostProvider.cs +++ b/src/Core/GenericHostProvider.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using GitCredentialManager.Authentication; using GitCredentialManager.Authentication.OAuth; -using GitCredentialManager.Tty; namespace GitCredentialManager { @@ -75,17 +74,25 @@ public async Task GetCredentialAsync(GitRequest request) 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(request); + } + else if (Token.TryCreate(credential.Password, out var token)) + { + _context.Trace.WriteLine($"Existing token found (type={token.Type})."); + // comparing null and long will always be false + if (!(token.Expiry < DateTimeOffset.Now.ToUnixTimeSeconds())) { + return GitResponse.Ok(new GitCredential(credential.Account, token.Value), isEphemeral: token.Expiry != null, authtype: null); + } + _context.Trace.WriteLine("Credential token is expired."); } else { _context.Trace.WriteLine("Existing credential found."); + return GitResponse.Ok(credential); } - return new GitResponse(credential); + // No valid credential was found, create a new one + _context.Trace.WriteLine("Creating new credential..."); + return await GenerateCredentialAsync(request); } public Task StoreCredentialAsync(GitRequest request) @@ -159,9 +166,13 @@ public async Task GenerateCredentialAsync(GitRequest request) _context.Trace.WriteLine($"\tUseAuthHeader = {oauthConfig.UseAuthHeader}"); _context.Trace.WriteLine($"\tDefaultUserName = {oauthConfig.DefaultUserName}"); - return new GitResponse( - await GetOAuthAccessToken(uri, request.UserName, oauthConfig) - ); + var credential = await GetOAuthAccessToken(uri, request.UserName, oauthConfig); + if (Token.TryCreate(credential.Password, out IToken token)) + { + return GitResponse.Ok(new GitCredential(credential.Account, token.Value), isEphemeral: token.Expiry != null, authtype: null); + } + + return new GitResponse(credential); } // Try detecting WIA for this remote, if permitted and possible (http(s) required for probing WIA)