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);
}
}