Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(generic): check local token expiry #1837

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/shared/Core/Authentication/OAuth/Json/WebToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace GitCredentialManager.Authentication.Oauth.Json
{
public class WebToken(WebToken.TokenHeader header, WebToken.TokenPayload payload, string signature)
{
public class TokenHeader
{
[JsonRequired]
[JsonInclude]
[JsonPropertyName("typ")]
public string Type { get; private set; }
}
public class TokenPayload
{
[JsonRequired]
[JsonInclude]
[JsonPropertyName("exp")]
public long Expiry { get; private set; }
}
public TokenHeader Header { get; } = header;
public TokenPayload Payload { get; } = payload;
public string Signature { get; } = signature;

static public WebToken TryCreate(string value)
{
try
{
var parts = value.Split('.');
if (parts.Length != 3)
{
return null;
}
var header = JsonSerializer.Deserialize<TokenHeader>(Base64UrlConvert.Decode(parts[0]));
if (!"JWT".Equals(header.Type))
{
return null;
}
var payload = JsonSerializer.Deserialize<TokenPayload>(Base64UrlConvert.Decode(parts[1]));
return new WebToken(header, payload, parts[2]);
}
catch
{
return null;
}
}

static public bool IsExpiredToken(string value)
{
var token = TryCreate(value);
return token != null && token.Payload.Expiry < DateTimeOffset.Now.ToUnixTimeSeconds();

}
}
}
39 changes: 30 additions & 9 deletions src/shared/Core/Base64UrlConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
16 changes: 15 additions & 1 deletion src/shared/Core/GenericHostProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using GitCredentialManager.Authentication;
using GitCredentialManager.Authentication.Oauth.Json;
using GitCredentialManager.Authentication.OAuth;

namespace GitCredentialManager
Expand Down Expand Up @@ -125,6 +126,19 @@ public override async Task<ICredential> GenerateCredentialAsync(InputArguments i
return await _basicAuth.GetCredentialsAsync(uri.AbsoluteUri, input.UserName);
}

public override async Task<ICredential> GetCredentialAsync(InputArguments input)
{
var credential = await base.GetCredentialAsync(input);
if (WebToken.IsExpiredToken(credential.Password))
{
// No existing credential was found, create a new one
Context.Trace.WriteLine("Refreshing expired JWT credential...");
credential = await GenerateCredentialAsync(input);
Context.Trace.WriteLine("Credential created.");
}
return credential;
}

private async Task<ICredential> GetOAuthAccessToken(Uri remoteUri, string userName, GenericOAuthConfig config, ITrace2 trace2)
{
// TODO: Determined user info from a webcall? ID token? Need OIDC support
Expand Down Expand Up @@ -152,7 +166,7 @@ private async Task<ICredential> GetOAuthAccessToken(Uri remoteUri, string userNa

// Try to use a refresh token if we have one
ICredential refreshToken = Context.CredentialStore.Get(refreshService, userName);
if (refreshToken != null)
if (refreshToken != null && !WebToken.IsExpiredToken(refreshToken.Password))
{
try
{
Expand Down