using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using System.Xml.Serialization;
namespace OpenTap.Authentication
{
///
/// Represents a set of Oauth2/OpenID Connect tokens (access and possibly refresh token) that grants access to a given domain.
///
public class TokenInfo
{
///
/// An enumeration of known token kinds
///
private enum TokenKind
{
Jwt,
UserToken
}
///
/// The kind of token this instance contains
///
private TokenKind Kind => DetermineTokenKind();
private TokenKind DetermineTokenKind()
{
// Repository user tokens are guaranteed to never contain a period.
return AccessToken.Contains(".") ? TokenKind.Jwt : TokenKind.UserToken;
}
static DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
///
/// Raw access token string. This value can be used as a Bearer token. The HttpClient
/// returned from will automatically do
/// this for requests that go to domains that match .
///
public string AccessToken { get; set; }
///
/// Raw refresh token string. May be null if no refresh token is available.
///
public string RefreshToken { get; set; }
private string domain;
///
/// The hostname or IP address this token is intended for. Used by the HttpClient
/// returned from to determine which TokenInfo
/// in the list to use for a given request.
///
public string Domain
{
get => domain;
set
{
if (Uri.IsWellFormedUriString(value, UriKind.Absolute))
throw new ArgumentException("Domain should only be the host part of a URI and not a full absolute URI.");
domain = value;
}
}
private Dictionary _Claims;
///
/// Claims contained in the .
///
public IReadOnlyDictionary Claims
{
get
{
if (_Claims == null)
_Claims = GetClaims();
return _Claims;
}
}
private Dictionary GetClaims()
{
if (Kind == TokenKind.Jwt)
return GetPayload().RootElement.EnumerateObject().ToDictionary(c => c.Name, c => c.Value.ToString());
return new Dictionary();
}
/// Expiration date of the .
[XmlIgnore]
[Obsolete("Expiration time is not supported. Future token types may not contain expiration information. " +
"Consider manually decoding the token if you know the specific format.")]
public DateTime Expiration =>
Kind == TokenKind.Jwt ? unixEpoch.AddSeconds(long.Parse(Claims["exp"])) : DateTime.MaxValue;
JsonDocument payload;
JsonDocument GetPayload()
{
if (payload != null) return payload;
var payloadData = AccessToken.Split('.')[1];
payload = JsonDocument.Parse(Base64UrlDecode(payloadData));
return payload;
}
byte[] Base64UrlDecode(string encoded)
{
string substituded = encoded;
substituded = substituded.Replace('-', '+');
substituded = substituded.Replace('_', '/');
while (substituded.Length % 4 != 0)
{
substituded += '=';
}
return Convert.FromBase64String(substituded);
}
///
/// Constructor used by serializer, please use constructor with arguments from user code.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public TokenInfo()
{
}
///
/// Default constructor from user code
///
/// The raw token string for the access token
/// Access, Refresh or ID type
/// Domain name for which this token is valid
public TokenInfo(string access_token, string refresh_token, string domain)
{
AccessToken = access_token;
RefreshToken = refresh_token;
Domain = domain;
}
/// Creates a TokenInfo object based on the given OAuth response (json format).
public static TokenInfo FromResponse(string response, string domain)
{
var ti = new TokenInfo();
ti.Domain = domain;
var json = JsonDocument.Parse(response);
if (json.RootElement.TryGetProperty("access_token", out var accessTokenData))
ti.AccessToken = accessTokenData.GetString();
if (json.RootElement.TryGetProperty("refresh_token", out var refreshTokenData))
ti.RefreshToken = refreshTokenData.GetString();
return ti;
}
}
}