Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Backup*/

# Coverage
OpenCover/
**/coverage*.xml

# .NET tools
src/tools/
Expand Down
92 changes: 92 additions & 0 deletions src/Yoti.Auth/CentralAuth/AuthenticationTokenGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;

namespace Yoti.Auth.CentralAuth
{
public class AuthenticationTokenGenerator
{
private readonly string _sdkId;
private readonly AsymmetricCipherKeyPair _keyPair;
private readonly IList<string> _scopes;
private readonly string _authApiUrl;

internal AuthenticationTokenGenerator(string sdkId, AsymmetricCipherKeyPair keyPair, IList<string> scopes, string authApiUrl)
{
_sdkId = sdkId;
_keyPair = keyPair;
_scopes = scopes;
_authApiUrl = authApiUrl;
}

public async Task<AuthenticationTokenResponse> GetToken(HttpClient httpClient)
{
Validation.NotNull(httpClient, nameof(httpClient));
string jwt = BuildJwt();
string scope = string.Join(" ", _scopes);

Comment on lines +27 to +32
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
new KeyValuePair<string, string>("client_assertion", jwt),
new KeyValuePair<string, string>("scope", scope)
});

using (var response = await httpClient.PostAsync(_authApiUrl, formContent).ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
string body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var tokenResponse = JsonConvert.DeserializeObject<AuthenticationTokenResponse>(body);
if (tokenResponse == null)
throw new InvalidOperationException("The authentication server returned an unexpected empty response.");
return tokenResponse;
}
}

private string BuildJwt()
{
var header = new { alg = "PS384", typ = "JWT" };
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
var payload = new
{
iss = _sdkId,
sub = _sdkId,
aud = _authApiUrl,
iat = now,
exp = now + 300,
jti = Guid.NewGuid().ToString()
};

string encodedHeader = Base64UrlEncode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header)));
string encodedPayload = Base64UrlEncode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload)));
string signingInput = $"{encodedHeader}.{encodedPayload}";

byte[] signingBytes = Encoding.ASCII.GetBytes(signingInput);
byte[] signature = SignPs384(signingBytes);

return $"{signingInput}.{Base64UrlEncode(signature)}";
}

private byte[] SignPs384(byte[] data)
{
ISigner signer = SignerUtilities.GetSigner("SHA-384withRSAandMGF1");
signer.Init(true, _keyPair.Private);
signer.BlockUpdate(data, 0, data.Length);
return signer.GenerateSignature();
}

private static string Base64UrlEncode(byte[] input)
{
return Convert.ToBase64String(input)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
}
}
}
75 changes: 75 additions & 0 deletions src/Yoti.Auth/CentralAuth/AuthenticationTokenGeneratorBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.IO;
using Org.BouncyCastle.Crypto;
using Yoti.Auth.Constants;

namespace Yoti.Auth.CentralAuth
{
public class AuthenticationTokenGeneratorBuilder
{
private string _sdkId;
private AsymmetricCipherKeyPair _keyPair;
private readonly List<string> _scopes = new List<string>();
private string _authApiUrl = Api.DefaultAuthApiUrl;

public AuthenticationTokenGeneratorBuilder WithSdkId(string sdkId)
{
Validation.NotNullOrEmpty(sdkId, nameof(sdkId));
_sdkId = sdkId;
return this;
}

public AuthenticationTokenGeneratorBuilder WithKey(StreamReader privateKeyStream)
{
if (privateKeyStream == null)
throw new ArgumentNullException(nameof(privateKeyStream));
_keyPair = CryptoEngine.LoadRsaKey(privateKeyStream);
return this;
}

public AuthenticationTokenGeneratorBuilder WithKey(AsymmetricCipherKeyPair keyPair)
{
_keyPair = keyPair ?? throw new ArgumentNullException(nameof(keyPair));
return this;
}

public AuthenticationTokenGeneratorBuilder WithScopes(IEnumerable<string> scopes)
{
if (scopes == null)
throw new ArgumentNullException(nameof(scopes));
foreach (var scope in scopes)
{
Validation.NotNullOrWhiteSpace(scope, nameof(scopes));
_scopes.Add(scope);
}
return this;
}

public AuthenticationTokenGeneratorBuilder WithScope(string scope)
{
Validation.NotNullOrWhiteSpace(scope, nameof(scope));
_scopes.Add(scope);
return this;
}
Comment thread
Copilot marked this conversation as resolved.

public AuthenticationTokenGeneratorBuilder WithAuthApiUrl(string authApiUrl)
{
Validation.NotNullOrEmpty(authApiUrl, nameof(authApiUrl));
_authApiUrl = authApiUrl;
return this;
}

public AuthenticationTokenGenerator Build()
{
Validation.NotNullOrEmpty(_sdkId, nameof(_sdkId));
if (_keyPair == null)
throw new InvalidOperationException("A key pair must be provided via WithKey before calling Build().");

if (_scopes.Count == 0)
throw new InvalidOperationException("At least one scope must be added via WithScope or WithScopes before calling Build().");

return new AuthenticationTokenGenerator(_sdkId, _keyPair, _scopes, _authApiUrl);
}
}
}
19 changes: 19 additions & 0 deletions src/Yoti.Auth/CentralAuth/AuthenticationTokenResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json;

namespace Yoti.Auth.CentralAuth
{
public class AuthenticationTokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }

[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }

[JsonProperty("token_type")]
public string TokenType { get; set; }

[JsonProperty("scope")]
public string Scope { get; set; }
}
}
3 changes: 3 additions & 0 deletions src/Yoti.Auth/Constants/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,8 @@ public static class Api
public const string ContentTypeJson = "application/json";

public const string SdkIdentifier = ".NET";

public const string AuthorizationHeader = "Authorization";
public const string DefaultAuthApiUrl = "https://auth.api.yoti.com/v1/oauth/token";
}
}
Loading