diff --git a/FMData.sln b/FMData.sln index 9d46dfa..a80e64a 100644 --- a/FMData.sln +++ b/FMData.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FMData.Xml", "src\FMData.Xm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FMData.Xml.Tests", "tests\FMData.Xml.Tests\FMData.Xml.Tests.csproj", "{36143397-0EED-4CA4-9D25-C60A8C93B1FA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FMData.Rest.Auth.FileMakerCloud", "src\FMData.Rest.Auth.Cloud\FMData.Rest.Auth.FileMakerCloud.csproj", "{36772074-2B2B-4FB5-8A48-7F4EA1FB1233}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,18 @@ Global {36143397-0EED-4CA4-9D25-C60A8C93B1FA}.Release|x64.Build.0 = Release|Any CPU {36143397-0EED-4CA4-9D25-C60A8C93B1FA}.Release|x86.ActiveCfg = Release|Any CPU {36143397-0EED-4CA4-9D25-C60A8C93B1FA}.Release|x86.Build.0 = Release|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Debug|x64.ActiveCfg = Debug|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Debug|x64.Build.0 = Debug|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Debug|x86.ActiveCfg = Debug|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Debug|x86.Build.0 = Debug|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Release|Any CPU.Build.0 = Release|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Release|x64.ActiveCfg = Release|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Release|x64.Build.0 = Release|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Release|x86.ActiveCfg = Release|Any CPU + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,6 +119,7 @@ Global {DF7C8CD9-D26F-4A66-A3C4-95A6E7EF269E} = {6926CF2C-9411-467D-AC6F-06B18C3A41B6} {2CB7F5C1-7D49-4ADE-B137-831D5653958B} = {6926CF2C-9411-467D-AC6F-06B18C3A41B6} {36143397-0EED-4CA4-9D25-C60A8C93B1FA} = {FC1158B5-9752-46DE-B885-B8B553E252B2} + {36772074-2B2B-4FB5-8A48-7F4EA1FB1233} = {6926CF2C-9411-467D-AC6F-06B18C3A41B6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {13001953-EB62-4CE6-82B1-3795390B514E} diff --git a/src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj b/src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj new file mode 100644 index 0000000..ffd1c63 --- /dev/null +++ b/src/FMData.Rest.Auth.Cloud/FMData.Rest.Auth.FileMakerCloud.csproj @@ -0,0 +1,86 @@ + + + netstandard2.0;net6.0 + true + FMData.Rest.Auth.FileMakerCloud + FMData.Rest.Auth.FileMakerCloud + FileMaker Data API Authentication for FileMaker Cloud. + en-US + https://github.com/fuzzzerd/fmdata/blob/master/LICENSE + https://fmdata.io/ + nuget.png + readme.md + https://github.com/fuzzzerd/fmdata + GitHub + Nate Bross + + data-api filemaker dataapi dapi-client data dapi api filemaker-rest filemaker-api netstandard json dotnet-standard + true + Embedded + true + true + true + + + + + + + + 4.3 + v + beta + + + + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + + + + + + + + + + + + + + + + + 2.2.2 + + + 3.7.4 + + + + + + 2.2.2 + + + 3.7.4 + + + + + + 3.7.4 + + + + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch)-pr.$(APPVEYOR_PULL_REQUEST_NUMBER).build-id.$(APPVEYOR_BUILD_ID).$(MinVerPreRelease) + $(PackageVersion)+$(MinVerBuildMetadata) + $(PackageVersion) + + + diff --git a/src/FMData.Rest.Auth.Cloud/FileMakerCloudAuthTokenProvider.cs b/src/FMData.Rest.Auth.Cloud/FileMakerCloudAuthTokenProvider.cs new file mode 100644 index 0000000..ae15d5e --- /dev/null +++ b/src/FMData.Rest.Auth.Cloud/FileMakerCloudAuthTokenProvider.cs @@ -0,0 +1,59 @@ +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Amazon.CognitoIdentityProvider; +using Amazon.Extensions.CognitoAuthentication; +using Amazon.Runtime; +using Amazon; + +namespace FMData.Rest +{ + /// + /// FileMaker Cloud authentication provider (AWS Cognito) + /// + public class FileMakerCloudAuthTokenProvider : IAuthTokenProvider + { + private readonly ConnectionInfo _conn; + + /// + /// Constructor + /// + /// Connection config values + public FileMakerCloudAuthTokenProvider(ConnectionInfo conn) + { + _conn = conn; + } + + /// + /// Connection config values + /// + public ConnectionInfo ConnectionInfo { get => _conn; } + + /// + /// Gets the AuthenticationHeaderValue + /// + /// AuthenticationHeaderValue + public async Task GetAuthenticationHeaderValue() + { + var region = RegionEndpoint.GetBySystemName(_conn.RegionEndpoint); + var identityProviderClient = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), region); + string token = await GetToken(identityProviderClient).ConfigureAwait(false); + return AuthenticationHeaderValue.Parse("FMID " + token); + } + + /// + /// Provide an AWS Cognito Identiy Token + /// + /// AmazonCognitoIdentityProviderClient + /// returns the IdToken of the AuthenticationResult + private async Task GetToken(AmazonCognitoIdentityProviderClient identityProviderClient) + { + var userpool = new CognitoUserPool(ConnectionInfo.CognitoUserPoolID, ConnectionInfo.CognitoClientID, identityProviderClient); + var user = new CognitoUser(ConnectionInfo.Username, ConnectionInfo.CognitoClientID, userpool, identityProviderClient); + var initiateSrpAuthRequest = new InitiateSrpAuthRequest(); //SRP means Secure Remote Password + initiateSrpAuthRequest.Password = ConnectionInfo.Password; + + var response = await user.StartWithSrpAuthAsync(initiateSrpAuthRequest).ConfigureAwait(false); + return response.AuthenticationResult.IdToken; + } + } +} diff --git a/src/FMData.Rest/DefaultAuthTokenProvider.cs b/src/FMData.Rest/DefaultAuthTokenProvider.cs new file mode 100644 index 0000000..799b8f0 --- /dev/null +++ b/src/FMData.Rest/DefaultAuthTokenProvider.cs @@ -0,0 +1,42 @@ +using System; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace FMData.Rest +{ + /// + /// Default authentication provider + /// + public class DefaultAuthTokenProvider : IAuthTokenProvider + { + private readonly ConnectionInfo _conn; + + /// + /// Constructor + /// + /// Provide Connection details + public DefaultAuthTokenProvider(ConnectionInfo conn) + { + _conn = conn; + } + + /// + /// Connection config values + /// + public ConnectionInfo ConnectionInfo { get => _conn; } + + /// + /// Get base64 encoded AuthenticationHeaderValue + /// + /// + /// + public Task GetAuthenticationHeaderValue() + { + if (string.IsNullOrEmpty(_conn.Username)) throw new ArgumentException("Username is a required parameter."); + if (string.IsNullOrEmpty(_conn.Password)) throw new ArgumentException("Password is a required parameter."); + var header = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_conn.Username}:{_conn.Password}"))); + return Task.FromResult(header); + } + } +} diff --git a/src/FMData.Rest/FileMakerRestClient.cs b/src/FMData.Rest/FileMakerRestClient.cs index 6c15444..41a0b4d 100644 --- a/src/FMData.Rest/FileMakerRestClient.cs +++ b/src/FMData.Rest/FileMakerRestClient.cs @@ -1,4 +1,4 @@ -using FMData.Rest.Requests; +using FMData.Rest.Requests; using FMData.Rest.Responses; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -18,7 +18,7 @@ namespace FMData.Rest /// /// FileMaker Data API Client Implementation. /// - public class FileMakerRestClient : FileMakerApiClientBase, IFileMakerApiClient + public class FileMakerRestClient : FileMakerApiClientBase, IFileMakerRestClient { #region Request Factories /// @@ -50,6 +50,8 @@ public class FileMakerRestClient : FileMakerApiClientBase, IFileMakerApiClient private AuthenticationHeaderValue _authHeader; private DateTime _dataTokenLastUse = DateTime.MinValue; + private readonly IAuthTokenProvider _authTokenProvider; + #region Constructors /// /// Create a FileMakerRestClient with a new instance of HttpClient. @@ -68,9 +70,18 @@ public FileMakerRestClient(string fmsUri, string file, string user, string pass) /// The HttpClient instance to use. /// The connection information for FMS. public FileMakerRestClient(HttpClient client, ConnectionInfo conn) - : base(client, conn) - { + : this(client, new DefaultAuthTokenProvider(conn)) + { } + /// + /// FM Data Constructor with HttpClient, ConnectionInfo and an authentication provider. Useful for Dependency Injection situations. + /// + /// The HttpClient instance to use. + /// Authentication provider + public FileMakerRestClient(HttpClient client, IAuthTokenProvider authTokenProvider) + : base(client, authTokenProvider.ConnectionInfo) + { + _authTokenProvider = authTokenProvider; #if NETSTANDARD1_3 var header = new System.Net.Http.Headers.ProductHeaderValue("FMData.Rest", "4"); var userAgent = new System.Net.Http.Headers.ProductInfoHeaderValue(header); @@ -82,6 +93,7 @@ public FileMakerRestClient(HttpClient client, ConnectionInfo conn) #endif Client.DefaultRequestHeaders.UserAgent.Add(userAgent); } + #endregion #region API Endpoint Functions @@ -161,23 +173,18 @@ public FileMakerRestClient(HttpClient client, ConnectionInfo conn) /// private async Task UpdateTokenDateAsync() { - if (!IsAuthenticated) { await RefreshTokenAsync(UserName, Password).ConfigureAwait(false); } + if (!IsAuthenticated) { await RefreshTokenAsync().ConfigureAwait(false); } _dataTokenLastUse = DateTime.UtcNow; } /// /// Refreshes the internally stored authentication token from filemaker server. /// - /// Username of the account to authenticate. - /// Password of the account to authenticate. /// An AuthResponse from deserialized from FileMaker Server json response. - public async Task RefreshTokenAsync(string username, string password) + public async Task RefreshTokenAsync() { - // parameter checks - if (string.IsNullOrEmpty(username)) throw new ArgumentException("Username is a required parameter."); - if (string.IsNullOrEmpty(password)) throw new ArgumentException("Password is a required parameter."); + var authHeader = await _authTokenProvider.GetAuthenticationHeaderValue().ConfigureAwait(false); - var authHeader = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"))); var requestMessage = new HttpRequestMessage(HttpMethod.Post, AuthEndpoint()); requestMessage.Headers.Authorization = authHeader; requestMessage.Content = new StringContent("{ }", Encoding.UTF8, "application/json"); @@ -854,10 +861,7 @@ public override async Task> GetDatabasesAsync() var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{FmsUri}/fmi/data/v1/databases"); // special non-token auth to list databases - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("basic", Convert.ToBase64String( - Encoding.UTF8.GetBytes($"{UserName}:{Password}") - ) - ); + requestMessage.Headers.Authorization = await _authTokenProvider.GetAuthenticationHeaderValue().ConfigureAwait(false); // run the patch action var response = await Client.SendAsync(requestMessage).ConfigureAwait(false); diff --git a/src/FMData.Rest/IAuthTokenProvider.cs b/src/FMData.Rest/IAuthTokenProvider.cs new file mode 100644 index 0000000..8ed936e --- /dev/null +++ b/src/FMData.Rest/IAuthTokenProvider.cs @@ -0,0 +1,22 @@ +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace FMData.Rest +{ + /// + /// FileMaker REST Client Auth Provider Interface. + /// + public interface IAuthTokenProvider + { + /// + /// Connection config values + /// + ConnectionInfo ConnectionInfo { get; } + + /// + /// Provide the AuthenticationHeaderValue + /// + /// AuthenticationHeaderValue + Task GetAuthenticationHeaderValue(); + } +} diff --git a/src/FMData.Rest/IFileMakerRestClient.cs b/src/FMData.Rest/IFileMakerRestClient.cs index e8bcb39..ee54958 100644 --- a/src/FMData.Rest/IFileMakerRestClient.cs +++ b/src/FMData.Rest/IFileMakerRestClient.cs @@ -13,12 +13,8 @@ public interface IFileMakerRestClient : IFileMakerApiClient /// /// Refreshes the internally stored authentication token from filemaker server. /// - /// Username of the account to authenticate. - /// Password of the account to authenticate. /// An AuthResponse from deserialized from FileMaker Server json response. - Task RefreshTokenAsync( - string username, - string password); + Task RefreshTokenAsync(); /// /// Logs the user out and nullifies the token. @@ -129,4 +125,4 @@ Task ExecuteRequestAsync( /// bool IsAuthenticated { get; } } -} \ No newline at end of file +} diff --git a/src/FMData/ConnectionInfo.cs b/src/FMData/ConnectionInfo.cs index 2498e38..856c204 100644 --- a/src/FMData/ConnectionInfo.cs +++ b/src/FMData/ConnectionInfo.cs @@ -1,4 +1,4 @@ -namespace FMData +namespace FMData { /// /// Represents the connection information for FMS. @@ -21,5 +21,35 @@ public class ConnectionInfo /// Password to use when making the connection. /// public string Password { get; set; } + + #region FileMaker Cloud + + /* + * Using Claris ID for authentication + * ------------------------------------------- + * If you want to use the FileMaker Data API with FileMaker Cloud, you must authenticate using your Claris ID account. + * FileMaker Cloud uses Amazon Cognito for authentication. + * + * FileMaker Cloud provides the following endpoint to gather the required informations: + * https://www.ifmcloud.com/endpoint/userpool/2.2.0.my.claris.com.json + */ + + /// + /// AWS Cognito UserPoolID + /// data.UserPool_ID + /// + public string CognitoUserPoolID { get; set; } = "us-west-2_NqkuZcXQY"; + /// + /// AWS Cognito ClientID + /// data.Client_ID + /// + public string CognitoClientID { get; set; } = "4l9rvl4mv5es1eep1qe97cautn"; + /// + /// AWS Cognito Region + /// data.Region + /// + public string RegionEndpoint { get; set; } = "us-west-2"; + + #endregion } -} \ No newline at end of file +} diff --git a/tests/FMData.Rest.Tests/AuthenticationTests.cs b/tests/FMData.Rest.Tests/AuthenticationTests.cs index aa80adb..2fed1f2 100644 --- a/tests/FMData.Rest.Tests/AuthenticationTests.cs +++ b/tests/FMData.Rest.Tests/AuthenticationTests.cs @@ -1,4 +1,4 @@ -using RichardSzalay.MockHttp; +using RichardSzalay.MockHttp; using System; using System.Linq; using System.Net; @@ -73,7 +73,7 @@ public async Task RefreshToken_ShouldGet_NewToken() .Respond(HttpStatusCode.OK, "application/json", ""); using var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), new ConnectionInfo { FmsUri = server, Database = file, Username = user, Password = pass }); - var response = await fdc.RefreshTokenAsync("integration", "test"); + var response = await fdc.RefreshTokenAsync(); Assert.Equal("someOtherToken", response.Response.Token); } @@ -95,7 +95,7 @@ public async Task RefreshToken_Requires_AllParameters(string user, string pass) // pass in actual values here since we DON'T want this to blow up on constructor using var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), new ConnectionInfo { FmsUri = server, Database = file, Username = user, Password = pass }); - await Assert.ThrowsAsync(async () => await fdc.RefreshTokenAsync(user, pass)); + await Assert.ThrowsAsync(async () => await fdc.RefreshTokenAsync()); } [Fact(DisplayName = "User-Agent Should Match Version Of Assembly")] @@ -120,7 +120,7 @@ public async Task UserAgent_Should_Match_Version_Of_Assembly() .Respond(HttpStatusCode.OK, "application/json", ""); using var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), new ConnectionInfo { FmsUri = server, Database = file, Username = user, Password = pass }); - await fdc.RefreshTokenAsync(user, pass); + await fdc.RefreshTokenAsync(); Assert.True(fdc.IsAuthenticated); } } diff --git a/tests/FMData.Rest.Tests/FindAsync.Tests.cs b/tests/FMData.Rest.Tests/FindAsync.Tests.cs index 2a0f47e..508a855 100644 --- a/tests/FMData.Rest.Tests/FindAsync.Tests.cs +++ b/tests/FMData.Rest.Tests/FindAsync.Tests.cs @@ -3,6 +3,7 @@ using RichardSzalay.MockHttp; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; @@ -66,7 +67,7 @@ public async Task Find_WithScript_ShouldHaveScript() }, "nos_ran", null, null); // assert - var responseDataContainsResult = response.Any(r => r.Created == DateTime.Parse("03/29/2018 15:22:09")); + var responseDataContainsResult = response.Any(r => r.Created == DateTime.ParseExact("03/29/2018 15:22:09", "MM/dd/yyyy HH:mm:ss", CultureInfo.InvariantCulture)); Assert.True(responseDataContainsResult); } diff --git a/tests/FMData.Rest.Tests/GeneralTests.cs b/tests/FMData.Rest.Tests/GeneralTests.cs index 6567f40..4ac2cc8 100644 --- a/tests/FMData.Rest.Tests/GeneralTests.cs +++ b/tests/FMData.Rest.Tests/GeneralTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Runtime.Serialization; @@ -191,7 +192,7 @@ public async Task Test_DateTime_To_Timestamp_Parsing() }); // assert - var responseDataContainsResult = response.Any(r => r.Created == DateTime.Parse("03/29/2018 15:22:09")); + var responseDataContainsResult = response.Any(r => r.Created == DateTime.ParseExact("03/29/2018 15:22:09", "MM/dd/yyyy HH:mm:ss", CultureInfo.InvariantCulture)); Assert.True(responseDataContainsResult); } }