diff --git a/.gitignore b/.gitignore index 6e27c66c..035d18d8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ Backup*/ # Coverage OpenCover/ +**/coverage*.xml # .NET tools src/tools/ diff --git a/src/Yoti.Auth/CentralAuth/AuthenticationTokenGenerator.cs b/src/Yoti.Auth/CentralAuth/AuthenticationTokenGenerator.cs new file mode 100644 index 00000000..d9a3ef68 --- /dev/null +++ b/src/Yoti.Auth/CentralAuth/AuthenticationTokenGenerator.cs @@ -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 _scopes; + private readonly string _authApiUrl; + + internal AuthenticationTokenGenerator(string sdkId, AsymmetricCipherKeyPair keyPair, IList scopes, string authApiUrl) + { + _sdkId = sdkId; + _keyPair = keyPair; + _scopes = scopes; + _authApiUrl = authApiUrl; + } + + public async Task GetToken(HttpClient httpClient) + { + Validation.NotNull(httpClient, nameof(httpClient)); + string jwt = BuildJwt(); + string scope = string.Join(" ", _scopes); + + var formContent = new FormUrlEncodedContent(new[] + { + new KeyValuePair("grant_type", "client_credentials"), + new KeyValuePair("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), + new KeyValuePair("client_assertion", jwt), + new KeyValuePair("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(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('='); + } + } +} diff --git a/src/Yoti.Auth/CentralAuth/AuthenticationTokenGeneratorBuilder.cs b/src/Yoti.Auth/CentralAuth/AuthenticationTokenGeneratorBuilder.cs new file mode 100644 index 00000000..dd6ae7e8 --- /dev/null +++ b/src/Yoti.Auth/CentralAuth/AuthenticationTokenGeneratorBuilder.cs @@ -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 _scopes = new List(); + 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 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; + } + + 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); + } + } +} diff --git a/src/Yoti.Auth/CentralAuth/AuthenticationTokenResponse.cs b/src/Yoti.Auth/CentralAuth/AuthenticationTokenResponse.cs new file mode 100644 index 00000000..6b894c1a --- /dev/null +++ b/src/Yoti.Auth/CentralAuth/AuthenticationTokenResponse.cs @@ -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; } + } +} diff --git a/src/Yoti.Auth/Constants/Api.cs b/src/Yoti.Auth/Constants/Api.cs index 08bc4b75..969ea1f8 100644 --- a/src/Yoti.Auth/Constants/Api.cs +++ b/src/Yoti.Auth/Constants/Api.cs @@ -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"; } } \ No newline at end of file diff --git a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs index 3043fca8..de552c6a 100644 --- a/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs +++ b/src/Yoti.Auth/DigitalIdentity/DigitalIdentityService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Net.Http; @@ -24,249 +24,191 @@ public static class DigitalIdentityService private const string sessionCreation = "/v2/sessions"; private const string yotiAuthId = "X-Yoti-Auth-Id"; - internal static async Task CreateShareSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, ShareSessionRequest shareSessionRequestPayload) + internal static async Task CreateShareSession(HttpClient httpClient, Uri apiUrl, IAuthStrategy authStrategy, ShareSessionRequest shareSessionRequestPayload) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); - Validation.NotNull(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNull(shareSessionRequestPayload, nameof(shareSessionRequestPayload)); string serializedScenario = JsonConvert.SerializeObject( shareSessionRequestPayload, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }); + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); byte[] body = Encoding.UTF8.GetBytes(serializedScenario); - Request shareSessionRequest = new RequestBuilder() - .WithKeyPair(keyPair) + var builder = new RequestBuilder() + .WithAuthStrategy(authStrategy) .WithBaseUri(apiUrl) - .WithHeader(yotiAuthId, sdkId) .WithEndpoint(sessionCreation) - .WithQueryParam("sdkID", sdkId) .WithHttpMethod(HttpMethod.Post) - .WithContent(body) - .Build(); + .WithContent(body); + + if (authStrategy.SdkId != null) + { + builder = builder + .WithHeader(yotiAuthId, authStrategy.SdkId) + .WithQueryParam("sdkID", authStrategy.SdkId); + } + + Request shareSessionRequest = builder.Build(); using (HttpResponseMessage response = await shareSessionRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - internal static async Task GetSession(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) + internal static async Task GetSession(HttpClient httpClient, Uri apiUrl, IAuthStrategy authStrategy, string sessionId) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); - Validation.NotNull(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); - Validation.NotNull(sessionId, nameof(sessionId)); + Validation.NotNull(authStrategy, nameof(authStrategy)); + Validation.NotNull(sessionId, nameof(sessionId)); - - Request getSessionRequest = new RequestBuilder() - .WithKeyPair(keyPair) + var builder = new RequestBuilder() + .WithAuthStrategy(authStrategy) .WithBaseUri(apiUrl) - .WithHeader(yotiAuthId, sdkId) .WithEndpoint(string.Format("{0}/{1}", sessionCreation, sessionId)) - .WithQueryParam("appId", sdkId) - .WithHttpMethod(HttpMethod.Get) - .Build(); + .WithHttpMethod(HttpMethod.Get); + + if (authStrategy.SdkId != null) + { + builder = builder + .WithHeader(yotiAuthId, authStrategy.SdkId) + .WithQueryParam("appId", authStrategy.SdkId); + } + + Request getSessionRequest = builder.Build(); using (HttpResponseMessage response = await getSessionRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - internal static async Task CreateQrCode(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId,QrRequest qrRequestPayload) + internal static async Task CreateQrCode(HttpClient httpClient, Uri apiUrl, IAuthStrategy authStrategy, string sessionId, QrRequest qrRequestPayload) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); - Validation.NotNull(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); + Validation.NotNullOrEmpty(sessionId, nameof(sessionId)); + Validation.NotNull(qrRequestPayload, nameof(qrRequestPayload)); string serializedQrCode = JsonConvert.SerializeObject( qrRequestPayload, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }); + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); byte[] body = Encoding.UTF8.GetBytes(serializedQrCode); - - Request createQrRequest = new RequestBuilder() - .WithKeyPair(keyPair) + var builder = new RequestBuilder() + .WithAuthStrategy(authStrategy) .WithBaseUri(apiUrl) - .WithHeader(yotiAuthId, sdkId) - .WithEndpoint(string.Format($"/v2/sessions/{0}/qr-codes", sessionId)) - .WithQueryParam("appId", sdkId) + .WithEndpoint(string.Format("/v2/sessions/{0}/qr-codes", sessionId)) .WithHttpMethod(HttpMethod.Post) - .WithContent(body) - .Build(); - + .WithContent(body); + + if (authStrategy.SdkId != null) + { + builder = builder + .WithHeader(yotiAuthId, authStrategy.SdkId) + .WithQueryParam("appId", authStrategy.SdkId); + } + + Request createQrRequest = builder.Build(); + using (HttpResponseMessage response = await createQrRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - - internal static async Task GetQrCode(HttpClient httpClient, Uri apiUrl, string sdkId, AsymmetricCipherKeyPair keyPair, string qrCodeId) + + internal static async Task GetQrCode(HttpClient httpClient, Uri apiUrl, IAuthStrategy authStrategy, string qrCodeId) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); - Validation.NotNull(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNull(qrCodeId, nameof(qrCodeId)); - Request QrCodeRequest = new RequestBuilder() - .WithKeyPair(keyPair) + var builder = new RequestBuilder() + .WithAuthStrategy(authStrategy) .WithBaseUri(apiUrl) - .WithHeader(yotiAuthId, sdkId) - .WithEndpoint(string.Format($"/v2/qr-codes/{0}", qrCodeId)) - .WithQueryParam("appId", sdkId) - .WithHttpMethod(HttpMethod.Get) - .Build(); + .WithEndpoint(string.Format("/v2/qr-codes/{0}", qrCodeId)) + .WithHttpMethod(HttpMethod.Get); - using (HttpResponseMessage response = await QrCodeRequest.Execute(httpClient).ConfigureAwait(false)) + if (authStrategy.SdkId != null) { - if (!response.IsSuccessStatusCode) - { - Response.CreateYotiExceptionFromStatusCode(response); - } - - var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + builder = builder + .WithHeader(yotiAuthId, authStrategy.SdkId) + .WithQueryParam("appId", authStrategy.SdkId); } - } - - private static async Task GetReceipt(HttpClient httpClient, string receiptId, string sdkId,Uri apiUrl, AsymmetricCipherKeyPair keyPair) - { - Validation.NotNull(httpClient, nameof(httpClient)); - Validation.NotNull(apiUrl, nameof(apiUrl)); - Validation.NotNull(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); - string receiptUrl = Base64ToBase64URL(receiptId); - string endpoint = string.Format(receiptRetrieval, receiptUrl); - - Request ReceiptRequest = new RequestBuilder() - .WithKeyPair(keyPair) - .WithBaseUri(apiUrl) - .WithHeader(yotiAuthId, sdkId) - .WithEndpoint(endpoint) - .WithQueryParam("sdkID", sdkId) - .WithHttpMethod(HttpMethod.Get) - .Build(); + Request qrCodeRequest = builder.Build(); - using (HttpResponseMessage response = await ReceiptRequest.Execute(httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await qrCodeRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - - public static string Base64ToBase64URL(string base64Str) - { - try - { - byte[] decodedBytes = Convert.FromBase64String(base64Str); - string base64URL = Convert.ToBase64String(decodedBytes) - .Replace('+', '-') - .Replace('/', '_') - .TrimEnd('='); - return base64URL; - } - catch (FormatException) - { - return ""; - } - } - - public static async Task GetShareReceipt(HttpClient httpClient, string clientSdkId, Uri apiUrl, AsymmetricCipherKeyPair key, string receiptId) + public static async Task GetShareReceipt(HttpClient httpClient, Uri apiUrl, IAuthStrategy authStrategy, string receiptId) { Validation.NotNullOrEmpty(receiptId, nameof(receiptId)); + + var keyPair = (authStrategy as SignedRequestAuthStrategy)?.KeyPair; + if (keyPair == null) + throw new InvalidOperationException("GetShareReceipt requires a signed-request strategy with a private key for receipt decryption."); + try { - var receiptResponse = await GetReceipt(httpClient, receiptId, clientSdkId, apiUrl, key); + var receiptResponse = await GetReceipt(httpClient, receiptId, apiUrl, authStrategy); var itemKeyId = receiptResponse.WrappedItemKeyId; - - var encryptedItemKeyResponse = await GetReceiptItemKey(httpClient, itemKeyId, clientSdkId, apiUrl, key); - var receiptContentKey = CryptoEngine.UnwrapReceiptKey(receiptResponse.WrappedKey, encryptedItemKeyResponse.Value, encryptedItemKeyResponse.Iv, key); + var encryptedItemKeyResponse = await GetReceiptItemKey(httpClient, itemKeyId, apiUrl, authStrategy); + + var receiptContentKey = CryptoEngine.UnwrapReceiptKey(receiptResponse.WrappedKey, encryptedItemKeyResponse.Value, encryptedItemKeyResponse.Iv, keyPair); var (attrData, aextra, decryptAttrDataError) = DecryptReceiptContent(receiptResponse.Content, receiptContentKey); if (decryptAttrDataError != null) - { throw new Exception($"An unexpected error occurred: {decryptAttrDataError.Message}"); - } - var parsedAttributesApp = AttributeConverter.ConvertToBaseAttributes(attrData); - var appProfile = new ApplicationProfile(parsedAttributesApp - ); - + var parsedAttributesApp = AttributeConverter.ConvertToBaseAttributes(attrData); + var appProfile = new ApplicationProfile(parsedAttributesApp); + var (attrOtherData, aOtherExtra, decryptOtherAttrDataError) = DecryptReceiptContent(receiptResponse.OtherPartyContent, receiptContentKey); - if (decryptAttrDataError != null) - { - throw new Exception($"An unexpected error occurred: {decryptAttrDataError.Message}"); - } + if (decryptOtherAttrDataError != null) + throw new Exception($"An unexpected error occurred: {decryptOtherAttrDataError.Message}"); var userProfile = new YotiProfile(); if (attrOtherData != null) { - var parsedAttributesUser = AttributeConverter.ConvertToBaseAttributes(attrOtherData); + var parsedAttributesUser = AttributeConverter.ConvertToBaseAttributes(attrOtherData); userProfile = new YotiProfile(parsedAttributesUser); } - - + ExtraData userExtraData = new ExtraData(); if (aOtherExtra != null) - { userExtraData = ExtraDataConverter.ParseExtraDataProto(aOtherExtra); - } + ExtraData appExtraData = new ExtraData(); if (aextra != null) - { - appExtraData = ExtraDataConverter.ParseExtraDataProto(aextra); - } - - var sharedReceiptResponse = new SharedReceiptResponse + + return new SharedReceiptResponse { ID = receiptResponse.ID, SessionID = receiptResponse.SessionID, @@ -285,46 +227,94 @@ public static async Task GetShareReceipt(HttpClient httpC }, Error = receiptResponse.Error, ErrorDetails = receiptResponse.ErrorDetails - }; - - return sharedReceiptResponse; } - catch (Exception ex) + catch (Exception ex) { throw new Exception($"An unexpected error occurred: {ex.Message}"); - } } - private static async Task GetReceiptItemKey(HttpClient httpClient, string receiptItemKeyId, string sdkId, Uri apiUrl, AsymmetricCipherKeyPair keyPair) + private static async Task GetReceipt(HttpClient httpClient, string receiptId, Uri apiUrl, IAuthStrategy authStrategy) { Validation.NotNull(httpClient, nameof(httpClient)); Validation.NotNull(apiUrl, nameof(apiUrl)); - Validation.NotNull(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); + + string receiptUrl = Base64ToBase64URL(receiptId); + string endpoint = string.Format(receiptRetrieval, receiptUrl); + + var builder = new RequestBuilder() + .WithAuthStrategy(authStrategy) + .WithBaseUri(apiUrl) + .WithEndpoint(endpoint) + .WithHttpMethod(HttpMethod.Get); + + if (authStrategy.SdkId != null) + { + builder = builder + .WithHeader(yotiAuthId, authStrategy.SdkId) + .WithQueryParam("sdkID", authStrategy.SdkId); + } + + Request receiptRequest = builder.Build(); + + using (HttpResponseMessage response = await receiptRequest.Execute(httpClient).ConfigureAwait(false)) + { + if (!response.IsSuccessStatusCode) + Response.CreateYotiExceptionFromStatusCode(response); + + var responseObject = await response.Content.ReadAsStringAsync(); + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + } + } + + private static async Task GetReceiptItemKey(HttpClient httpClient, string receiptItemKeyId, Uri apiUrl, IAuthStrategy authStrategy) + { + Validation.NotNull(httpClient, nameof(httpClient)); + Validation.NotNull(apiUrl, nameof(apiUrl)); + Validation.NotNull(authStrategy, nameof(authStrategy)); + string endpoint = string.Format(receiptKeyRetrieval, receiptItemKeyId); - Request ReceiptItemKeyRequest = new RequestBuilder() - .WithKeyPair(keyPair) + var builder = new RequestBuilder() + .WithAuthStrategy(authStrategy) .WithBaseUri(apiUrl) - .WithHeader(yotiAuthId, sdkId) .WithEndpoint(endpoint) - .WithQueryParam("appId", sdkId) - .WithHttpMethod(HttpMethod.Get) - .Build(); + .WithHttpMethod(HttpMethod.Get); - using (HttpResponseMessage response = await ReceiptItemKeyRequest.Execute(httpClient).ConfigureAwait(false)) + if (authStrategy.SdkId != null) + { + builder = builder + .WithHeader(yotiAuthId, authStrategy.SdkId) + .WithQueryParam("appId", authStrategy.SdkId); + } + + Request receiptItemKeyRequest = builder.Build(); + + using (HttpResponseMessage response = await receiptItemKeyRequest.Execute(httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); + } + } - return deserialized; + public static string Base64ToBase64URL(string base64Str) + { + try + { + byte[] decodedBytes = Convert.FromBase64String(base64Str); + return Convert.ToBase64String(decodedBytes) + .Replace('+', '-') + .Replace('/', '_') + .TrimEnd('='); + } + catch (FormatException) + { + return ""; } } @@ -332,7 +322,6 @@ public static (AttributeList attrData, byte[] aextra, Exception error) DecryptRe { AttributeList attrData = null; byte[] aextra = null; - Exception error = null; if (content != null) { @@ -346,8 +335,7 @@ public static (AttributeList attrData, byte[] aextra, Exception error) DecryptRe } catch (Exception ex) { - error = new Exception($"failed to decrypt content profile: {ex.Message}", ex); - return (null, null, error); + return (null, null, new Exception($"failed to decrypt content profile: {ex.Message}", ex)); } } @@ -359,15 +347,12 @@ public static (AttributeList attrData, byte[] aextra, Exception error) DecryptRe } catch (Exception ex) { - error = new Exception($"failed to decrypt receipt content extra data: {ex.Message}", ex); - return (null, null, error); + return (null, null, new Exception($"failed to decrypt receipt content extra data: {ex.Message}", ex)); } } } - + return (attrData, aextra, null); } } - - } diff --git a/src/Yoti.Auth/DigitalIdentityClient.cs b/src/Yoti.Auth/DigitalIdentityClient.cs index 628a1a06..a95ce692 100644 --- a/src/Yoti.Auth/DigitalIdentityClient.cs +++ b/src/Yoti.Auth/DigitalIdentityClient.cs @@ -1,129 +1,117 @@ -using System; +using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; using Org.BouncyCastle.Crypto; using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.Web; namespace Yoti.Auth { public class DigitalIdentityClient { - private readonly string _sdkId; - private readonly AsymmetricCipherKeyPair _keyPair; + private readonly IAuthStrategy _authStrategy; private readonly DigitalIdentityClientEngine _yotiDigitalClientEngine; internal Uri ApiUri { get; private set; } /// - /// Create a + /// Create a using signed-request authentication. /// - /// The client SDK ID provided on the Yoti Hub. - /// - /// The private key file provided on the Yoti Hub as a . - /// public DigitalIdentityClient(string sdkId, StreamReader privateKeyStream) : this(new HttpClient(), sdkId, CryptoEngine.LoadRsaKey(privateKeyStream)) { } /// - /// Create a with a specified + /// Create a using signed-request authentication with a specified . /// - /// Allows the specification of a HttpClient - /// The client SDK ID provided on the Yoti Hub. - /// - /// The private key file provided on the Yoti Hub as a . - /// public DigitalIdentityClient(HttpClient httpClient, string sdkId, StreamReader privateKeyStream) : this(httpClient, sdkId, CryptoEngine.LoadRsaKey(privateKeyStream)) { } /// - /// Create a with a specified + /// Create a using signed-request authentication with a specified . /// - /// Allows the specification of a HttpClient - /// The client SDK ID provided on the Yoti Hub. - /// The key pair from the Yoti Hub. public DigitalIdentityClient(HttpClient httpClient, string sdkId, AsymmetricCipherKeyPair keyPair) { Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); Validation.NotNull(keyPair, nameof(keyPair)); - _sdkId = sdkId; - _keyPair = keyPair; - + _authStrategy = new SignedRequestAuthStrategy(keyPair, sdkId); SetYotiApiUri(); - _yotiDigitalClientEngine = new DigitalIdentityClientEngine(httpClient); } - + + /// + /// Creates a using central auth bearer token authentication. + /// Use this factory method instead of a constructor to avoid ambiguity with the sdkId overloads. + /// + /// The bearer token supplied by the relying business. + public static DigitalIdentityClient FromBearerToken(string authToken) + => FromBearerToken(new HttpClient(), authToken); + /// - /// Initiate a sharing process based on a . + /// Creates a using central auth bearer token authentication with a specified . /// - /// - /// Details of the device's callback endpoint, and extensions for the application - /// - /// + /// The to use. + /// The bearer token supplied by the relying business. + public static DigitalIdentityClient FromBearerToken(HttpClient httpClient, string authToken) + { + Validation.NotNullOrEmpty(authToken, nameof(authToken)); + return new DigitalIdentityClient(new BearerTokenAuthStrategy(authToken), httpClient); + } + + private DigitalIdentityClient(IAuthStrategy authStrategy, HttpClient httpClient) + { + _authStrategy = authStrategy; + SetYotiApiUri(); + _yotiDigitalClientEngine = new DigitalIdentityClientEngine(httpClient); + } + public ShareSessionResult CreateShareSession(ShareSessionRequest shareSessionRequest) { Task task = Task.Run(async () => await CreateShareSessionAsync(shareSessionRequest).ConfigureAwait(false)); - return task.Result; } - /// - /// Asynchronously initiate a sharing process based on a . - /// - /// - /// Details of the device's callback endpoint, and extensions for the application - /// - /// public async Task CreateShareSessionAsync(ShareSessionRequest shareSessionRequest) { - return await _yotiDigitalClientEngine.CreateShareSessionAsync(_sdkId, _keyPair, ApiUri, shareSessionRequest).ConfigureAwait(false); + return await _yotiDigitalClientEngine.CreateShareSessionAsync(_authStrategy, ApiUri, shareSessionRequest).ConfigureAwait(false); } public SharedReceiptResponse GetShareReceipt(string receiptId) { - Task task = Task.Run(async () => await _yotiDigitalClientEngine.GetShareReceipt(_sdkId, _keyPair, ApiUri, receiptId).ConfigureAwait(false)); + Task task = Task.Run(async () => await _yotiDigitalClientEngine.GetShareReceipt(_authStrategy, ApiUri, receiptId).ConfigureAwait(false)); return task.Result; } - - + public async Task CreateQrCode(string sessionId, QrRequest qrRequest) { - return await _yotiDigitalClientEngine.CreateQrCodeAsync(_sdkId, _keyPair, ApiUri, sessionId, qrRequest).ConfigureAwait(false); + return await _yotiDigitalClientEngine.CreateQrCodeAsync(_authStrategy, ApiUri, sessionId, qrRequest).ConfigureAwait(false); } - + public async Task GetQrCode(string qrCodeId) { - return await _yotiDigitalClientEngine.GetQrCodeAsync(_sdkId, _keyPair, ApiUri, qrCodeId).ConfigureAwait(false); + return await _yotiDigitalClientEngine.GetQrCodeAsync(_authStrategy, ApiUri, qrCodeId).ConfigureAwait(false); } - + public async Task GetSession(string sessionId) { - return await _yotiDigitalClientEngine.GetSession(_sdkId, _keyPair, ApiUri, sessionId).ConfigureAwait(false); + return await _yotiDigitalClientEngine.GetSession(_authStrategy, ApiUri, sessionId).ConfigureAwait(false); } internal void SetYotiApiUri() { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("YOTI_API_URL"))) - { - ApiUri = new Uri(Environment.GetEnvironmentVariable("YOTI_API_URL")); - } - else - { - ApiUri = new Uri(Constants.Api.DefaultYotiShareApiUrl); - } + string envUrl = Environment.GetEnvironmentVariable("YOTI_API_URL"); + ApiUri = !string.IsNullOrEmpty(envUrl) + ? new Uri(envUrl) + : new Uri(Constants.Api.DefaultYotiShareApiUrl); } public DigitalIdentityClient OverrideApiUri(Uri apiUri) { ApiUri = apiUri; - return this; } } diff --git a/src/Yoti.Auth/DigitalIdentityClientEngine.cs b/src/Yoti.Auth/DigitalIdentityClientEngine.cs index 0db68023..a2d1bfbc 100644 --- a/src/Yoti.Auth/DigitalIdentityClientEngine.cs +++ b/src/Yoti.Auth/DigitalIdentityClientEngine.cs @@ -1,11 +1,11 @@ -using System; +using System; #pragma warning disable S1128 using System.Net; #pragma warning restore S1128 using System.Net.Http; using System.Threading.Tasks; -using Org.BouncyCastle.Crypto; using Yoti.Auth.DigitalIdentity; +using Yoti.Auth.Web; namespace Yoti.Auth { @@ -21,50 +21,40 @@ public DigitalIdentityClientEngine(HttpClient httpClient) ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; #endif } - - public async Task CreateShareSessionAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, ShareSessionRequest shareSessionRequest) + + public async Task CreateShareSessionAsync(IAuthStrategy authStrategy, Uri apiUrl, ShareSessionRequest shareSessionRequest) { - ShareSessionResult result = await Task.Run(async () => await DigitalIdentityService.CreateShareSession( - _httpClient, apiUrl, sdkId, keyPair, shareSessionRequest).ConfigureAwait(false)) + return await Task.Run(async () => await DigitalIdentityService.CreateShareSession( + _httpClient, apiUrl, authStrategy, shareSessionRequest).ConfigureAwait(false)) .ConfigureAwait(false); - - return result; } - public async Task GetShareReceipt(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string receiptId) + public async Task GetShareReceipt(IAuthStrategy authStrategy, Uri apiUrl, string receiptId) { - SharedReceiptResponse result = await Task.Run(async () => await DigitalIdentityService.GetShareReceipt( - _httpClient, sdkId, apiUrl, keyPair, receiptId).ConfigureAwait(false)) + return await Task.Run(async () => await DigitalIdentityService.GetShareReceipt( + _httpClient, apiUrl, authStrategy, receiptId).ConfigureAwait(false)) .ConfigureAwait(false); - - return result; } - - public async Task CreateQrCodeAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string sessionid, QrRequest qRRequest) + + public async Task CreateQrCodeAsync(IAuthStrategy authStrategy, Uri apiUrl, string sessionId, QrRequest qrRequest) { - CreateQrResult result = await Task.Run(async () => await DigitalIdentityService.CreateQrCode( - _httpClient, apiUrl, sdkId, keyPair, sessionid, qRRequest).ConfigureAwait(false)) + return await Task.Run(async () => await DigitalIdentityService.CreateQrCode( + _httpClient, apiUrl, authStrategy, sessionId, qrRequest).ConfigureAwait(false)) .ConfigureAwait(false); - - return result; } - - public async Task GetQrCodeAsync(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string qrcodeId) + + public async Task GetQrCodeAsync(IAuthStrategy authStrategy, Uri apiUrl, string qrCodeId) { - GetQrCodeResult result = await Task.Run(async () => await DigitalIdentityService.GetQrCode( - _httpClient, apiUrl, sdkId, keyPair, qrcodeId).ConfigureAwait(false)) + return await Task.Run(async () => await DigitalIdentityService.GetQrCode( + _httpClient, apiUrl, authStrategy, qrCodeId).ConfigureAwait(false)) .ConfigureAwait(false); - - return result; } - - public async Task GetSession(string sdkId, AsymmetricCipherKeyPair keyPair, Uri apiUrl, string sessionId) + + public async Task GetSession(IAuthStrategy authStrategy, Uri apiUrl, string sessionId) { - var result = await Task.Run(async () => await DigitalIdentityService.GetSession( - _httpClient, apiUrl, sdkId, keyPair, sessionId).ConfigureAwait(false)) + return await Task.Run(async () => await DigitalIdentityService.GetSession( + _httpClient, apiUrl, authStrategy, sessionId).ConfigureAwait(false)) .ConfigureAwait(false); - - return result; } } } diff --git a/src/Yoti.Auth/DocScan/DocScanClient.cs b/src/Yoti.Auth/DocScan/DocScanClient.cs index 70266f23..503866f6 100644 --- a/src/Yoti.Auth/DocScan/DocScanClient.cs +++ b/src/Yoti.Auth/DocScan/DocScanClient.cs @@ -9,6 +9,7 @@ using Yoti.Auth.DocScan.Session.Retrieve.Configuration; using Yoti.Auth.DocScan.Session.Retrieve.CreateFaceCaptureResourceResponse; using Yoti.Auth.DocScan.Support; +using Yoti.Auth.Web; namespace Yoti.Auth.DocScan { @@ -17,8 +18,7 @@ namespace Yoti.Auth.DocScan /// public class DocScanClient { - private readonly string _sdkId; - private readonly AsymmetricCipherKeyPair _keyPair; + private readonly IAuthStrategy _authStrategy; private readonly DocScanService _docScanService; private readonly NLog.Logger _logger; @@ -37,11 +37,30 @@ public DocScanClient(string sdkId, AsymmetricCipherKeyPair keyPair, HttpClient h if (httpClient == null) httpClient = new HttpClient(); - _sdkId = sdkId; - _keyPair = keyPair; + _authStrategy = new SignedRequestAuthStrategy(keyPair, sdkId); _docScanService = new DocScanService(httpClient, apiUrl); } + /// + /// Creates a using central auth bearer token authentication. + /// Use this factory method instead of a constructor to avoid ambiguity with the sdkId overloads. + /// + /// The bearer token supplied by the relying business. + /// Optional to use. + /// Optional API URI override. + public static DocScanClient FromBearerToken(string authToken, HttpClient httpClient = null, Uri apiUri = null) + { + Validation.NotNullOrEmpty(authToken, nameof(authToken)); + return new DocScanClient(new BearerTokenAuthStrategy(authToken), httpClient ?? new HttpClient(), apiUri); + } + + private DocScanClient(IAuthStrategy authStrategy, HttpClient httpClient, Uri apiUri) + { + _logger = NLog.LogManager.GetCurrentClassLogger(); + _authStrategy = authStrategy; + _docScanService = new DocScanService(httpClient, apiUri); + } + /// /// Creates a Doc Scan session using the supplied session specification /// @@ -51,7 +70,7 @@ public async Task CreateSessionAsync(SessionSpecification s { _logger.Debug("Creating a Yoti Doc Scan session..."); - return await _docScanService.CreateSession(_sdkId, _keyPair, sessionSpec).ConfigureAwait(false); + return await _docScanService.CreateSession(_authStrategy, sessionSpec).ConfigureAwait(false); } /// @@ -73,7 +92,7 @@ public async Task GetSessionAsync(string sessionId) { _logger.Debug($"Retrieving session '{sessionId}'"); - return await _docScanService.GetSession(_sdkId, _keyPair, sessionId).ConfigureAwait(false); + return await _docScanService.GetSession(_authStrategy, sessionId).ConfigureAwait(false); } /// @@ -94,7 +113,7 @@ public async Task DeleteSessionAsync(string sessionId) { _logger.Debug($"Deleting session '{sessionId}'"); - await _docScanService.DeleteSession(_sdkId, _keyPair, sessionId).ConfigureAwait(false); + await _docScanService.DeleteSession(_authStrategy, sessionId).ConfigureAwait(false); } /// @@ -116,7 +135,7 @@ public async Task GetMediaContentAsync(string sessionId, string medi { _logger.Debug($"Retrieving media content '{mediaId}' in session '{sessionId}'"); - return await _docScanService.GetMediaContent(_sdkId, _keyPair, sessionId, mediaId).ConfigureAwait(false); + return await _docScanService.GetMediaContent(_authStrategy, sessionId, mediaId).ConfigureAwait(false); } /// @@ -148,7 +167,7 @@ public async Task DeleteMediaContentAsync(string sessionId, string mediaId) { _logger.Debug($"Deleting media content '{mediaId}' in session '{sessionId}'"); - await _docScanService.DeleteMediaContent(_sdkId, _keyPair, sessionId, mediaId).ConfigureAwait(false); + await _docScanService.DeleteMediaContent(_authStrategy, sessionId, mediaId).ConfigureAwait(false); } /// @@ -168,7 +187,7 @@ public async Task GetSupportedDocumentsAsync(bool is { _logger.Debug("Retrieving supported documents"); - return await _docScanService.GetSupportedDocuments(_sdkId, _keyPair, isStrictlyLatin).ConfigureAwait(false); + return await _docScanService.GetSupportedDocuments(_authStrategy, isStrictlyLatin).ConfigureAwait(false); } /// @@ -192,7 +211,7 @@ public async Task CreateFaceCaptureResourceAs { _logger.Debug($"Creating Face Capture resource in session '{sessionId}' for requirement '{createFaceCaptureResourcePayload.RequirementId}'"); - return await _docScanService.CreateFaceCaptureResource(_sdkId, _keyPair, sessionId, createFaceCaptureResourcePayload).ConfigureAwait(false); + return await _docScanService.CreateFaceCaptureResource(_authStrategy, sessionId, createFaceCaptureResourcePayload).ConfigureAwait(false); } /// @@ -216,7 +235,7 @@ public async Task UploadFaceCaptureImageAsync(string sessionId, string resourceI { _logger.Debug($"Uploading image to Face Capture resource '{resourceId}' for session '{sessionId}'"); - await _docScanService.UploadFaceCaptureImage(_sdkId, _keyPair, sessionId, resourceId, uploadFaceCaptureImagePayload).ConfigureAwait(false); + await _docScanService.UploadFaceCaptureImage(_authStrategy, sessionId, resourceId, uploadFaceCaptureImagePayload).ConfigureAwait(false); } /// @@ -238,7 +257,7 @@ public async Task GetSessionConfigurationAsync(str { _logger.Debug($"Getting configuration for session '{sessionId}'"); - return await _docScanService.GetSessionConfiguration(_sdkId, _keyPair, sessionId).ConfigureAwait(false); + return await _docScanService.GetSessionConfiguration(_authStrategy, sessionId).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/Yoti.Auth/DocScan/DocScanService.cs b/src/Yoti.Auth/DocScan/DocScanService.cs index fff51c41..e09f49f5 100644 --- a/src/Yoti.Auth/DocScan/DocScanService.cs +++ b/src/Yoti.Auth/DocScan/DocScanService.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; -using Org.BouncyCastle.Crypto; using Yoti.Auth.Constants; using Yoti.Auth.DocScan.Session.Create; using Yoti.Auth.DocScan.Session.Create.FaceCapture; @@ -27,336 +26,271 @@ public DocScanService(HttpClient httpClient, Uri apiUri) { _logger = NLog.LogManager.GetCurrentClassLogger(); _httpClient = httpClient; - - if (apiUri == null) - { - apiUri = GetApiUri(); - } - - ApiUri = apiUri; + ApiUri = apiUri ?? GetApiUri(); } - public async Task CreateSession(string sdkId, AsymmetricCipherKeyPair keyPair, SessionSpecification sessionSpec) + public async Task CreateSession(IAuthStrategy authStrategy, SessionSpecification sessionSpec) { - Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNull(sessionSpec, nameof(sessionSpec)); string serializedSessionSpec = JsonConvert.SerializeObject(sessionSpec, YotiDefaultJsonSettings); byte[] body = Encoding.UTF8.GetBytes(serializedSessionSpec); - Request createSessionRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) .WithHttpMethod(HttpMethod.Post) .WithBaseUri(ApiUri) .WithEndpoint("/sessions") - .WithQueryParam("sdkId", sdkId) .WithContent(body) - .WithContentHeader(Api.ContentTypeHeader, Api.ContentTypeJson) - .Build(); + .WithContentHeader(Api.ContentTypeHeader, Api.ContentTypeJson); + + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); - using (HttpResponseMessage response = await createSessionRequest.Execute(_httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - public async Task GetSession(string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) + public async Task GetSession(IAuthStrategy authStrategy, string sessionId) { - Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNull(sessionId, nameof(sessionId)); string sessionEndpoint = SessionEndpoint(sessionId); _logger.Info($"Fetching session from '{sessionEndpoint}'"); - Request sessionRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) .WithHttpMethod(HttpMethod.Get) .WithBaseUri(ApiUri) - .WithEndpoint(sessionEndpoint) - .WithQueryParam("sdkId", sdkId) - .Build(); + .WithEndpoint(sessionEndpoint); + + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); - using (HttpResponseMessage response = await sessionRequest.Execute(_httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - public async Task DeleteSession(string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) + public async Task DeleteSession(IAuthStrategy authStrategy, string sessionId) { - Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNull(sessionId, nameof(sessionId)); string sessionEndpoint = SessionEndpoint(sessionId); _logger.Info($"Deleting session at '{sessionEndpoint}'"); - Request deleteSessionRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) .WithHttpMethod(HttpMethod.Delete) .WithBaseUri(ApiUri) - .WithEndpoint(sessionEndpoint) - .WithQueryParam("sdkId", sdkId) - .Build(); + .WithEndpoint(sessionEndpoint); - using (HttpResponseMessage response = await deleteSessionRequest.Execute(_httpClient).ConfigureAwait(false)) + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); + + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } } } - public async Task GetMediaContent(string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId, string mediaId) + public async Task GetMediaContent(IAuthStrategy authStrategy, string sessionId, string mediaId) { - Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNull(sessionId, nameof(sessionId)); Validation.NotNull(mediaId, nameof(mediaId)); string mediaContentPath = MediaEndpoint(sessionId, mediaId); _logger.Info($"Fetching media from '{mediaContentPath}'"); - Request getMediaContentRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) .WithHttpMethod(HttpMethod.Get) .WithBaseUri(ApiUri) - .WithEndpoint(mediaContentPath) - .WithQueryParam("sdkId", sdkId) - .Build(); + .WithEndpoint(mediaContentPath); + + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); - using (HttpResponseMessage response = await getMediaContentRequest.Execute(_httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } - if (response.Content == null) - { + if (response.Content?.Headers?.ContentType == null) return null; - } - - if (response.Content.Headers.ContentType == null) - { - return null; - } - if (response.Content.Headers.ContentType == null) - { - return null; - } - string contentType = response.Content.Headers.ContentType.MediaType; - var responseObject = await response.Content.ReadAsByteArrayAsync(); - var deserialized = await Task.Factory.StartNew(() => new MediaValue(contentType, responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => new MediaValue(contentType, responseObject)); } } - public async Task DeleteMediaContent(string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId, string mediaId) + public async Task DeleteMediaContent(IAuthStrategy authStrategy, string sessionId, string mediaId) { - Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNull(sessionId, nameof(sessionId)); Validation.NotNull(mediaId, nameof(mediaId)); string mediaContentPath = MediaEndpoint(sessionId, mediaId); _logger.Info($"Deleting media at '{mediaContentPath}'"); - Request deleteMediaContentRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) .WithHttpMethod(HttpMethod.Delete) .WithBaseUri(ApiUri) - .WithEndpoint(mediaContentPath) - .WithQueryParam("sdkId", sdkId) - .Build(); + .WithEndpoint(mediaContentPath); + + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); - using (HttpResponseMessage response = await deleteMediaContentRequest.Execute(_httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } } } - public async Task GetSupportedDocuments(string sdkId, AsymmetricCipherKeyPair keyPair, bool isStrictlyLatin) + public async Task GetSupportedDocuments(IAuthStrategy authStrategy, bool isStrictlyLatin) { - Validation.NotNullOrEmpty(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); - _logger.Info($"Retrieving supported documents'"); + Validation.NotNull(authStrategy, nameof(authStrategy)); + _logger.Info("Retrieving supported documents"); - Request supportedDocumentsRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) .WithHttpMethod(HttpMethod.Get) .WithBaseUri(ApiUri) .WithEndpoint("/supported-documents") - .WithQueryParam("includeNonLatin", isStrictlyLatin ? "1": "0") - .WithQueryParam("sdkId", sdkId) - .Build(); + .WithQueryParam("includeNonLatin", isStrictlyLatin ? "1" : "0"); - using (HttpResponseMessage response = await supportedDocumentsRequest.Execute(_httpClient).ConfigureAwait(false)) + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); + + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - public async Task CreateFaceCaptureResource(string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId, CreateFaceCaptureResourcePayload createFaceCaptureResourcePayload) + public async Task CreateFaceCaptureResource(IAuthStrategy authStrategy, string sessionId, CreateFaceCaptureResourcePayload createFaceCaptureResourcePayload) { - Validation.NotNullOrWhiteSpace(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNullOrWhiteSpace(sessionId, nameof(sessionId)); Validation.NotNull(createFaceCaptureResourcePayload, nameof(createFaceCaptureResourcePayload)); - _logger.Info($"Creating new Face Capture resource"); + _logger.Info("Creating new Face Capture resource"); - string serializedFaceCaptureResourcePayload = JsonConvert.SerializeObject(createFaceCaptureResourcePayload, YotiDefaultJsonSettings); - byte[] body = Encoding.UTF8.GetBytes(serializedFaceCaptureResourcePayload); + string serializedPayload = JsonConvert.SerializeObject(createFaceCaptureResourcePayload, YotiDefaultJsonSettings); + byte[] body = Encoding.UTF8.GetBytes(serializedPayload); - Request createFaceCaptureRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) .WithHttpMethod(HttpMethod.Post) .WithBaseUri(ApiUri) .WithEndpoint($"/sessions/{sessionId}/resources/face-capture") - .WithQueryParam("sdkId", sdkId) .WithContent(body) - .WithContentHeader(Api.ContentTypeHeader, Api.ContentTypeJson) - .Build(); + .WithContentHeader(Api.ContentTypeHeader, Api.ContentTypeJson); - using (HttpResponseMessage response = await createFaceCaptureRequest.Execute(_httpClient).ConfigureAwait(false)) + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); + + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } - var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; - + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } - public async Task UploadFaceCaptureImage(string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId, string resourceId, UploadFaceCaptureImagePayload uploadFaceCaptureImagePayload) + public async Task UploadFaceCaptureImage(IAuthStrategy authStrategy, string sessionId, string resourceId, UploadFaceCaptureImagePayload uploadFaceCaptureImagePayload) { - Validation.NotNullOrWhiteSpace(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNullOrWhiteSpace(sessionId, nameof(sessionId)); Validation.NotNullOrWhiteSpace(resourceId, nameof(resourceId)); Validation.NotNull(uploadFaceCaptureImagePayload, nameof(uploadFaceCaptureImagePayload)); - _logger.Info($"Uploading image to Face Capture resource"); - - Request uploadFaceCaptureImageRequest = GetSignedRequestBuilder() - .WithMultipartBoundary(DocScanConstants.MultiPartBoundary) - .WithMultipartBinaryContent(DocScanConstants.UploadFaceCaptureImageBinaryContentName, - uploadFaceCaptureImagePayload.ImageContents, - uploadFaceCaptureImagePayload.ImageContentType, - DocScanConstants.UploadFaceCaptureImageFileName) - .WithKeyPair(keyPair) - .WithHttpMethod(HttpMethod.Put) - .WithBaseUri(ApiUri) - .WithEndpoint($"/sessions/{sessionId}/resources/face-capture/{resourceId}/image") - .WithQueryParam("sdkId", sdkId) - .Build(); - - using (HttpResponseMessage response = await uploadFaceCaptureImageRequest.Execute(_httpClient).ConfigureAwait(false)) + _logger.Info("Uploading image to Face Capture resource"); + + var builder = GetRequestBuilder() + .WithMultipartBoundary(DocScanConstants.MultiPartBoundary) + .WithMultipartBinaryContent( + DocScanConstants.UploadFaceCaptureImageBinaryContentName, + uploadFaceCaptureImagePayload.ImageContents, + uploadFaceCaptureImagePayload.ImageContentType, + DocScanConstants.UploadFaceCaptureImageFileName) + .WithAuthStrategy(authStrategy) + .WithHttpMethod(HttpMethod.Put) + .WithBaseUri(ApiUri) + .WithEndpoint($"/sessions/{sessionId}/resources/face-capture/{resourceId}/image"); + + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); + + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } } } - public async Task GetSessionConfiguration(string sdkId, AsymmetricCipherKeyPair keyPair, string sessionId) + public async Task GetSessionConfiguration(IAuthStrategy authStrategy, string sessionId) { - Validation.NotNullOrWhiteSpace(sdkId, nameof(sdkId)); - Validation.NotNull(keyPair, nameof(keyPair)); + Validation.NotNull(authStrategy, nameof(authStrategy)); Validation.NotNullOrWhiteSpace(sessionId, nameof(sessionId)); - _logger.Info($"Getting Session Configuration"); + _logger.Info("Getting Session Configuration"); - Request getSessionConfigurationRequest = GetSignedRequestBuilder() - .WithKeyPair(keyPair) - .WithHttpMethod(HttpMethod.Get) - .WithBaseUri(ApiUri) - .WithEndpoint($"/sessions/{sessionId}/configuration") - .WithQueryParam("sdkId", sdkId) - .Build(); + var builder = GetRequestBuilder() + .WithAuthStrategy(authStrategy) + .WithHttpMethod(HttpMethod.Get) + .WithBaseUri(ApiUri) + .WithEndpoint($"/sessions/{sessionId}/configuration"); + + if (authStrategy.SdkId != null) + builder = builder.WithQueryParam("sdkId", authStrategy.SdkId); - using (HttpResponseMessage response = await getSessionConfigurationRequest.Execute(_httpClient).ConfigureAwait(false)) + using (HttpResponseMessage response = await builder.Build().Execute(_httpClient).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) - { Response.CreateYotiExceptionFromStatusCode(response); - } var responseObject = await response.Content.ReadAsStringAsync(); - var deserialized = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); - - return deserialized; + return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(responseObject)); } } private static Uri GetApiUri() { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("YOTI_DOC_SCAN_API_URL"))) - { - return new Uri(Environment.GetEnvironmentVariable("YOTI_DOC_SCAN_API_URL")); - } - else - { - return Api.DefaultYotiDocsUrl; - } + string envUrl = Environment.GetEnvironmentVariable("YOTI_DOC_SCAN_API_URL"); + return !string.IsNullOrEmpty(envUrl) ? new Uri(envUrl) : Api.DefaultYotiDocsUrl; } - public static RequestBuilder GetSignedRequestBuilder() - { - return new RequestBuilder(); - } + private static RequestBuilder GetRequestBuilder() => new RequestBuilder(); - private static string SessionEndpoint(string sessionId) - { - return $"sessions/{sessionId}"; - } + private static string SessionEndpoint(string sessionId) => $"sessions/{sessionId}"; - private static string MediaEndpoint(string sessionId, string mediaId) - { - return $"sessions/{sessionId}/media/{mediaId}/content"; - } + private static string MediaEndpoint(string sessionId, string mediaId) => $"sessions/{sessionId}/media/{mediaId}/content"; private static JsonSerializerSettings YotiDefaultJsonSettings => new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; } diff --git a/src/Yoti.Auth/Web/BearerTokenAuthStrategy.cs b/src/Yoti.Auth/Web/BearerTokenAuthStrategy.cs new file mode 100644 index 00000000..a27823e7 --- /dev/null +++ b/src/Yoti.Auth/Web/BearerTokenAuthStrategy.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Yoti.Auth.Constants; + +namespace Yoti.Auth.Web +{ + public class BearerTokenAuthStrategy : IAuthStrategy + { + private readonly string _token; + + public string SdkId => null; + + public BearerTokenAuthStrategy(string token) + { + if (string.IsNullOrWhiteSpace(token)) + throw new ArgumentException("Authentication token must not be null, empty, or whitespace.", nameof(token)); + + _token = token; + } + + public IDictionary CreateAuthHeaders(HttpMethod httpMethod, string endpoint, byte[] body) + { + return new Dictionary + { + [Api.AuthorizationHeader] = "Bearer " + _token + }; + } + + public IDictionary CreateQueryParams() + { + return new Dictionary(); + } + } +} diff --git a/src/Yoti.Auth/Web/HeadersFactory.cs b/src/Yoti.Auth/Web/HeadersFactory.cs index a09c1be9..51a68402 100644 --- a/src/Yoti.Auth/Web/HeadersFactory.cs +++ b/src/Yoti.Auth/Web/HeadersFactory.cs @@ -14,6 +14,22 @@ internal static HttpRequestMessage AddHeaders(HttpRequestMessage httpRequestMess return PutHeaders(httpRequestMessage, authDigest, SDKVersion); } + internal static HttpRequestMessage AddStrategyHeaders(HttpRequestMessage httpRequestMessage, IAuthStrategy authStrategy, HttpMethod httpMethod, string endpoint, byte[] httpContent) + { + string SDKVersion = typeof(YotiClientEngine).GetTypeInfo().Assembly.GetName().Version.ToString(); + + var authHeaders = authStrategy.CreateAuthHeaders(httpMethod, endpoint, httpContent); + foreach (var header in authHeaders) + { + httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + httpRequestMessage.Headers.Add(Constants.Api.YotiSdkHeader, Constants.Api.SdkIdentifier); + httpRequestMessage.Headers.Add(Constants.Api.YotiSdkVersionHeader, $"{Constants.Api.SdkIdentifier}-{SDKVersion}"); + + return httpRequestMessage; + } + internal static HttpRequestMessage PutHeaders(HttpRequestMessage httpRequestMessage, string authDigest, string SDKVersion) { httpRequestMessage.Headers.Add(Constants.Api.DigestHeader, authDigest); diff --git a/src/Yoti.Auth/Web/IAuthStrategy.cs b/src/Yoti.Auth/Web/IAuthStrategy.cs new file mode 100644 index 00000000..197f18c0 --- /dev/null +++ b/src/Yoti.Auth/Web/IAuthStrategy.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Net.Http; + +namespace Yoti.Auth.Web +{ + public interface IAuthStrategy + { + /// + /// Returns auth-specific headers (e.g. Authorization: Bearer, or X-Yoti-Auth-Digest). + /// + IDictionary CreateAuthHeaders(HttpMethod httpMethod, string endpoint, byte[] body); + + /// + /// Returns auth-specific query params (e.g. nonce + timestamp for signed requests; empty for bearer). + /// + IDictionary CreateQueryParams(); + + /// + /// The SDK ID associated with this strategy, or null when not applicable (e.g. bearer token auth). + /// Services use this to decide whether to include sdkId in headers and query parameters. + /// + string SdkId { get; } + } +} diff --git a/src/Yoti.Auth/Web/NoAuthStrategy.cs b/src/Yoti.Auth/Web/NoAuthStrategy.cs new file mode 100644 index 00000000..d2486bba --- /dev/null +++ b/src/Yoti.Auth/Web/NoAuthStrategy.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Net.Http; + +namespace Yoti.Auth.Web +{ + public class NoAuthStrategy : IAuthStrategy + { + public string SdkId => null; + + public IDictionary CreateAuthHeaders(HttpMethod httpMethod, string endpoint, byte[] body) + { + return new Dictionary(); + } + + public IDictionary CreateQueryParams() + { + return new Dictionary(); + } + } +} diff --git a/src/Yoti.Auth/Web/RequestBuilder.cs b/src/Yoti.Auth/Web/RequestBuilder.cs index bb7da5aa..7a1d82f3 100644 --- a/src/Yoti.Auth/Web/RequestBuilder.cs +++ b/src/Yoti.Auth/Web/RequestBuilder.cs @@ -16,6 +16,7 @@ public class RequestBuilder private UriBuilder _baseUriBuilder; private string _endpoint; private AsymmetricCipherKeyPair _keyPair; + private IAuthStrategy _authStrategy; private HttpMethod _httpMethod; private byte[] _content; private MultipartFormDataContent _multipartFormDataContent; @@ -91,6 +92,19 @@ public RequestBuilder WithKeyPair(AsymmetricCipherKeyPair keyPair) return this; } + /// + /// Sets an for authentication. Mutually exclusive with + /// — use one or the other. + /// + /// + /// + public RequestBuilder WithAuthStrategy(IAuthStrategy strategy) + { + Validation.NotNull(strategy, nameof(strategy)); + _authStrategy = strategy; + return this; + } + /// /// Adds a custom header to the request. See @@ -199,9 +213,14 @@ public Request Build() { Validation.NotNull(_baseUriBuilder, nameof(_baseUriBuilder)); Validation.NotNullOrWhiteSpace(_endpoint, nameof(_endpoint)); - Validation.NotNull(_keyPair, nameof(_keyPair)); Validation.NotNull(_httpMethod, nameof(_httpMethod)); + if (_authStrategy == null && _keyPair == null) + throw new InvalidOperationException("Either WithAuthStrategy or WithKeyPair must be called before Build()."); + + if (_authStrategy != null && _keyPair != null) + throw new InvalidOperationException("WithAuthStrategy and WithKeyPair are mutually exclusive."); + if (!_baseUriBuilder.Path.EndsWith("/", StringComparison.Ordinal)) _baseUriBuilder.Path += "/"; @@ -227,12 +246,24 @@ public Request Build() contentForHeaderCreation = _multipartFormDataContent.ReadAsByteArrayAsync().Result; } - httpRequestMessage = HeadersFactory.AddHeaders( - httpRequestMessage, - _keyPair, - _httpMethod, - endpointWithParameters, - contentForHeaderCreation); + if (_authStrategy != null) + { + httpRequestMessage = HeadersFactory.AddStrategyHeaders( + httpRequestMessage, + _authStrategy, + _httpMethod, + endpointWithParameters, + contentForHeaderCreation); + } + else + { + httpRequestMessage = HeadersFactory.AddHeaders( + httpRequestMessage, + _keyPair, + _httpMethod, + endpointWithParameters, + contentForHeaderCreation); + } AddCustomHeaders(httpRequestMessage); AddCustomContentHeaders(httpRequestMessage); @@ -278,6 +309,17 @@ private string AddQueryParametersToEndpoint() endpointBuilder.Append($"{param.Key}={param.Value}&"); } } + + if (_authStrategy != null) + { + var authParams = _authStrategy.CreateQueryParams(); + foreach (var param in authParams) + { + endpointBuilder.Append($"{param.Key}={param.Value}&"); + } + return endpointBuilder.ToString().TrimEnd('&').TrimEnd('?'); + } + endpointBuilder.Append($"timestamp={GetTimestamp()}&nonce={CryptoEngine.GenerateNonce()}"); return endpointBuilder.ToString(); } diff --git a/src/Yoti.Auth/Web/SignedRequestAuthStrategy.cs b/src/Yoti.Auth/Web/SignedRequestAuthStrategy.cs new file mode 100644 index 00000000..37b1a624 --- /dev/null +++ b/src/Yoti.Auth/Web/SignedRequestAuthStrategy.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.Constants; + +namespace Yoti.Auth.Web +{ + public class SignedRequestAuthStrategy : IAuthStrategy + { + public AsymmetricCipherKeyPair KeyPair { get; } + public string SdkId { get; } + + public SignedRequestAuthStrategy(AsymmetricCipherKeyPair keyPair, string sdkId) + { + KeyPair = keyPair ?? throw new ArgumentNullException(nameof(keyPair)); + if (string.IsNullOrEmpty(sdkId)) + throw new ArgumentException("SDK ID must not be null or empty.", nameof(sdkId)); + SdkId = sdkId; + } + + public IDictionary CreateAuthHeaders(HttpMethod httpMethod, string endpoint, byte[] body) + { + string digest = SignedMessageFactory.SignMessage(httpMethod, endpoint, KeyPair, body); + return new Dictionary + { + [Api.DigestHeader] = digest + }; + } + + public IDictionary CreateQueryParams() + { + return new Dictionary + { + ["nonce"] = CryptoEngine.GenerateNonce(), + ["timestamp"] = GetTimestamp() + }; + } + + private static string GetTimestamp() + { + long ms = (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalMilliseconds; + return ms.ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/test/Yoti.Auth.Tests/CentralAuth/AuthenticationTokenGeneratorBuilderTests.cs b/test/Yoti.Auth.Tests/CentralAuth/AuthenticationTokenGeneratorBuilderTests.cs new file mode 100644 index 00000000..0c846a90 --- /dev/null +++ b/test/Yoti.Auth.Tests/CentralAuth/AuthenticationTokenGeneratorBuilderTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.CentralAuth; +using Yoti.Auth.Tests.Common; + +namespace Yoti.Auth.Tests.CentralAuth +{ + [TestClass] + public class AuthenticationTokenGeneratorBuilderTests + { + private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); + private const string _sdkId = "test-sdk-id"; + private const string _scope = "identity:create"; + + [TestMethod] + public void WithSdkId_NullShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder().WithSdkId(null)); + } + + [TestMethod] + public void WithSdkId_EmptyShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder().WithSdkId(string.Empty)); + } + + [TestMethod] + public void WithKey_NullKeyPairShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder().WithKey((AsymmetricCipherKeyPair)null)); + } + + [TestMethod] + public void WithKey_NullStreamReaderShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder().WithKey((StreamReader)null)); + } + + [TestMethod] + public void WithScopes_NullShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder().WithScopes(null)); + } + + [TestMethod] + public void WithAuthApiUrl_NullShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder().WithAuthApiUrl(null)); + } + + [TestMethod] + public void WithAuthApiUrl_EmptyShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder().WithAuthApiUrl(string.Empty)); + } + + [TestMethod] + public void Build_MissingSdkIdShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder() + .WithKey(_keyPair) + .WithScope(_scope) + .Build()); + } + + [TestMethod] + public void Build_MissingKeyPairShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder() + .WithSdkId(_sdkId) + .WithScope(_scope) + .Build()); + } + + [TestMethod] + public void Build_EmptyScopesShouldThrow() + { + Assert.ThrowsException(() => + new AuthenticationTokenGeneratorBuilder() + .WithSdkId(_sdkId) + .WithKey(_keyPair) + .Build()); + } + + [TestMethod] + public void Build_ValidConfigurationShouldSucceed() + { + var generator = new AuthenticationTokenGeneratorBuilder() + .WithSdkId(_sdkId) + .WithKey(_keyPair) + .WithScope(_scope) + .Build(); + + Assert.IsNotNull(generator); + } + + [TestMethod] + public void Build_WithMultipleScopesViaWithScopes() + { + var generator = new AuthenticationTokenGeneratorBuilder() + .WithSdkId(_sdkId) + .WithKey(_keyPair) + .WithScopes(new List { _scope, "identity:read" }) + .Build(); + + Assert.IsNotNull(generator); + } + + [TestMethod] + public void Build_WithKeyStreamShouldSucceed() + { + using (StreamReader stream = KeyPair.GetValidKeyStream()) + { + var generator = new AuthenticationTokenGeneratorBuilder() + .WithSdkId(_sdkId) + .WithKey(stream) + .WithScope(_scope) + .Build(); + + Assert.IsNotNull(generator); + } + } + + [TestMethod] + public void Build_WithCustomAuthApiUrlShouldSucceed() + { + var generator = new AuthenticationTokenGeneratorBuilder() + .WithSdkId(_sdkId) + .WithKey(_keyPair) + .WithScope(_scope) + .WithAuthApiUrl("https://custom.auth.example.com/token") + .Build(); + + Assert.IsNotNull(generator); + } + } +} diff --git a/test/Yoti.Auth.Tests/CentralAuth/AuthenticationTokenGeneratorTests.cs b/test/Yoti.Auth.Tests/CentralAuth/AuthenticationTokenGeneratorTests.cs new file mode 100644 index 00000000..032fb260 --- /dev/null +++ b/test/Yoti.Auth.Tests/CentralAuth/AuthenticationTokenGeneratorTests.cs @@ -0,0 +1,123 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Moq.Protected; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.CentralAuth; +using Yoti.Auth.Tests.Common; + +namespace Yoti.Auth.Tests.CentralAuth +{ + [TestClass] + public class AuthenticationTokenGeneratorTests + { + private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); + private const string _sdkId = "test-sdk-id"; + private const string _scope = "identity:create"; + + private AuthenticationTokenGenerator BuildGenerator() + { + return new AuthenticationTokenGeneratorBuilder() + .WithSdkId(_sdkId) + .WithKey(_keyPair) + .WithScope(_scope) + .Build(); + } + + private static Mock SetupMockHandler(HttpStatusCode statusCode, string responseBody) + { + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = statusCode, + Content = new StringContent(responseBody) + }) + .Verifiable(); + return handlerMock; + } + + [TestMethod] + public async Task GetToken_SuccessfulResponseShouldReturnToken() + { + var handlerMock = SetupMockHandler( + HttpStatusCode.OK, + "{\"access_token\":\"abc123\",\"expires_in\":3600,\"token_type\":\"Bearer\",\"scope\":\"identity:create\"}"); + + var generator = BuildGenerator(); + var response = await generator.GetToken(new HttpClient(handlerMock.Object)); + + Assert.IsNotNull(response); + Assert.AreEqual("abc123", response.AccessToken); + Assert.AreEqual(3600, response.ExpiresIn); + Assert.AreEqual("Bearer", response.TokenType); + } + + [TestMethod] + public async Task GetToken_UnauthorizedShouldThrow() + { + var handlerMock = SetupMockHandler(HttpStatusCode.Unauthorized, "Unauthorized"); + + var generator = BuildGenerator(); + await Assert.ThrowsExceptionAsync(() => + generator.GetToken(new HttpClient(handlerMock.Object))); + } + + [TestMethod] + public async Task GetToken_ServerErrorShouldThrow() + { + var handlerMock = SetupMockHandler(HttpStatusCode.InternalServerError, "Server Error"); + + var generator = BuildGenerator(); + await Assert.ThrowsExceptionAsync(() => + generator.GetToken(new HttpClient(handlerMock.Object))); + } + + [TestMethod] + public async Task GetToken_NullBodyShouldThrow() + { + var handlerMock = SetupMockHandler(HttpStatusCode.OK, "null"); + + var generator = BuildGenerator(); + await Assert.ThrowsExceptionAsync(() => + generator.GetToken(new HttpClient(handlerMock.Object))); + } + + [TestMethod] + public async Task GetToken_RequestShouldContainClientAssertionType() + { + HttpRequestMessage capturedRequest = null; + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"access_token\":\"t\",\"expires_in\":3600,\"token_type\":\"Bearer\",\"scope\":\"s\"}") + }) + .Callback((req, _) => capturedRequest = req) + .Verifiable(); + + var generator = BuildGenerator(); + await generator.GetToken(new HttpClient(handlerMock.Object)); + + Assert.IsNotNull(capturedRequest); + string body = await capturedRequest.Content.ReadAsStringAsync(); + Assert.IsTrue(body.Contains("client_assertion_type")); + Assert.IsTrue(body.Contains("grant_type=client_credentials")); + } + } +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs index 0e43838f..7f52209e 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentity/DigitalIdentityServiceTests.cs @@ -1,12 +1,11 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; using Microsoft.VisualStudio.TestTools.UnitTesting; using Org.BouncyCastle.Crypto; using Yoti.Auth.DigitalIdentity; using Yoti.Auth.Tests.Common; -using static System.Net.Mime.MediaTypeNames; +using Yoti.Auth.Web; namespace Yoti.Auth.Tests.DigitalIdentity { @@ -21,6 +20,7 @@ public class DigitalIdentityServiceTests private ShareSessionRequest _someShareSessionRequest; private const string _sessionID = "someSessionID"; private QrRequest _someCreateQrRequest; + private IAuthStrategy _authStrategy; [TestInitialize] public void Startup() @@ -28,6 +28,7 @@ public void Startup() _someHeaders.Add("Key", "Value"); _someCreateQrRequest = TestTools.CreateQr.CreateQrStandard(); _someShareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); + _authStrategy = new SignedRequestAuthStrategy(_keyPair, _sdkID); } [TestMethod] @@ -35,7 +36,7 @@ public void ShouldFailWithNullHttpClient() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateShareSession(null, _apiURL, _sdkID, _keyPair, _someShareSessionRequest).Wait(); + DigitalIdentityService.CreateShareSession(null, _apiURL, _authStrategy, _someShareSessionRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -47,7 +48,7 @@ public void ShouldFailWithNullApiUrl() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateShareSession(_httpClient, null, _sdkID, _keyPair, _someShareSessionRequest).Wait(); + DigitalIdentityService.CreateShareSession(_httpClient, null, _authStrategy, _someShareSessionRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -55,27 +56,15 @@ public void ShouldFailWithNullApiUrl() } [TestMethod] - public void ShouldFailWithNullSdkId() + public void ShouldFailWithNullAuthStrategy() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, null, _keyPair, _someShareSessionRequest).Wait(); + DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, null, _someShareSessionRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("sdkId")); - } - - [TestMethod] - public void ShouldFailWithNullKeyPair() - { - var aggregateException = Assert.ThrowsException(() => - { - DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, _sdkID, null, _someShareSessionRequest).Wait(); - }); - - Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("authStrategy")); } [TestMethod] @@ -83,7 +72,7 @@ public void ShouldFailWithNullDynamicScenario() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, _sdkID, _keyPair, null).Wait(); + DigitalIdentityService.CreateShareSession(_httpClient, _apiURL, _authStrategy, null).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -91,25 +80,15 @@ public void ShouldFailWithNullDynamicScenario() } [TestMethod] - public void RetrieveSessionShouldThrowExceptionForMissingSdkId() + public void RetrieveSessionShouldThrowExceptionForNullAuthStrategy() { - var exception = Assert.ThrowsExceptionAsync(async () => + var aggregateException = Assert.ThrowsException(() => { - await DigitalIdentityService.GetSession(_httpClient, _apiURL, null, _keyPair, _sessionID); + DigitalIdentityService.GetSession(_httpClient, _apiURL, null, _sessionID).Wait(); }); - Assert.IsTrue(exception.Exception.InnerException.Message.Contains("sdkId")); - } - - [TestMethod] - public void RetrieveSessionShouldThrowExceptionForMissingKeyPair() - { - var exception = Assert.ThrowsExceptionAsync(async () => - { - await DigitalIdentityService.GetSession(_httpClient, _apiURL, _sdkID, null, _sessionID); - }).Result; - - Assert.IsTrue(exception.Message.Contains("keyPair")); + Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("authStrategy")); } [TestMethod] @@ -117,7 +96,7 @@ public void RetrieveSessionShouldThrowExceptionForMissingSessionId() { var exception = Assert.ThrowsExceptionAsync(async () => { - await DigitalIdentityService.GetSession(_httpClient, _apiURL, _sdkID, _keyPair, null); + await DigitalIdentityService.GetSession(_httpClient, _apiURL, _authStrategy, null); }).Result; Assert.IsTrue(exception.Message.Contains("sessionId")); @@ -128,7 +107,7 @@ public void CreateQrCodeShouldFailWithNullHttpClient() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(null, _apiURL, _sdkID, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.CreateQrCode(null, _apiURL, _authStrategy, _sessionID, _someCreateQrRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -140,7 +119,7 @@ public void CreateQrCodeShouldFailWithNullApiUrl() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(_httpClient, null, _sdkID, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.CreateQrCode(_httpClient, null, _authStrategy, _sessionID, _someCreateQrRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -148,49 +127,27 @@ public void CreateQrCodeShouldFailWithNullApiUrl() } [TestMethod] - public void CreateQrCodeShouldFailWithNullSdkId() + public void CreateQrCodeShouldFailWithNullAuthStrategy() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, null, _keyPair, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, null, _sessionID, _someCreateQrRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("sdkId")); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("authStrategy")); } [TestMethod] - public void CreateQrCodeShouldFailWithNullKeyPair() + public void RetrieveQrShouldThrowExceptionForNullAuthStrategy() { var aggregateException = Assert.ThrowsException(() => { - DigitalIdentityService.CreateQrCode(_httpClient, _apiURL, _sdkID, null, _sessionID, _someCreateQrRequest).Wait(); + DigitalIdentityService.GetQrCode(_httpClient, _apiURL, null, _sessionID).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); - } - - [TestMethod] - public void RetrieveQrShouldThrowExceptionForMissingSdkId() - { - var exception = Assert.ThrowsExceptionAsync(async () => - { - await DigitalIdentityService.GetQrCode(_httpClient, _apiURL, null, _keyPair, _sessionID); - }); - - Assert.IsTrue(exception.Exception.InnerException.Message.Contains("sdkId")); - } - - [TestMethod] - public void RetrieveQrCodeShouldThrowExceptionForMissingKeyPair() - { - var exception = Assert.ThrowsExceptionAsync(async () => - { - await DigitalIdentityService.GetQrCode(_httpClient, _apiURL, _sdkID, null, _sessionID); - }).Result; - - Assert.IsTrue(exception.Message.Contains("keyPair")); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("authStrategy")); } [TestMethod] @@ -198,11 +155,10 @@ public void RetrieveQrCodeShouldThrowExceptionForMissingSessionId() { var exception = Assert.ThrowsExceptionAsync(async () => { - await DigitalIdentityService.GetQrCode(_httpClient, _apiURL, _sdkID, _keyPair, null); + await DigitalIdentityService.GetQrCode(_httpClient, _apiURL, _authStrategy, null); }).Result; Assert.IsTrue(exception.Message.Contains("qrCodeId")); } - } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs index 8dddfe73..a2d0eb34 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientEngineTests.cs @@ -10,6 +10,7 @@ using Yoti.Auth.DigitalIdentity; using Yoti.Auth.Exceptions; using Yoti.Auth.Tests.Common; +using Yoti.Auth.Web; namespace Yoti.Auth.Tests { @@ -20,6 +21,13 @@ public class DigitalIdentityClientEngineTests private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); private static HttpRequestMessage _httpRequestMessage; private const string SdkId = "fake-sdk-id"; + private IAuthStrategy _authStrategy; + + [TestInitialize] + public void Setup() + { + _authStrategy = new SignedRequestAuthStrategy(_keyPair, SdkId); + } [TestMethod] public async Task CreateSessionAsyncShouldReturnCorrectValues() @@ -33,7 +41,7 @@ public async Task CreateSessionAsyncShouldReturnCorrectValues() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); - ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); + ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); Assert.IsNotNull(shareSessionResult); Assert.AreEqual(refId, shareSessionResult.Id); @@ -53,7 +61,7 @@ public async Task CreateQrCodeAsyncShouldReturnCorrectValues() QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); string sessionId = "test-session-id"; - CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); + CreateQrResult result = await engine.CreateQrCodeAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); Assert.IsNotNull(result); Assert.AreEqual(qrCodeId, result.Id); @@ -73,7 +81,7 @@ public async Task GetQrCodeAsyncShouldReturnCorrectValues() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - GetQrCodeResult result = await engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); + GetQrCodeResult result = await engine.GetQrCodeAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); Assert.IsNotNull(result); Assert.AreEqual(qrCodeId, result.Id); @@ -94,7 +102,7 @@ public async Task GetSessionShouldReturnCorrectValues() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - GetSessionResult result = await engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + GetSessionResult result = await engine.GetSession(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); Assert.IsNotNull(result); Assert.AreEqual(sessionId, result.Id); @@ -121,7 +129,7 @@ public void CreateShareSessionNonSuccessStatusCodesShouldThrowException(HttpStat var aggregateException = Assert.ThrowsException(() => { - engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiApiUrl), shareSessionRequest).Wait(); + engine.CreateShareSessionAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiApiUrl), shareSessionRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -146,7 +154,7 @@ public void GetShareReceiptNonSuccessStatusCodesShouldThrowException(HttpStatusC var aggregateException = Assert.ThrowsException(() => { - engine.GetShareReceipt(SdkId, _keyPair, apiUrl, receiptId).Wait(); + engine.GetShareReceipt(_authStrategy, apiUrl, receiptId).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -171,7 +179,7 @@ public void CreateQrCodeAsyncNonSuccessStatusCodesShouldThrowException(HttpStatu var aggregateException = Assert.ThrowsException(() => { - engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest).Wait(); + engine.CreateQrCodeAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -195,7 +203,7 @@ public void GetQrCodeAsyncNonSuccessStatusCodesShouldThrowException(HttpStatusCo var aggregateException = Assert.ThrowsException(() => { - engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId).Wait(); + engine.GetQrCodeAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -219,7 +227,7 @@ public void GetSessionNonSuccessStatusCodesShouldThrowException(HttpStatusCode h var aggregateException = Assert.ThrowsException(() => { - engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId).Wait(); + engine.GetSession(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -264,7 +272,7 @@ public async Task GetShareReceiptShouldThrowWhenReceiptIdIsEmpty() Uri apiUrl = new Uri("https://example.com/api"); await Assert.ThrowsExceptionAsync(() => - engine.GetShareReceipt(SdkId, _keyPair, apiUrl, "")); + engine.GetShareReceipt(_authStrategy, apiUrl, "")); } [TestMethod] @@ -277,7 +285,7 @@ public async Task CreateShareSessionAsyncShouldHandleEmptyResponse() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); - ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); + ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); Assert.IsNotNull(shareSessionResult); } @@ -293,7 +301,7 @@ public async Task CreateQrCodeAsyncShouldHandleEmptyResponse() QrRequest qrRequest = TestTools.CreateQr.CreateQrStandard(); string sessionId = "test-session-id"; - CreateQrResult result = await engine.CreateQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); + CreateQrResult result = await engine.CreateQrCodeAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId, qrRequest); Assert.IsNotNull(result); } @@ -309,7 +317,7 @@ public async Task GetQrCodeAsyncShouldHandleEmptyResponse() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - GetQrCodeResult result = await engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); + GetQrCodeResult result = await engine.GetQrCodeAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); Assert.IsNotNull(result); } @@ -325,7 +333,7 @@ public async Task GetSessionShouldHandleEmptyResponse() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - GetSessionResult result = await engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + GetSessionResult result = await engine.GetSession(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); Assert.IsNotNull(result); } @@ -342,7 +350,7 @@ public async Task CreateShareSessionAsyncShouldHandleSpecialCharactersInId() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); ShareSessionRequest shareSessionRequest = TestTools.ShareSession.CreateStandardShareSessionRequest(); - ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); + ShareSessionResult shareSessionResult = await engine.CreateShareSessionAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), shareSessionRequest); Assert.IsNotNull(shareSessionResult); Assert.AreEqual(refId, shareSessionResult.Id); @@ -359,7 +367,7 @@ public async Task GetQrCodeAsyncShouldHandleNullSessionInResponse() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - GetQrCodeResult result = await engine.GetQrCodeAsync(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); + GetQrCodeResult result = await engine.GetQrCodeAsync(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), qrCodeId); Assert.IsNotNull(result); Assert.AreEqual(qrCodeId, result.Id); @@ -377,7 +385,7 @@ public async Task GetSessionShouldHandleNullQrCodeAndReceiptInResponse() var engine = new DigitalIdentityClientEngine(new HttpClient(handlerMock.Object)); - GetSessionResult result = await engine.GetSession(SdkId, _keyPair, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); + GetSessionResult result = await engine.GetSession(_authStrategy, new Uri(Constants.Api.DefaultYotiShareApiUrl), sessionId); Assert.IsNotNull(result); Assert.AreEqual(sessionId, result.Id); diff --git a/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs index 805165de..5c5b8b6e 100644 --- a/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs +++ b/test/Yoti.Auth.Tests/DigitalIdentityClientTests.cs @@ -118,6 +118,62 @@ public void ApiUriEnvVariableIsUsed() Uri expectedApiUri = new Uri("https://envapiuri.com"); Assert.AreEqual(expectedApiUri, client.ApiUri); } + [TestMethod] + public void FromBearerToken_NullTokenShouldThrow() + { + Assert.ThrowsException(() => + DigitalIdentityClient.FromBearerToken(null)); + } + + [TestMethod] + public void FromBearerToken_EmptyTokenShouldThrow() + { + Assert.ThrowsException(() => + DigitalIdentityClient.FromBearerToken(string.Empty)); + } + + [TestMethod] + public void FromBearerToken_ValidTokenShouldCreateClient() + { + var client = DigitalIdentityClient.FromBearerToken("my-token"); + Assert.IsNotNull(client); + Assert.AreEqual(_expectedDefaultUri, client.ApiUri); + } + + [TestMethod] + public void FromBearerToken_WithHttpClient_NullTokenShouldThrow() + { + Assert.ThrowsException(() => + DigitalIdentityClient.FromBearerToken(new HttpClient(), null)); + } + + [TestMethod] + public void FromBearerToken_WithHttpClient_ValidTokenShouldCreateClient() + { + var client = DigitalIdentityClient.FromBearerToken(new HttpClient(), "my-token"); + Assert.IsNotNull(client); + Assert.AreEqual(_expectedDefaultUri, client.ApiUri); + } + + [TestMethod] + public void FromBearerToken_GetShareReceiptShouldThrowInvalidOperation() + { + var client = DigitalIdentityClient.FromBearerToken("my-token"); + var ex = Assert.ThrowsException(() => + client.GetShareReceipt("some-receipt-id")); + + bool hasExpectedInner = false; + foreach (var inner in ex.InnerExceptions) + { + if (inner is InvalidOperationException ioe && ioe.Message.Contains("signed-request")) + { + hasExpectedInner = true; + break; + } + } + Assert.IsTrue(hasExpectedInner, "Expected InvalidOperationException about signed-request strategy."); + } + private static DigitalIdentityClient CreateDigitalIdentityClient() { StreamReader privateStreamKey = KeyPair.GetValidKeyStream(); diff --git a/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs b/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs index 243aec81..43a758ed 100644 --- a/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs +++ b/test/Yoti.Auth.Tests/DocScan/DocScanClientTests.cs @@ -64,6 +64,34 @@ public void ShouldFailForNullSdkId() Assert.IsTrue(exception.Message.Contains("sdkId")); } + [TestMethod] + public void FromBearerToken_NullTokenShouldThrow() + { + Assert.ThrowsException(() => + DocScanClient.FromBearerToken(null)); + } + + [TestMethod] + public void FromBearerToken_EmptyTokenShouldThrow() + { + Assert.ThrowsException(() => + DocScanClient.FromBearerToken(string.Empty)); + } + + [TestMethod] + public void FromBearerToken_ValidTokenShouldCreateClient() + { + var client = DocScanClient.FromBearerToken("my-bearer-token"); + Assert.IsNotNull(client); + } + + [TestMethod] + public void FromBearerToken_WithHttpClient_ValidTokenShouldCreateClient() + { + var client = DocScanClient.FromBearerToken("my-bearer-token", new HttpClient()); + Assert.IsNotNull(client); + } + [TestMethod] public void ShouldFailForNullKeyPair() { diff --git a/test/Yoti.Auth.Tests/DocScan/DocScanServiceTests.cs b/test/Yoti.Auth.Tests/DocScan/DocScanServiceTests.cs index de3adf21..abaf8d0d 100644 --- a/test/Yoti.Auth.Tests/DocScan/DocScanServiceTests.cs +++ b/test/Yoti.Auth.Tests/DocScan/DocScanServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -20,6 +20,7 @@ public class DocScanServiceTests private const string _someMediaId = "someMediaId"; private AsymmetricCipherKeyPair _keyPair; + private IAuthStrategy _authStrategy; private DocScanService _docScanService; private CreateFaceCaptureResourcePayload _createFaceCaptureResourcePayload; private string _someResourceId = "someResourceId"; @@ -29,6 +30,7 @@ public class DocScanServiceTests public void Startup() { _keyPair = Tests.Common.KeyPair.Get(); + _authStrategy = new SignedRequestAuthStrategy(_keyPair, _sdkId); _docScanService = new DocScanService(new HttpClient(), apiUri: null); _createFaceCaptureResourcePayload = new CreateFaceCaptureResourcePayload("someRequirementId"); _uploadFaceCaptureImagePayload = new UploadFaceCaptureImagePayload(DocScanConstants.MimeTypePng, new byte[] { 0x00, 0x21, 0x60, 0x1F, 0xA1 }); @@ -68,25 +70,14 @@ public void ApiUriEnvVariableIsUsed() } [TestMethod] - public void CreateSessionShouldThrowExceptionForMissingSdkId() - { - var exception = Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.CreateSession(null, _keyPair, new SessionSpecificationBuilder().Build()); - }).Result; - - Assert.IsTrue(exception.Message.Contains("sdkId")); - } - - [TestMethod] - public void CreateSessionShouldThrowExceptionForMissingKeyPair() + public void CreateSessionShouldThrowExceptionForNullAuthStrategy() { var exception = Assert.ThrowsExceptionAsync(async () => { - await _docScanService.CreateSession(_sdkId, null, new SessionSpecificationBuilder().Build()); + await _docScanService.CreateSession(null, new SessionSpecificationBuilder().Build()); }).Result; - Assert.IsTrue(exception.Message.Contains("keyPair")); + Assert.IsTrue(exception.Message.Contains("authStrategy")); } [TestMethod] @@ -94,32 +85,21 @@ public void CreateSessionShouldThrowExceptionForMissingSessionSpec() { var exception = Assert.ThrowsExceptionAsync(async () => { - await _docScanService.CreateSession(_sdkId, _keyPair, null); + await _docScanService.CreateSession(_authStrategy, null); }).Result; Assert.IsTrue(exception.Message.Contains("sessionSpec")); } [TestMethod] - public void RetrieveSessionShouldThrowExceptionForMissingSdkId() - { - var exception = Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.GetSession(null, _keyPair, _someSessionId); - }).Result; - - Assert.IsTrue(exception.Message.Contains("sdkId")); - } - - [TestMethod] - public void RetrieveSessionShouldThrowExceptionForMissingKeyPair() + public void RetrieveSessionShouldThrowExceptionForNullAuthStrategy() { var exception = Assert.ThrowsExceptionAsync(async () => { - await _docScanService.GetSession(_sdkId, null, _someSessionId); + await _docScanService.GetSession(null, _someSessionId); }).Result; - Assert.IsTrue(exception.Message.Contains("keyPair")); + Assert.IsTrue(exception.Message.Contains("authStrategy")); } [TestMethod] @@ -127,34 +107,22 @@ public void RetrieveSessionShouldThrowExceptionForMissingSessionId() { var exception = Assert.ThrowsExceptionAsync(async () => { - await _docScanService.GetSession(_sdkId, _keyPair, null); + await _docScanService.GetSession(_authStrategy, null); }).Result; Assert.IsTrue(exception.Message.Contains("sessionId")); } [TestMethod] - public void DeleteSessionShouldThrowExceptionForMissingSdkId() + public void DeleteSessionShouldThrowExceptionForNullAuthStrategy() { var aggregateException = Assert.ThrowsException(() => { - _docScanService.DeleteSession(null, _keyPair, _someSessionId).Wait(); - }); - - Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("sdkId")); - } - - [TestMethod] - public void DeleteSessionShouldThrowExceptionForMissingKeyPair() - { - var aggregateException = Assert.ThrowsException(() => - { - _docScanService.DeleteSession(_sdkId, null, _someSessionId).Wait(); + _docScanService.DeleteSession(null, _someSessionId).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("authStrategy")); } [TestMethod] @@ -162,7 +130,7 @@ public void DeleteSessionShouldThrowExceptionForMissingSessionId() { var aggregateException = Assert.ThrowsException(() => { - _docScanService.DeleteSession(_sdkId, _keyPair, null).Wait(); + _docScanService.DeleteSession(_authStrategy, null).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -170,25 +138,14 @@ public void DeleteSessionShouldThrowExceptionForMissingSessionId() } [TestMethod] - public void GetMediaContentShouldThrowExceptionForMissingSdkId() - { - var exception = Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.GetMediaContent(null, _keyPair, _someSessionId, _someMediaId); - }).Result; - - Assert.IsTrue(exception.Message.Contains("sdkId")); - } - - [TestMethod] - public void GetMediaContentShouldThrowExceptionForMissingKeyPair() + public void GetMediaContentShouldThrowExceptionForNullAuthStrategy() { var exception = Assert.ThrowsExceptionAsync(async () => { - await _docScanService.GetMediaContent(_sdkId, null, _someSessionId, _someMediaId); + await _docScanService.GetMediaContent(null, _someSessionId, _someMediaId); }).Result; - Assert.IsTrue(exception.Message.Contains("keyPair")); + Assert.IsTrue(exception.Message.Contains("authStrategy")); } [TestMethod] @@ -196,7 +153,7 @@ public void GetMediaContentShouldThrowExceptionForMissingSessionId() { var exception = Assert.ThrowsExceptionAsync(async () => { - await _docScanService.GetMediaContent(_sdkId, _keyPair, null, _someMediaId); + await _docScanService.GetMediaContent(_authStrategy, null, _someMediaId); }).Result; Assert.IsTrue(exception.Message.Contains("sessionId")); @@ -207,34 +164,22 @@ public void GetMediaContentShouldThrowExceptionForMissingMediaId() { var exception = Assert.ThrowsExceptionAsync(async () => { - await _docScanService.GetMediaContent(_sdkId, _keyPair, _someSessionId, null); + await _docScanService.GetMediaContent(_authStrategy, _someSessionId, null); }).Result; Assert.IsTrue(exception.Message.Contains("mediaId")); } [TestMethod] - public void DeleteMediaContentShouldThrowExceptionForMissingSdkId() + public void DeleteMediaContentShouldThrowExceptionForNullAuthStrategy() { var aggregateException = Assert.ThrowsException(() => { - _docScanService.DeleteMediaContent(null, _keyPair, _someSessionId, _someMediaId).Wait(); - }); - - Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("sdkId")); - } - - [TestMethod] - public void DeleteMediaContentShouldThrowExceptionForMissingKeyPair() - { - var aggregateException = Assert.ThrowsException(() => - { - _docScanService.DeleteMediaContent(_sdkId, null, _someSessionId, _someMediaId).Wait(); + _docScanService.DeleteMediaContent(null, _someSessionId, _someMediaId).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); - Assert.IsTrue(aggregateException.InnerException.Message.Contains("keyPair")); + Assert.IsTrue(aggregateException.InnerException.Message.Contains("authStrategy")); } [TestMethod] @@ -242,7 +187,7 @@ public void DeleteMediaContentShouldThrowExceptionForMissingSessionId() { var aggregateException = Assert.ThrowsException(() => { - _docScanService.DeleteMediaContent(_sdkId, _keyPair, null, _someMediaId).Wait(); + _docScanService.DeleteMediaContent(_authStrategy, null, _someMediaId).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); @@ -254,48 +199,13 @@ public void DeleteMediaContentShouldThrowExceptionForMissingMediaId() { var aggregateException = Assert.ThrowsException(() => { - _docScanService.DeleteMediaContent(_sdkId, _keyPair, _someSessionId, null).Wait(); + _docScanService.DeleteMediaContent(_authStrategy, _someSessionId, null).Wait(); }); Assert.IsTrue(TestTools.Exceptions.IsExceptionInAggregateException(aggregateException)); Assert.IsTrue(aggregateException.InnerException.Message.Contains("mediaId")); } - [TestMethod] - public void GetSignedRequestBuilderShouldReturnNewInstance() - { - RequestBuilder requestBuilder1 = DocScanService.GetSignedRequestBuilder(); - RequestBuilder requestBuilder2 = DocScanService.GetSignedRequestBuilder(); - - Assert.AreNotSame(requestBuilder1, requestBuilder2); - } - - [DataTestMethod] - [DataRow(null)] - [DataRow("")] - [DataRow(" ")] - [DataRow(" ")] - public async Task CreateFaceCaptureResourceShouldThrowExceptionWhenSdkIdIsNullEmptyOrWhitespace(string sdkId) - { - var exception = await Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.CreateFaceCaptureResource(sdkId, _keyPair, _someSessionId, _createFaceCaptureResourcePayload); - }); - - Assert.IsTrue(exception.Message.Contains(nameof(sdkId))); - } - - [TestMethod] - public async Task CreateFaceCaptureResourceShouldThrowExceptionWhenKeyPairIsNull() - { - var exception = await Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.CreateFaceCaptureResource(_sdkId, null, _someSessionId, _createFaceCaptureResourcePayload); - }); - - Assert.IsTrue(exception.Message.Contains("keyPair")); - } - [DataTestMethod] [DataRow(null)] [DataRow("")] @@ -305,7 +215,7 @@ public async Task CreateFaceCaptureResourceShouldThrowExceptionWhenSessionIdIsNu { var exception = await Assert.ThrowsExceptionAsync(async () => { - await _docScanService.CreateFaceCaptureResource(_sdkId, _keyPair, sessionId, _createFaceCaptureResourcePayload); + await _docScanService.CreateFaceCaptureResource(_authStrategy, sessionId, _createFaceCaptureResourcePayload); }); Assert.IsTrue(exception.Message.Contains(nameof(sessionId))); @@ -316,38 +226,12 @@ public async Task CreateFaceCaptureResourceShouldThrowExceptionWhenCreateFaceCap { var exception = await Assert.ThrowsExceptionAsync(async () => { - await _docScanService.CreateFaceCaptureResource(_sdkId, _keyPair, _someSessionId, null); + await _docScanService.CreateFaceCaptureResource(_authStrategy, _someSessionId, null); }); Assert.IsTrue(exception.Message.Contains("createFaceCaptureResourcePayload")); } - [DataTestMethod] - [DataRow(null)] - [DataRow("")] - [DataRow(" ")] - [DataRow(" ")] - public async Task UploadFaceCaptureImageShouldThrowExceptionWhenSdkIdIsNullEmptyOrWhitespace(string sdkId) - { - var exception = await Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.UploadFaceCaptureImage(sdkId, _keyPair, _someSessionId, _someResourceId, _uploadFaceCaptureImagePayload); - }); - - Assert.IsTrue(exception.Message.Contains(nameof(sdkId))); - } - - [TestMethod] - public async Task UploadFaceCaptureImageShouldThrowExceptionWhenKeyPairIsNull() - { - var exception = await Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.UploadFaceCaptureImage(_sdkId, null, _someSessionId, _someResourceId, _uploadFaceCaptureImagePayload); - }); - - Assert.IsTrue(exception.Message.Contains("keyPair")); - } - [DataTestMethod] [DataRow(null)] [DataRow("")] @@ -357,7 +241,7 @@ public async Task UploadFaceCaptureImageShouldThrowExceptionWhenSessionIdIsNullE { var exception = await Assert.ThrowsExceptionAsync(async () => { - await _docScanService.UploadFaceCaptureImage(_sdkId, _keyPair, sessionId, _someResourceId, _uploadFaceCaptureImagePayload); + await _docScanService.UploadFaceCaptureImage(_authStrategy, sessionId, _someResourceId, _uploadFaceCaptureImagePayload); }); Assert.IsTrue(exception.Message.Contains(nameof(sessionId))); @@ -372,7 +256,7 @@ public async Task UploadFaceCaptureImageShouldThrowExceptionWhenResourceIdIsNull { var exception = await Assert.ThrowsExceptionAsync(async () => { - await _docScanService.UploadFaceCaptureImage(_sdkId, _keyPair, _someSessionId, resourceId, _uploadFaceCaptureImagePayload); + await _docScanService.UploadFaceCaptureImage(_authStrategy, _someSessionId, resourceId, _uploadFaceCaptureImagePayload); }); Assert.IsTrue(exception.Message.Contains(nameof(resourceId))); @@ -383,38 +267,12 @@ public async Task UploadFaceCaptureImageShouldThrowExceptionWhenUploadFaceCaptur { var exception = await Assert.ThrowsExceptionAsync(async () => { - await _docScanService.UploadFaceCaptureImage(_sdkId, _keyPair, _someSessionId, _someResourceId, null); + await _docScanService.UploadFaceCaptureImage(_authStrategy, _someSessionId, _someResourceId, null); }); Assert.IsTrue(exception.Message.Contains("uploadFaceCaptureImagePayload")); } - [DataTestMethod] - [DataRow(null)] - [DataRow("")] - [DataRow(" ")] - [DataRow(" ")] - public async Task GetSessionConfigurationShouldThrowExceptionWhenSdkIdIsNullEmptyOrWhitespace(string sdkId) - { - var exception = await Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.GetSessionConfiguration(sdkId, _keyPair, _someSessionId); - }); - - Assert.IsTrue(exception.Message.Contains(nameof(sdkId))); - } - - [TestMethod] - public async Task GetSessionConfigurationShouldThrowExceptionWhenKeyPairIsNull() - { - var exception = await Assert.ThrowsExceptionAsync(async () => - { - await _docScanService.GetSessionConfiguration(_sdkId, null, _someSessionId); - }); - - Assert.IsTrue(exception.Message.Contains("keyPair")); - } - [DataTestMethod] [DataRow(null)] [DataRow("")] @@ -424,10 +282,10 @@ public async Task GetSessionConfigurationShouldThrowExceptionWhenSessionIdIsNull { var exception = await Assert.ThrowsExceptionAsync(async () => { - await _docScanService.GetSessionConfiguration(_sdkId, _keyPair, sessionId); + await _docScanService.GetSessionConfiguration(_authStrategy, sessionId); }); Assert.IsTrue(exception.Message.Contains(nameof(sessionId))); } } -} \ No newline at end of file +} diff --git a/test/Yoti.Auth.Tests/Web/BearerTokenAuthStrategyTests.cs b/test/Yoti.Auth.Tests/Web/BearerTokenAuthStrategyTests.cs new file mode 100644 index 00000000..9c3650b9 --- /dev/null +++ b/test/Yoti.Auth.Tests/Web/BearerTokenAuthStrategyTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Net.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.Web; + +namespace Yoti.Auth.Tests.Web +{ + [TestClass] + public class BearerTokenAuthStrategyTests + { + [TestMethod] + public void NullTokenShouldThrow() + { + Assert.ThrowsException(() => new BearerTokenAuthStrategy(null)); + } + + [TestMethod] + public void EmptyTokenShouldThrow() + { + Assert.ThrowsException(() => new BearerTokenAuthStrategy(string.Empty)); + } + + [TestMethod] + public void SdkIdShouldBeNull() + { + var strategy = new BearerTokenAuthStrategy("some-token"); + Assert.IsNull(strategy.SdkId); + } + + [TestMethod] + public void CreateAuthHeadersShouldReturnBearerHeader() + { + const string token = "my-bearer-token"; + var strategy = new BearerTokenAuthStrategy(token); + + var headers = strategy.CreateAuthHeaders(HttpMethod.Get, "/test", null); + + Assert.IsTrue(headers.ContainsKey(Constants.Api.AuthorizationHeader)); + Assert.AreEqual("Bearer " + token, headers[Constants.Api.AuthorizationHeader]); + } + + [TestMethod] + public void CreateQueryParamsShouldReturnEmpty() + { + var strategy = new BearerTokenAuthStrategy("token"); + var queryParams = strategy.CreateQueryParams(); + Assert.AreEqual(0, queryParams.Count); + } + } +} diff --git a/test/Yoti.Auth.Tests/Web/NoAuthStrategyTests.cs b/test/Yoti.Auth.Tests/Web/NoAuthStrategyTests.cs new file mode 100644 index 00000000..581141bd --- /dev/null +++ b/test/Yoti.Auth.Tests/Web/NoAuthStrategyTests.cs @@ -0,0 +1,30 @@ +using System.Net.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Yoti.Auth.Web; + +namespace Yoti.Auth.Tests.Web +{ + [TestClass] + public class NoAuthStrategyTests + { + [TestMethod] + public void SdkIdShouldBeNull() + { + Assert.IsNull(new NoAuthStrategy().SdkId); + } + + [TestMethod] + public void CreateAuthHeadersShouldReturnEmpty() + { + var headers = new NoAuthStrategy().CreateAuthHeaders(HttpMethod.Get, "/test", null); + Assert.AreEqual(0, headers.Count); + } + + [TestMethod] + public void CreateQueryParamsShouldReturnEmpty() + { + var queryParams = new NoAuthStrategy().CreateQueryParams(); + Assert.AreEqual(0, queryParams.Count); + } + } +} diff --git a/test/Yoti.Auth.Tests/Web/RequestBuilderTests.cs b/test/Yoti.Auth.Tests/Web/RequestBuilderTests.cs index 3a107cc9..94b415da 100644 --- a/test/Yoti.Auth.Tests/Web/RequestBuilderTests.cs +++ b/test/Yoti.Auth.Tests/Web/RequestBuilderTests.cs @@ -29,7 +29,7 @@ public class RequestBuilderTests [TestMethod] public void ShouldNotBuildWithoutKeyPair() { - var argumentNullException = Assert.ThrowsException(() => + var exception = Assert.ThrowsException(() => { new RequestBuilder() .WithBaseUri(_testBaseUri) @@ -38,7 +38,37 @@ public void ShouldNotBuildWithoutKeyPair() .Build(); }); - Assert.IsTrue(argumentNullException.Message.Contains("_keyPair")); + Assert.IsTrue(exception.Message.Contains("WithAuthStrategy") || exception.Message.Contains("WithKeyPair")); + } + + [TestMethod] + public void ShouldNotBuildWithBothKeyPairAndAuthStrategy() + { + var exception = Assert.ThrowsException(() => + { + new RequestBuilder() + .WithBaseUri(_testBaseUri) + .WithEndpoint("/a") + .WithHttpMethod(HttpMethod.Get) + .WithKeyPair(KeyPair.Get()) + .WithAuthStrategy(new Yoti.Auth.Web.NoAuthStrategy()) + .Build(); + }); + + Assert.IsTrue(exception.Message.Contains("mutually exclusive")); + } + + [TestMethod] + public void ShouldBuildWithAuthStrategy() + { + var request = new RequestBuilder() + .WithBaseUri(_testBaseUri) + .WithEndpoint("/a") + .WithHttpMethod(HttpMethod.Get) + .WithAuthStrategy(new Yoti.Auth.Web.NoAuthStrategy()) + .Build(); + + Assert.IsNotNull(request); } [TestMethod] diff --git a/test/Yoti.Auth.Tests/Web/SignedRequestAuthStrategyTests.cs b/test/Yoti.Auth.Tests/Web/SignedRequestAuthStrategyTests.cs new file mode 100644 index 00000000..f121d2a2 --- /dev/null +++ b/test/Yoti.Auth.Tests/Web/SignedRequestAuthStrategyTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Net.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Crypto; +using Yoti.Auth.Tests.Common; +using Yoti.Auth.Web; + +namespace Yoti.Auth.Tests.Web +{ + [TestClass] + public class SignedRequestAuthStrategyTests + { + private readonly AsymmetricCipherKeyPair _keyPair = KeyPair.Get(); + private const string _sdkId = "test-sdk-id"; + + [TestMethod] + public void NullKeyPairShouldThrow() + { + Assert.ThrowsException(() => + new SignedRequestAuthStrategy(null, _sdkId)); + } + + [TestMethod] + public void NullSdkIdShouldThrow() + { + Assert.ThrowsException(() => + new SignedRequestAuthStrategy(_keyPair, null)); + } + + [TestMethod] + public void EmptySdkIdShouldThrow() + { + Assert.ThrowsException(() => + new SignedRequestAuthStrategy(_keyPair, string.Empty)); + } + + [TestMethod] + public void SdkIdShouldBeSet() + { + var strategy = new SignedRequestAuthStrategy(_keyPair, _sdkId); + Assert.AreEqual(_sdkId, strategy.SdkId); + } + + [TestMethod] + public void CreateAuthHeadersShouldReturnDigestHeader() + { + var strategy = new SignedRequestAuthStrategy(_keyPair, _sdkId); + var headers = strategy.CreateAuthHeaders(HttpMethod.Get, "/sessions?nonce=abc×tamp=123", null); + + Assert.IsTrue(headers.ContainsKey(Constants.Api.DigestHeader)); + Assert.IsFalse(string.IsNullOrEmpty(headers[Constants.Api.DigestHeader])); + } + + [TestMethod] + public void CreateQueryParamsShouldContainNonceAndTimestamp() + { + var strategy = new SignedRequestAuthStrategy(_keyPair, _sdkId); + var queryParams = strategy.CreateQueryParams(); + + Assert.IsTrue(queryParams.ContainsKey("nonce")); + Assert.IsTrue(queryParams.ContainsKey("timestamp")); + Assert.IsFalse(string.IsNullOrEmpty(queryParams["nonce"])); + Assert.IsFalse(string.IsNullOrEmpty(queryParams["timestamp"])); + } + } +}