Skip to content

Commit 32dae46

Browse files
committed
fix(generic): check expiry of structured tokens
add expiry check for `auth_token` and `refresh token` values add generic StructuredToken class with expiration status property include minimal JWT reader to decode and extract data
1 parent c111e16 commit 32dae46

File tree

2 files changed

+67
-7
lines changed

2 files changed

+67
-7
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace GitCredentialManager.Authentication
6+
{
7+
public abstract class StructuredToken
8+
{
9+
private class JwtHeader
10+
{
11+
[JsonRequired]
12+
[JsonInclude]
13+
[JsonPropertyName("typ")]
14+
public string Type { get; private set; }
15+
}
16+
private class JwtPayload : StructuredToken
17+
{
18+
[JsonRequired]
19+
[JsonInclude]
20+
[JsonPropertyName("exp")]
21+
public long Expiry { get; private set; }
22+
23+
public override bool IsExpired
24+
{
25+
get
26+
{
27+
return Expiry < DateTimeOffset.Now.ToUnixTimeSeconds();
28+
}
29+
}
30+
}
31+
32+
public abstract bool IsExpired { get; }
33+
34+
public static bool TryCreate(string value, out StructuredToken token)
35+
{
36+
try
37+
{
38+
// elements of JWT structure "<header>.<payload>.<signature>"
39+
var parts = value.Split('.');
40+
if (parts.Length == 3)
41+
{
42+
var header = JsonSerializer.Deserialize<JwtHeader>(Base64UrlConvert.Decode(parts[0]));
43+
if ("JWT".Equals(header.Type, StringComparison.OrdinalIgnoreCase))
44+
{
45+
token = JsonSerializer.Deserialize<JwtPayload>(Base64UrlConvert.Decode(parts[1]));
46+
return true;
47+
}
48+
}
49+
}
50+
catch { }
51+
52+
// invalid token data on content mismatch or deserializer exception
53+
token = null;
54+
return false;
55+
}
56+
}
57+
}

src/shared/Core/GenericHostProvider.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,20 @@ public async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)
7474
if (credential == null)
7575
{
7676
_context.Trace.WriteLine("No existing credentials found.");
77-
78-
// No existing credential was found, create a new one
79-
_context.Trace.WriteLine("Creating new credential...");
80-
return await GenerateCredentialAsync(input);
77+
}
78+
else if (StructuredToken.TryCreate(credential.Password, out var token) && token.IsExpired)
79+
{
80+
_context.Trace.WriteLine("Credential is expired token.");
8181
}
8282
else
8383
{
8484
_context.Trace.WriteLine("Existing credential found.");
85+
return new GetCredentialResult(credential);
8586
}
8687

87-
return new GetCredentialResult(credential);
88+
// No valid credential was found, create a new one
89+
_context.Trace.WriteLine("Creating new credential...");
90+
return await GenerateCredentialAsync(input);
8891
}
8992

9093
public Task StoreCredentialAsync(InputArguments input)
@@ -288,9 +291,9 @@ private async Task<ICredential> GetOAuthAccessToken(Uri remoteUri, string userNa
288291
string refreshService = new UriBuilder(remoteUri) { Host = $"refresh_token.{remoteUri.Host}" }
289292
.Uri.AbsoluteUri.TrimEnd('/');
290293

291-
// Try to use a refresh token if we have one
294+
// Try to use a (valid) refresh token if we have one
292295
ICredential refreshToken = _context.CredentialStore.Get(refreshService, userName);
293-
if (refreshToken != null)
296+
if (refreshToken != null && !(StructuredToken.TryCreate(refreshToken.Password, out var token) && token.IsExpired))
294297
{
295298
try
296299
{

0 commit comments

Comments
 (0)