Skip to content

Commit

Permalink
Service client for Yandex Identity and Access Management (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
gkurbesov authored Nov 17, 2024
1 parent ad0d042 commit 717ee9a
Show file tree
Hide file tree
Showing 16 changed files with 414 additions and 6 deletions.
16 changes: 16 additions & 0 deletions YaCloudKit.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaCloudKit.MQ.Transport.Exa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaCloudKit.TTS.Example", "samples\YaCloudKit.TTS.Example\YaCloudKit.TTS.Example.csproj", "{8796825F-8B82-4EDA-B21C-9D552168407B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IdentityAccessManagement", "src\IdentityAccessManagement", "{A584AADB-F033-49CE-8740-D75C8B21D387}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaCloudKit.IAM", "src\IdentityAccessManagement\YaCloudKit.IAM\YaCloudKit.IAM.csproj", "{6AF165C7-DAA3-4000-9352-994954A9C77D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YaCloudKit.IAM.Examples", "samples\YaCloudKit.IAM.Examples\YaCloudKit.IAM.Examples.csproj", "{26E7F91F-B3A5-4EE7-A615-BB524ABFDA74}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -69,6 +75,14 @@ Global
{8796825F-8B82-4EDA-B21C-9D552168407B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8796825F-8B82-4EDA-B21C-9D552168407B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8796825F-8B82-4EDA-B21C-9D552168407B}.Release|Any CPU.Build.0 = Release|Any CPU
{6AF165C7-DAA3-4000-9352-994954A9C77D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AF165C7-DAA3-4000-9352-994954A9C77D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AF165C7-DAA3-4000-9352-994954A9C77D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AF165C7-DAA3-4000-9352-994954A9C77D}.Release|Any CPU.Build.0 = Release|Any CPU
{26E7F91F-B3A5-4EE7-A615-BB524ABFDA74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26E7F91F-B3A5-4EE7-A615-BB524ABFDA74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26E7F91F-B3A5-4EE7-A615-BB524ABFDA74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26E7F91F-B3A5-4EE7-A615-BB524ABFDA74}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -83,6 +97,8 @@ Global
{0BFD6AEA-01EB-47F3-A9B9-DB5F222877BB} = {3B55C34B-BA59-4ABB-87C5-59153FF74CB5}
{EDBDB339-B140-4832-A54F-004D244808EA} = {3B55C34B-BA59-4ABB-87C5-59153FF74CB5}
{8796825F-8B82-4EDA-B21C-9D552168407B} = {3B55C34B-BA59-4ABB-87C5-59153FF74CB5}
{6AF165C7-DAA3-4000-9352-994954A9C77D} = {A584AADB-F033-49CE-8740-D75C8B21D387}
{26E7F91F-B3A5-4EE7-A615-BB524ABFDA74} = {3B55C34B-BA59-4ABB-87C5-59153FF74CB5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EB60A6CA-F76D-40F9-BFDD-ACA1F097B83D}
Expand Down
26 changes: 26 additions & 0 deletions samples/YaCloudKit.IAM.Examples/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using YaCloudKit.IAM;

var configurationSections = new Dictionary<string, string>
{
{ "YandexIam:ServiceAccountId", "your-service-account-id" },
{ "YandexIam:PublicKeyId", "your-public-key-id" }
};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configurationSections)
.Build();

var services = new ServiceCollection();
services
.AddYandexFilePrivateKeyProvider("your-private-key-file-path")
.AddDefaultYandexIamServiceClient(configuration);

var serviceProvider = services.BuildServiceProvider();

var yandexIamServiceClient = serviceProvider.GetRequiredService<IYandexIamServiceClient>();

var iamTokenResponse = await yandexIamServiceClient.GetIamForServiceAccountAsync();

Console.WriteLine("IAM token: " + iamTokenResponse.IamToken.Substring(0, 20) + "...");
Console.WriteLine("Expires at: " + iamTokenResponse.ExpiresAt);
15 changes: 15 additions & 0 deletions samples/YaCloudKit.IAM.Examples/YaCloudKit.IAM.Examples.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\IdentityAccessManagement\YaCloudKit.IAM\YaCloudKit.IAM.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using YaCloudKit.IAM.Model;

namespace YaCloudKit.IAM;

public interface IYandexIamServiceClient
{
Task<IamTokenResponse> GetIamForYandexAccountAsync(string yandexPassportOauthToken, CancellationToken cancellationToken = default);

Task<IamTokenResponse> GetIamForServiceAccountAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using YaCloudKit.IAM.Rsa;

namespace YaCloudKit.IAM.Jwt;

public class YandexJsonWebTokenGenerator(IOptionsMonitor<YandexIamOptions> options, IYandexRsaFactory rsaFactory)
{
public async Task<string> GenerateJwtAsync(CancellationToken cancellationToken = default)
{
var optionsValue = options.CurrentValue;
ArgumentNullException.ThrowIfNull(optionsValue.ServiceAccountId);
ArgumentNullException.ThrowIfNull(optionsValue.PublicKeyId);

using var rsa = await rsaFactory.CreateRsaAsync(cancellationToken);
var header = new JwtHeader(new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSsaPssSha256))
{
{ "kid", optionsValue.PublicKeyId }
};

var now = DateTimeOffset.UtcNow;
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Aud, "https://iam.api.cloud.yandex.net/iam/v1/tokens"),
new(JwtRegisteredClaimNames.Iss, optionsValue.ServiceAccountId),
new("iat", now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer),
new("exp", now.AddSeconds(3600).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer)
};
var payload = new JwtPayload(claims);
var jwtSecurityToken = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
return handler.WriteToken(jwtSecurityToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace YaCloudKit.IAM.Model;

public record IamTokenResponse
{
[JsonPropertyName("iamToken")]
public required string IamToken { get; init; }

[JsonPropertyName("expiresAt")]
public required DateTime ExpiresAt { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace YaCloudKit.IAM.Rsa;

public interface IYandexPrivateKeyProvider
{
Task<char[]> GetPrivateKeyAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace YaCloudKit.IAM.Rsa;

public abstract class YandexCachedPrivateKeyProvider(bool cache = false) : IYandexPrivateKeyProvider, IDisposable
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private char[]? _privateKey;

protected abstract Task<char[]> GetPrivateKeyCoreAsync(CancellationToken cancellationToken);


public async Task<char[]> GetPrivateKeyAsync(CancellationToken cancellation)
{
if (_privateKey is not null)
{
return _privateKey;
}

await _semaphore.WaitAsync(cancellation);
try
{
if (_privateKey is not null)
{
return _privateKey;
}

var localPrivateKey = await GetPrivateKeyCoreAsync(cancellation);
if (cache)
{
_privateKey = localPrivateKey;
}

return _privateKey ?? localPrivateKey;
}
finally
{
_semaphore.Release();
}
}

public void Dispose()
{
_semaphore.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace YaCloudKit.IAM.Rsa;

public class YandexFilePrivateKeyProvider(string privateKeyFilePath, bool cacheResult = false)
: YandexCachedPrivateKeyProvider(cacheResult)
{
protected override async Task<char[]> GetPrivateKeyCoreAsync(CancellationToken cancellationToken)
{
using var reader = new StreamReader(privateKeyFilePath);
return (await reader.ReadToEndAsync(cancellationToken)).ToCharArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace YaCloudKit.IAM.Rsa;

public class YandexFuncPrivateKeyProvider(
Func<CancellationToken, Task<char[]>> privateKeyFunc,
bool cacheResult = false)
: YandexCachedPrivateKeyProvider(cacheResult)
{
protected override Task<char[]> GetPrivateKeyCoreAsync(CancellationToken cancellationToken)
{
return privateKeyFunc(cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Security.Cryptography;

namespace YaCloudKit.IAM.Rsa;

public interface IYandexRsaFactory
{
Task<RSA> CreateRsaAsync(CancellationToken cancellationToken = default);
}

public class YandexRsaFactory(IYandexPrivateKeyProvider privateKeyProvider) : IYandexRsaFactory
{
public async Task<RSA> CreateRsaAsync(CancellationToken cancellationToken = default)
{
var rsa = RSA.Create();
var privateKey = await privateKeyProvider.GetPrivateKeyAsync(cancellationToken);
rsa.ImportFromPem(privateKey);
return rsa;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace YaCloudKit.IAM.Rsa;

public class YandexStaticPrivateKeyProvider : IYandexPrivateKeyProvider
{
private readonly char[] _privateKey;

public YandexStaticPrivateKeyProvider(string privateKey)
{
ArgumentNullException.ThrowIfNull(privateKey);
_privateKey = privateKey.ToCharArray();
}

public Task<char[]> GetPrivateKeyAsync(CancellationToken _) => Task.FromResult(_privateKey);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using YaCloudKit.IAM.Jwt;
using YaCloudKit.IAM.Rsa;

namespace YaCloudKit.IAM;

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddYandexFilePrivateKeyProvider(
this IServiceCollection services,
string privateKeyFilePath,
bool cacheResult = false)
{
services.TryAddSingleton<IYandexPrivateKeyProvider>(
new YandexFilePrivateKeyProvider(privateKeyFilePath, cacheResult));
return services;
}

public static IServiceCollection AddYandexStaticPrivateKeyProvider(
this IServiceCollection services,
string privateKey)
{
services.TryAddSingleton<IYandexPrivateKeyProvider>(
new YandexStaticPrivateKeyProvider(privateKey));
return services;
}

public static IServiceCollection AddYandexFuncPrivateKeyProvider(
this IServiceCollection services,
Func<IServiceProvider, CancellationToken, Task<char[]>> privateKeyFunc,
bool cacheResult = false)
{
Func<IServiceProvider, IYandexPrivateKeyProvider> factory = sp =>
{
return new YandexFuncPrivateKeyProvider(
ct => privateKeyFunc(sp, ct), cacheResult);
};
services.TryAddSingleton(factory);
return services;
}

public static IServiceCollection AddYandexFuncPrivateKeyProvider(
this IServiceCollection services,
Func<CancellationToken, Task<char[]>> privateKeyFunc,
bool cacheResult = false)
{
services.TryAddSingleton<IYandexPrivateKeyProvider>(
new YandexFuncPrivateKeyProvider(privateKeyFunc, cacheResult));
return services;
}

public static IServiceCollection AddYandexJwtGenerationServices(
this IServiceCollection services,
IConfiguration configuration,
string optionsSectionName = YandexIamOptions.SectionName)
{
services.AddOptions<YandexIamOptions>()
.Bind(configuration.GetSection(optionsSectionName));
services.TryAddSingleton<IYandexRsaFactory, YandexRsaFactory>();
services.TryAddSingleton<YandexJsonWebTokenGenerator>();
return services;
}

public static IServiceCollection AddYandexIamServiceClient(
this IServiceCollection services,
TimeSpan? requestTimeout = null)
{
services
.AddHttpClient<IYandexIamServiceClient, YandexIamServiceClient>(
(sp, httpClient) =>
{
httpClient.BaseAddress = new Uri(YandexIamOptions.ApiHost);
httpClient.Timeout = requestTimeout ?? TimeSpan.FromSeconds(10);
});
return services;
}

public static IServiceCollection AddDefaultYandexIamServiceClient(
this IServiceCollection services,
IConfiguration configuration,
TimeSpan? requestTimeout = null)
{
services.AddYandexJwtGenerationServices(configuration);
services.AddYandexIamServiceClient(requestTimeout);
return services;
}
}
Loading

0 comments on commit 717ee9a

Please sign in to comment.