Skip to content

Commit 79b4b6e

Browse files
jrmccannonTyrrrzJonas Hendrickx
authored
Adding Magic Links (#116)
Added Magic links to the SDK Co-authored-by: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Co-authored-by: Jonas Hendrickx <jhendrickx@bitwarden.com>
1 parent 1ba094c commit 79b4b6e

File tree

7 files changed

+87
-3
lines changed

7 files changed

+87
-3
lines changed

src/Passwordless/Helpers/PasswordlessSerializerContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ namespace Passwordless.Helpers;
2626
[JsonSerializable(typeof(JsonElement))]
2727
[JsonSerializable(typeof(GetEventLogRequest))]
2828
[JsonSerializable(typeof(GetEventLogResponse))]
29+
[JsonSerializable(typeof(SendMagicLinkApiRequest))]
2930
internal partial class PasswordlessSerializerContext : JsonSerializerContext;

src/Passwordless/IPasswordlessClient.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,12 @@ Task DeleteCredentialAsync(
110110
/// <param name="cancellationToken"></param>
111111
/// <returns></returns>
112112
Task<GetEventLogResponse> GetEventLogAsync(GetEventLogRequest request, CancellationToken cancellationToken = default);
113+
114+
/// <summary>
115+
/// Sends an email containing a magic link template allowing users to login.
116+
/// </summary>
117+
/// <param name="request"><see cref="SendMagicLinkRequest"/></param>
118+
/// <param name="cancellationToken"></param>
119+
/// <returns></returns>
120+
Task SendMagicLinkAsync(SendMagicLinkRequest request, CancellationToken cancellationToken = default);
113121
}

src/Passwordless/Models/GetEventLogRequest.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.ComponentModel.DataAnnotations;
2-
31
namespace Passwordless.Models;
42

53
/// <summary>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using Passwordless.Helpers.Extensions;
3+
4+
namespace Passwordless.Models;
5+
6+
/// <summary>
7+
/// Request for sending an email with a link that contains a 1-time-use token to be used for validating signin.
8+
/// </summary>
9+
/// <param name="EmailAddress">Valid email address that will be the recipient of the magic link email</param>
10+
/// <param name="UrlTemplate">Url template that needs to contain the token template, <token>. The token template will be replaced with a valid signin token to be sent to the verify sign in token endpoint (https://v4.passwsordless.dev/signin/verify).</param>
11+
/// <param name="UserId">Identifier for the user the email is intended for.</param>
12+
/// <param name="TimeToLive">Length of time the magic link will be active. Default value will be 15 minutes.</param>
13+
public record SendMagicLinkRequest(string EmailAddress, string UrlTemplate, string UserId, TimeSpan? TimeToLive)
14+
{
15+
internal SendMagicLinkApiRequest ToRequest() =>
16+
new(
17+
this.EmailAddress,
18+
this.UrlTemplate,
19+
this.UserId,
20+
this.TimeToLive?.TotalSeconds.Pipe(Convert.ToInt32));
21+
};
22+
23+
internal record SendMagicLinkApiRequest(string EmailAddress, string UrlTemplate, string UserId, int? TimeToLive);

src/Passwordless/PasswordlessClient.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ public async Task<GetEventLogResponse> GetEventLogAsync(GetEventLogRequest reque
115115
PasswordlessSerializerContext.Default.GetEventLogResponse,
116116
cancellationToken))!;
117117

118+
/// <inheritdoc />
119+
public async Task SendMagicLinkAsync(SendMagicLinkRequest request, CancellationToken cancellationToken = default)
120+
{
121+
using var response = await _http.PostAsJsonAsync(
122+
"magic-links/send",
123+
request.ToRequest(),
124+
PasswordlessSerializerContext.Default.SendMagicLinkApiRequest,
125+
cancellationToken);
126+
127+
response.EnsureSuccessStatusCode();
128+
}
129+
118130
/// <inheritdoc />
119131
public async Task<UsersCount> GetUsersCountAsync(CancellationToken cancellationToken = default) =>
120132
(await _http.GetFromJsonAsync(

tests/Passwordless.Tests.Infra/TestApi.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ public async Task<PasswordlessApplication> CreateAppAsync()
9696
{
9797
"adminEmail": "test@passwordless.dev",
9898
"eventLoggingIsEnabled": true,
99-
"eventLoggingRetentionPeriod": 7
99+
"eventLoggingRetentionPeriod": 7,
100+
"magicLinkEmailMonthlyQuota" : 100
100101
}
101102
""",
102103
Encoding.UTF8,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using Passwordless.Models;
6+
using Passwordless.Tests.Infra;
7+
using Xunit;
8+
using Xunit.Abstractions;
9+
10+
namespace Passwordless.Tests;
11+
12+
public class MagicLinksTests(TestApiFixture api, ITestOutputHelper testOutput) : ApiTestBase(api, testOutput)
13+
{
14+
[Fact]
15+
public async Task I_can_send_a_magic_link_with_a_specified_time_to_live()
16+
{
17+
// Arrange
18+
var passwordless = await Api.CreateClientAsync();
19+
var request = new SendMagicLinkRequest("test@passwordless.dev", "https://www.example.com?token=$TOKEN", "user", new TimeSpan(0, 15, 0));
20+
21+
// Act
22+
var action = async () => await passwordless.SendMagicLinkAsync(request, CancellationToken.None);
23+
24+
// Assert
25+
await action.Should().NotThrowAsync();
26+
}
27+
28+
[Fact]
29+
public async Task I_can_send_a_magic_link_without_a_time_to_live()
30+
{
31+
// Arrange
32+
var passwordless = await Api.CreateClientAsync();
33+
var request = new SendMagicLinkRequest("test@passwordless.dev", "https://www.example.com?token=$TOKEN", "user", null);
34+
35+
// Act
36+
var action = async () => await passwordless.SendMagicLinkAsync(request, CancellationToken.None);
37+
38+
// Assert
39+
await action.Should().NotThrowAsync();
40+
}
41+
}

0 commit comments

Comments
 (0)