Skip to main content

C#

Uses Microsoft.IdentityModel.JsonWebTokens. Targets .NET 8 or later.

dotnet add package Microsoft.IdentityModel.JsonWebTokens

Token client​

RynoTokenClient.cs
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

/// <summary>Requests and caches Ryno partner access tokens using a private_key_jwt client assertion.</summary>
public sealed class RynoTokenClient(HttpClient http, string clientId, string tokenUrl, RSA privateKey)
{
private readonly SemaphoreSlim _lock = new(1, 1);
private string? _token;
private DateTimeOffset _expiresAt;

/// <summary>Signs a single-use client assertion, valid for 60 seconds.</summary>
public string BuildAssertion()
{
var now = DateTime.UtcNow;
return new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Issuer = clientId,
Audience = tokenUrl,
IssuedAt = now,
NotBefore = now,
Expires = now.AddSeconds(60),
Claims = new Dictionary<string, object>
{
["sub"] = clientId,
["jti"] = Guid.NewGuid().ToString(),
},
SigningCredentials = new SigningCredentials(new RsaSecurityKey(privateKey), SecurityAlgorithms.RsaSha256),
});
}

/// <summary>Returns a cached access token, requesting a new one 30 seconds before expiry.</summary>
public async Task<string> GetAccessTokenAsync(string scope = "partner.read partner.write", CancellationToken ct = default)
{
await _lock.WaitAsync(ct);
try
{
if (_token is not null && DateTimeOffset.UtcNow < _expiresAt.AddSeconds(-30))
{
return _token;
}

using var response = await http.PostAsync(tokenUrl, new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = clientId,
["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
["client_assertion"] = BuildAssertion(),
["scope"] = scope,
}), ct);

if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Token request failed ({(int)response.StatusCode}): {await response.Content.ReadAsStringAsync(ct)}");
}

var body = (await response.Content.ReadFromJsonAsync<TokenResponse>(ct))!;
_token = body.AccessToken;
_expiresAt = DateTimeOffset.UtcNow.AddSeconds(body.ExpiresIn);
return _token;
}
finally
{
_lock.Release();
}
}

private sealed record TokenResponse(
[property: JsonPropertyName("access_token")] string AccessToken,
[property: JsonPropertyName("expires_in")] int ExpiresIn);
}

Call the API​

Program.cs
using System.Net.Http.Headers;
using System.Security.Cryptography;

var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(Environment.GetEnvironmentVariable("RYNO_PRIVATE_KEY_PATH")!));

using var http = new HttpClient();
var tokens = new RynoTokenClient(
http,
Environment.GetEnvironmentVariable("RYNO_CLIENT_ID")!,
Environment.GetEnvironmentVariable("RYNO_TOKEN_URL")!,
rsa);

using var request = new HttpRequestMessage(HttpMethod.Get,
"https://gateway.sandbox.rynopay.io/api/partner/v1/business-profiles?page=1&pageSize=20");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await tokens.GetAccessTokenAsync());

using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");