Creating an API Client with OAuth2
Creating an API Client with OAuth2
Using a client we can create a dynamic connection between the Connector and your API. In this step we’ll build an HTTP client so the data objects provided to the Connector will be from your system.
Key considerations
The data your client gets from the API should look similar to the data objects needed for your connector, but if the data does not match precisely, we can map the API data to the exact data object in the Data Reader.
Your client code will be executed by the App Xchange platform and as such will need to have access to your API.
CLI Command Options
Before running the CLI command to create a client, you have to know about the existing options it uses.
-
--type (aliases: -t)
- It indicates the type of client to create. HTTP is used by default
- Required: False
-
--auth-type (aliases: --auth)
- It indicates the type of authentication to use. Possible values include
ApiKey
,Basic
,Custom
,OAuth2CodeFlow
,OAuth2ClientCredentials
andOAuth2Password
. - Required: True
- It indicates the type of authentication to use. Possible values include
If your client needs ApiKey
, Basic
, OAuth2CodeFlow
, OAuth2ClientCredentials
and OAuth2Password
authentication, use the --auth-type
option with the specific authentication type. If you need a type of authentication that is not listed, you can use Custom
an implement what you need.
The CLI command:
xchange client new --type Http --auth-type {YOUR_AUTH_TYPE}
While in the root of your Connector project run the CLI command to create a new client. Depending on which --auth-type
was specified, different files will be created (the files for each case will be explained in detail shortly). A new Client
directory and a client named ApiClient.cs
will be created. This will also add the client to your dependency injection found in the ConnectorRegistration
file.
The client will by default come with a retry policy. Policies can be removed or added as needed.
Clients created with Authentication Types
Defining Connections for Authentication
When a connector builder using the App Xchange CLI tools wants to create a client with authentications, the connector will need the proper credentials to create valid requests, as well as a handler that adds the authentication headers the requests may need.
All the configuration needed to get authenticated requests will be contained in an AuthType class, and, depending on the type of authentication, its respective handler will take care of authenticating the requests.
About AuthType classes
The AuthType class contains what the client will use in order to get authenticated requests. The AuthType class will contain the secrets, as well as the Connection Environment and Base Url the secret belongs to.
Depending on the type of authentication the API used in the client needs, a different AuthType class will be created.
Currently, there are five types of authentication whose creation is supported: ApiKeyAuth
, BasicAuth
, CustomAuth
, OAuth2CodeFlow
, OAuth2ClientCredentials
and OAuth2Password
. In this page, we will focus on the first three, which implement an interface each: IApiKeyAuth
, IBasicAuth
and ICustomAuth
respectively.
If you need to use ApiKeyAuth
, BasicAuth
, or CustomAuth
, see Creating an API Client.
About AuthType OAuth2 classes
Unlike the other types of connections, with OAuth2CodeFlow
, OAuth2ClientCredentials
and OAuth2Password
the AuthType class generated will not implement an interface but extend existing classes OAuth2CodeFlowBase
, OAuth2ClientCredentialsBase
and OAuth2PasswordBase
.
When the AuthType class is created, it includes a property called ConnectionEnvironment
. This property will have the configuration of the possible environments. Depending on the ConnectionEnvironment
, the BaseUrl
will be set.
Upon creation, the BaseUrl
will be filled with placeholders. Be sure to remove those and use your actual URLs.
Connection definition classes are decorated with an attribute called ConnectionProperty
. This attribute is necessary for the property to show up in the extracted metadata of the connector.
Key considerations
Currently, only properties of type string
are supported.
About Client Authentication
The OAuth2 base classes have a property called ClientAuthentication
. The value of this property will define if the authentication request that will exchange the ClientId
and ClientSecret
for a token will add the properties as headers or as content inside the body of the request. If no value is given, the default value will be BasicAuthHeader
.
The ClientAuthentication
enum looks like this:
The ClientAuthentication
enum
public enum ClientAuthentication
{
Unknown = 0,
BasicAuthHeader = 1,
ClientCredentialsInBody = 2
}
OAuth2CodeFlow class
OAuth2CodeFlow
extends an existing class OAuth2CodeFlowBase
, which contains properties AuthorizationUrl
, TokenUrl
, RefreshUrl
, ClientId
, ClientSecret
, Scope
and ClientAuthentication
.
The OAuth2CodeFlowBase
class
public class OAuth2CodeFlowBase
{
public string AuthorizationUrl { get; init; } = string.Empty;
public string TokenUrl { get; init; } = string.Empty;
public string RefreshUrl { get; init; } = string.Empty;
public string ClientId { get; init; } = string.Empty;
public string ClientSecret { get; init; } = string.Empty;
public string Scope { get; init; } = string.Empty;
public string Prompt { get; init; } = string.Empty;
public ClientAuthentication ClientAuthentication { get; init; } = ClientAuthentication.BasicAuthHeader;
}
Example of an AuthType class of OAuth2CodeFlow
[ConnectionDefinition(title: "OAuth2CodeFlow", description: "")]
public class OAuth2CodeFlow : OAuth2CodeFlowBase
{
[ConnectionProperty(title: "Connection Environment", description: "", isRequired: true, isSensitive: false)]
public ConnectionEnvironmentOAuth2CodeFlow ConnectionEnvironment { get; set; } = ConnectionEnvironmentOAuth2CodeFlow.Unknown;
public string BaseUrl
{
get
{
switch (ConnectionEnvironment)
{
case ConnectionEnvironmentOAuth2CodeFlow.Production:
return "http://prod.example.com";
case ConnectionEnvironmentOAuth2CodeFlow.Test:
return "https://test.example.com";
default:
throw new Exception("No base url was set.");
}
}
}
}
public enum ConnectionEnvironmentOAuth2CodeFlow
{
Unknown = 0,
Production = 1,
Test = 2
}
OAuth2ClientCredentials class
OAuth2ClientCredentials
extends an existing class OAuth2ClientCredentialsBase
, which contains properties TokenUrl
, ClientId
, ClientSecret
, Scope
and ClientAuthentication
.
The OAuth2ClientCredentialsBase
class
public class OAuth2ClientCredentialsBase
{
public string TokenUrl { get; init; } = string.Empty;
public string ClientId { get; init; } = string.Empty;
public string ClientSecret { get; init; } = string.Empty;
public string Scope { get; init; } = string.Empty;
public ClientAuthentication ClientAuthentication { get; init; } = ClientAuthentication.BasicAuthHeader;
}
Example of an AuthType class of OAuth2ClientCredentials
[ConnectionDefinition(title: "OAuth2ClientCredentials", description: "")]
public class OAuth2ClientCredentials : OAuth2ClientCredentialsBase
{
[ConnectionProperty(title: "Connection Environment", description: "", isRequired: true, isSensitive: false)]
public ConnectionEnvironmentOAuth2ClientCredentials ConnectionEnvironment { get; set; } = ConnectionEnvironmentOAuth2ClientCredentials.Unknown;
public string BaseUrl
{
get
{
switch (ConnectionEnvironment)
{
case ConnectionEnvironmentOAuth2ClientCredentials.Production:
return "http://prod.example.com";
case ConnectionEnvironmentOAuth2ClientCredentials.Test:
return "https://test.example.com";
default:
throw new Exception("No base url was set.");
}
}
}
}
public enum ConnectionEnvironmentOAuth2ClientCredentials
{
Unknown = 0,
Production = 1,
Test = 2
}
OAuth2ClientPassword class
OAuth2ClientPassword
extends an existing class OAuth2ClientPasswordBase
, which contains properties TokenUrl
, ClientId
, ClientSecret
, Scope
, Username
, Password
and ClientAuthentication
.
The OAuth2PasswordBase
class
public class OAuth2PasswordBase
{
public string TokenUrl { get; init; } = string.Empty;
public string ClientId { get; init; } = string.Empty;
public string ClientSecret { get; init; } = string.Empty;
public string Scope { get; init; } = string.Empty;
[ConnectionProperty(title: "Username", description: "", isRequired: true, isSensitive: false)]
public string Username { get; init; } = string.Empty;
[ConnectionProperty(title: "Password", description: "", isRequired: true, isSensitive: true)]
public string Password { get; init; } = string.Empty;
public ClientAuthentication ClientAuthentication { get; init; } = ClientAuthentication.BasicAuthHeader;
}
username
and password
properties are decorated with the ConnectionProperty
attribute because these properties are part of the connection properties and not part of the OAuth2 client being used to get the token access.
Example of an AuthType class of OAuth2Password
[ConnectionDefinition(title: "OAuth2Password", description: "")]
public class OAuth2Password : OAuth2PasswordBase
{
[ConnectionProperty(title: "Connection Environment", description: "", isRequired: true, isSensitive: false)]
public ConnectionEnvironmentOAuth2Password ConnectionEnvironment { get; set; } = ConnectionEnvironmentOAuth2Password.Unknown;
public string BaseUrl
{
get
{
switch (ConnectionEnvironment)
{
case ConnectionEnvironmentOAuth2Password.Production:
return "http://prod.example.com";
case ConnectionEnvironmentOAuth2Password.Test:
return "https://test.example.com";
default:
throw new Exception("No base url was set.");
}
}
}
}
public enum ConnectionEnvironmentOAuth2Password
{
Unknown = 0,
Production = 1,
Test = 2
}
About OAuth2 Handlers
The AuthType Handler is a DelegatingHandler
and is used to add headers to the requests to establish authentication in a transaction. Depending on the type of authentication needed, the headers will be added.
Unlike the other types of connections, if we're using any OAuth2 type authentication, the handler used by the API client will not be generated by the CLI therefore will not be editable. Instead, the handlers are part of the SDK package, since they follow OAuth2 standards and should not be changed.
Working with OAuth2 connections
Once you've created a client with an OAuth2 authentication type (same as Connections with non-OAuth2 authentication) you will have two new folders in your repository, named Client
(with the ApiClient and the AuthType handler) and Connections
(with the AuthType class and a ConnectionTestHandler class). It will also implement IConfigureConnectorApiClient
in the ConnectorRegistration
file, where the injections needed for the client will be added.
Example of ApiClient
public class ApiClient
{
private readonly HttpClient _httpClient;
public ApiClient (HttpClient httpClient, string baseUrl)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new System.Uri(baseUrl);
}
public async Task<ApiResponse<JsonDocument>> GetExampleRecord(CancellationToken cancellationToken = default)
{
var response = await _httpClient
.GetAsync("example", cancellationToken: cancellationToken)
.ConfigureAwait(false);
return new ApiResponse<JsonDocument>
{
IsSuccessful = response.IsSuccessStatusCode,
StatusCode = (int)response.StatusCode,
Result = response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken: cancellationToken) : default,
RawResult = await response.Content.ReadAsStreamAsync(cancellationToken: cancellationToken)
};
}
}
Example of ConfigureConnectorApiClient
and RegisterConnectionTestHandler
public void ConfigureConnectorApiClient(IServiceCollection serviceCollection, IHostConnectionContext hostConnectionContext)
{
var activeConnection = hostConnectionContext.GetConnection();
serviceCollection.ResolveServices(activeConnection);
}
public void RegisterConnectionTestHandler(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<IConnectionTestHandler, ConnectionTestHandler>();
}
RegisterConnectionTestHandler
applies to version 1.2.3
or above
Another file that will be created is the ClientHelper, which will help resolve what we need in case there's multiple connection definitions. Depending on the definitionKey
of the active connection, it will inject the dependencies to support the AuthType in use. It will also instantiate the ApiClient
, using the configuration of the connector and the Base Url of the active connection.
Example of a ClientHelper
public static class ClientHelper
{
public static class AuthTypeKeyEnums
{
public const string OAuth2ClientCredentials = "oAuth2ClientCredentials";
}
public static void ResolveServices(this IServiceCollection serviceCollection, ConnectionContainer activeConnection)
{
serviceCollection.AddTransient<RetryPolicyHandler>();
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
switch (activeConnection.DefinitionKey)
{
case AuthTypeKeyEnums.OAuth2ClientCredentials:
var configOAuth2ClientCredentials = JsonSerializer.Deserialize<OAuth2ClientCredentials>(activeConnection.Configuration, options);
serviceCollection.AddSingleton<OAuth2ClientCredentialsBase>(configOAuth2ClientCredentials!);
serviceCollection.AddTransient<RetryPolicyHandler>();
serviceCollection.AddTransient<OAuth2ClientCredentialsHandler>();
serviceCollection.AddHttpClient<ApiClient, ApiClient>(client => new ApiClient(client, configOAuth2ClientCredentials!.BaseUrl))
.AddHttpMessageHandler<OAuth2ClientCredentialsHandler>()
.AddHttpMessageHandler<RetryPolicyHandler>();
break;
default:
throw new Exception($"Unable to find services for definition key {activeConnection.DefinitionKey}");
}
}
}
If another AuthType class is created, the case statement with the dependency injections needed will be added to the file.
Testing a Connection
NOTE: Applies to version
1.2.3-beta.2
or above
When a connection is created, we need a way to test the connection to see if it works before we decide to use it. For that purpose, the CLI will also generate in the Connections
folder a class called that implements IConnectionTestHandler
called ConnectionTestHandler. This class will implement a method called TestConnection
, in which a test process for the connection should be implemented.
The ConnectionTestHandler
generated by the CLI will look like this.
Example of an implemented ConnectionTestHandler
public async Task<TestConnectionResult> TestConnection()
{
// Make a call to your API/system to obtain the connection test result.
var response = await _apiClient.TestConnection();
// Depending on the response, make your own specific messages.
if (response == null)
{
return new TestConnectionResult()
{
Success = false,
Message = "Failed to get response from server",
StatusCode = 500
};
}
if (response.IsSuccessful)
{
return new TestConnectionResult()
{
Success = true,
Message = "Successful test.",
StatusCode = response.StatusCode
};
}
switch (response.StatusCode)
{
case 403:
return new TestConnectionResult()
{
Success = false,
Message = "Invalid Credentials: Forbidden.",
StatusCode = response.StatusCode
};
case 401:
return new TestConnectionResult()
{
Success = false,
Message = "Invalid Credentials: Unauthorized",
StatusCode = response.StatusCode
};
default:
return new TestConnectionResult()
{
Success = false,
Message = "Unknown Issue.",
StatusCode = response.StatusCode
};
}
}
The method TestConnection()
should make a call to the ApiClient
and it could go from testing a simple GET
endpoint to something more complicated; the purpose of the _apiClient.TestConnection()
method is to ensure the connector can make authenticated calls to the API.
Depending of the response of the ApiClient
call, Success
should indicate if the test as successful, the StatusCode
of the response should be set, and, if the test is unsuccessful, a Message
should be set in order to give more information.
For more information on how to run your test locally, you can check out Testing A Connector Locally,
In summary what is being done is:
- Given an OAuth2 AuthType, a class and a handler will be created and have its dependencies injected.
- The ApiClient will receive the BaseUrl from the OAuth2 AuthType class configured.
- Since multiple Auth Types are supported, the ClientHelper will help resolve the dependencies needed for the active connection.
- A handler for testing a connection is created to ensure the connector can perform as needed with the given connection.