Skip to content

Commit

Permalink
Implementing OTP API
Browse files Browse the repository at this point in the history
  • Loading branch information
Bas Gijzen committed Jan 8, 2024
1 parent 3501d8d commit 09bec63
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false

# this. preferences
dotnet_style_qualification_for_field = true
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
Expand Down
4 changes: 4 additions & 0 deletions CM.Text/Common/Constant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ internal static class Constant
internal static readonly string TextSdkReference = $"text-sdk-dotnet-{typeof(TextClient).Assembly.GetName().Version}";

internal const string BusinessMessagingGatewayJsonEndpoint = "https://gw.cmtelecom.com/v1.0/message";

internal const string OtpRequestEndpoint = "https://api.cm.com/otp/v2/otp";
internal const string OtpVerifyEndpointPrefix = "https://api.cm.com/otp/v2/otp/{0}/verify";

internal static readonly string BusinessMessagingGatewayMediaTypeJson = "application/json";
internal static readonly string BusinessMessagingBodyTypeAuto = "AUTO";
internal static readonly int BusinessMessagingMessagePartsMinDefault = 1;
Expand Down
83 changes: 83 additions & 0 deletions CM.Text/Identity/OtpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace CM.Text.Identity
{
/// <summary>
/// A request to send an OTP towards an end-user.
/// </summary>
[PublicAPI]
public class OtpRequest
{
/// <summary>
/// Required: This is the sender name.
/// The maximum length is 11 alphanumerical characters or 16 digits. Example: 'MyCompany'
/// </summary>
[JsonPropertyName("from")]
public string From { get; set; }

/// <summary>
/// Required: The destination mobile numbers.
/// This value should be in international format.
/// A single mobile number per request. Example: '00447911123456'
/// </summary>
[JsonPropertyName("to")]
public string To { get; set; }

/// <summary>
/// The length of the code (min 4, max 10). default: 5.
/// </summary>
[JsonPropertyName("digits")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Digits { get; set; }

/// <summary>
/// The expiry in seconds (min 10, max 3600). default: 60 seconds.
/// </summary>
[JsonPropertyName("expiry")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Expiry { get; set; }

/// <summary>
/// The channel to send the code.
/// Supported values: auto, sms, push, whatsapp, voice, email.
/// Channel auto is only available with a SOLiD subscription.
/// </summary>
[JsonPropertyName("channel")]
public string Channel { get; set; } = "sms";

/// <summary>
/// The locale, for WhatsApp supported values: en, nl, fr, de, it, es.
/// Default: en
///
/// For Voice: the spoken language in the voice call,
/// supported values: de-DE, en-AU, en-GB, en-IN, en-US, es-ES, fr-CA, fr-FR, it-IT, ja-JP, nl-NL
/// Default: en-GB.
///
/// For Email: The locale for the email template.
/// </summary>
[JsonPropertyName("locale")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[CanBeNull]
public string Locale { get; set; }

/// <summary>
/// The app key, when <see cref="Channel"/> is 'push'
/// </summary>
[JsonPropertyName("pushAppKey")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[CanBeNull]
public string PushAppKey { get; set; }

/// <summary>
/// For WhatsApp, set a custom message. You can use the placeholder {code}, this will be replaced by the actual code.
/// Example: Your code is: {code}. This is only used as a fallback in case the message could not be delivered via WhatsApp.
///
/// For email, Set a custom message to be used in the email message. Do not include the {code} placeholder.
/// </summary>
[JsonPropertyName("message")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[CanBeNull]
public string Message { get; set; }
}
}
105 changes: 105 additions & 0 deletions CM.Text/Identity/OtpRequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

using JetBrains.Annotations;

namespace CM.Text.Identity
{
/// <summary>
/// Builder class to construct messages
/// </summary>
[PublicAPI]
public class OtpRequestBuilder
{
private readonly OtpRequest _otpRequest;

/// <summary>
/// Creates a new OtpRequestBuilder
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
public OtpRequestBuilder(string from, string to)
{
_otpRequest = new OtpRequest { From = from, To = to };
}

/// <summary>
/// Constructs the request.
/// </summary>
/// <returns></returns>
public OtpRequest Build()
{
return _otpRequest;
}

/// <summary>
/// Set the channel
/// </summary>
public OtpRequestBuilder WithChannel(string channel)
{
_otpRequest.Channel = channel;
return this;
}

/// <summary>
/// Sets The length of the code (min 4, max 10). default: 5.
/// </summary>
/// <param name="digits"></param>
/// <returns></returns>
public OtpRequestBuilder WithDigits(int digits)
{
_otpRequest.Digits = digits;
return this;
}

/// <summary>
/// The expiry in seconds (min 10, max 3600). default: 60 seconds.
/// </summary>
public OtpRequestBuilder WithExpiry(int expiryInSeconds)
{
_otpRequest.Expiry = expiryInSeconds;
return this;
}

/// <summary>
/// The locale, for WhatsApp supported values: en, nl, fr, de, it, es.
/// Default: en
///
/// For Voice: the spoken language in the voice call,
/// supported values: de-DE, en-AU, en-GB, en-IN, en-US, es-ES, fr-CA, fr-FR, it-IT, ja-JP, nl-NL
/// Default: en-GB.
///
/// For Email: The locale for the email template.
/// </summary>
/// <param name="locale"></param>
/// <returns></returns>
public OtpRequestBuilder WithLocale(string locale)
{
_otpRequest.Locale = locale;
return this;
}

/// <summary>
/// The app key, when the channel is 'push'
/// </summary>
/// <param name="pushAppKey"></param>
/// <returns></returns>
public OtpRequestBuilder WithPushAppKey(string pushAppKey)
{
_otpRequest.PushAppKey = pushAppKey;
return this;
}

/// <summary>
/// For WhatsApp, set a custom message. You can use the placeholder {code}, this will be replaced by the actual code.
/// Example: Your code is: {code}. This is only used as a fallback in case the message could not be delivered via WhatsApp.
///
/// For email, Set a custom message to be used in the email message. Do not include the {code} placeholder.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public OtpRequestBuilder WithMessage(string message)
{
_otpRequest.Message = message;
return this;
}
}
}
37 changes: 37 additions & 0 deletions CM.Text/Identity/OtpResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Text.Json.Serialization;

namespace CM.Text.Identity
{
/// <summary>
/// The result of an OTP request.
/// </summary>
public class OtpResult
{
/// <summary>
/// The identifier of the OTP.
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; }
/// <summary>
/// The channel used to send the code.
/// </summary>
[JsonPropertyName("channel")]
public string Channel { get; set; }
/// <summary>
/// Indicates if the code was valid.
/// </summary>
[JsonPropertyName("verified")]
public bool Verified { get; set; }
/// <summary>
/// The date the OTP was created.
/// </summary>
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
/// <summary>
/// The date the OTP will expire.
/// </summary>
[JsonPropertyName("expiresAt")]
public DateTime ExpiresAt { get; set; }
}
}
72 changes: 72 additions & 0 deletions CM.Text/TextClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CM.Text.BusinessMessaging;
using CM.Text.BusinessMessaging.Model;
using CM.Text.Common;
using CM.Text.Identity;
using CM.Text.Interfaces;
using JetBrains.Annotations;

Expand Down Expand Up @@ -126,5 +128,75 @@ await requestResult.Content.ReadAsStringAsync()
}
}
}

/// <summary>
/// Sends an One Time Password asynchronously.
/// </summary>
/// <param name="otpRequest">The otp to send.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns></returns>
[PublicAPI]
public async Task<OtpResult> SendOtpAsync(
OtpRequest otpRequest,
CancellationToken cancellationToken = default(CancellationToken))
{
using (var request = new HttpRequestMessage(
HttpMethod.Post,
_endPointOverride ?? new Uri(Constant.OtpRequestEndpoint)
))
{
request.Content = new StringContent(
JsonSerializer.Serialize(otpRequest),
Encoding.UTF8,
Constant.BusinessMessagingGatewayMediaTypeJson
);

return await SendOtpApiRequestAsync(request, cancellationToken);
}
}

/// <summary>
/// Checks an One Time Password asynchronously.
/// </summary>
/// <param name="id">id of the OTP to check.</param>
/// <param name="code">The code the end user used</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns></returns>
[PublicAPI]
public async Task<OtpResult> VerifyOtpAsync(
string id,
string code,
CancellationToken cancellationToken = default(CancellationToken))
{
using (var request = new HttpRequestMessage(
HttpMethod.Post,
_endPointOverride ?? new Uri(string.Format(Constant.OtpVerifyEndpointPrefix, id))
))
{
request.Content = new StringContent(
JsonSerializer.Serialize(new { code = code } ),
Encoding.UTF8,
Constant.BusinessMessagingGatewayMediaTypeJson
);

return await SendOtpApiRequestAsync(request, cancellationToken);
}
}

private async Task<OtpResult> SendOtpApiRequestAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Add("X-CM-ProductToken", _apiKey.ToString());
using (var requestResult = await _httpClient.SendAsync(request, cancellationToken)
.ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();

return JsonSerializer.Deserialize<OtpResult>(
await requestResult.Content.ReadAsStringAsync()
.ConfigureAwait(false)
);
}
}
}
}
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,15 +419,16 @@ var result = await client.SendMessageAsync(message);
## Using the OTP API
Send a simple OTP code
```cs
var result = await client.SendOtp("Sender_Name", "316012345678", "sms").ConfigureAwait(false);
var client = new TextClient(new Guid(ConfigurationManager.AppSettings["ApiKey"]));
var otpBuilder = new OtpRequestBuilder("Sender_name", "Recipient_PhoneNumber");
otpBuilder.WithMessage("Your otp code is {code}.");
var result = await textClient.SendOtpAsync(otpBuilder.Build());
```

Verify the response code
```cs
client.VerifyOtp("OTP-ID", "code")
var verifyResult = client.VerifyOtp("OTP-ID", "code");
bool isValid = verifyResult.Verified;
```

More advanced scenarios
```cs

```
For more advanced scenarios see also https://developers.cm.com/identity/docs/one-time-password-create

0 comments on commit 09bec63

Please sign in to comment.