Skip to content

Commit de001d8

Browse files
renemadsenclaude
andcommitted
feat: add generic gRPC auth service for AuthenticateUser and RefreshToken
Shared auth gRPC endpoints that can be used by any mobile client plugin, avoiding duplication of auth logic across plugins. Directly injects IAuthService — no reflection needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 414a91a commit de001d8

7 files changed

Lines changed: 308 additions & 0 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Threading.Tasks;
2+
using eFormAPI.Web.Abstractions;
3+
using eFormAPI.Web.Grpc;
4+
using eFormAPI.Web.Infrastructure.Models.Auth;
5+
using eFormAPI.Web.Services.GrpcServices;
6+
using eFormAPI.Web.Tests.Helpers;
7+
using Microting.eFormApi.BasePn.Infrastructure.Models.API;
8+
using Microting.eFormApi.BasePn.Infrastructure.Models.Auth;
9+
using NSubstitute;
10+
using NUnit.Framework;
11+
12+
namespace eFormAPI.Web.Tests.GrpcServices;
13+
14+
[TestFixture]
15+
public class EformAuthGrpcServiceTests
16+
{
17+
private IAuthService _authService;
18+
private EformAuthGrpcService _grpcService;
19+
20+
[SetUp]
21+
public void SetUp()
22+
{
23+
_authService = Substitute.For<IAuthService>();
24+
_grpcService = new EformAuthGrpcService(_authService);
25+
}
26+
27+
[Test]
28+
public async Task AuthenticateUser_Success_ReturnsTokenAndUser()
29+
{
30+
_authService.AuthenticateUser(Arg.Any<LoginModel>())
31+
.Returns(new OperationDataResult<EformAuthorizeResult>(
32+
true, "OK", new EformAuthorizeResult
33+
{
34+
AccessToken = "jwt-token-abc",
35+
FirstName = "John",
36+
LastName = "Doe"
37+
}));
38+
39+
var request = new AuthenticateUserRequest
40+
{
41+
Username = "user@test.com",
42+
Password = "password123"
43+
};
44+
45+
var response = await _grpcService.AuthenticateUser(
46+
request, TestServerCallContextFactory.Create());
47+
48+
Assert.That(response.Success, Is.True);
49+
Assert.That(response.Model, Is.Not.Null);
50+
Assert.That(response.Model.AccessToken, Is.EqualTo("jwt-token-abc"));
51+
Assert.That(response.Model.FirstName, Is.EqualTo("John"));
52+
Assert.That(response.Model.LastName, Is.EqualTo("Doe"));
53+
54+
await _authService.Received(1).AuthenticateUser(
55+
Arg.Is<LoginModel>(m => m.Username == "user@test.com" && m.Password == "password123"));
56+
}
57+
58+
[Test]
59+
public async Task AuthenticateUser_Failure_ReturnsError()
60+
{
61+
_authService.AuthenticateUser(Arg.Any<LoginModel>())
62+
.Returns(new OperationDataResult<EformAuthorizeResult>(
63+
false, "Invalid credentials"));
64+
65+
var request = new AuthenticateUserRequest
66+
{
67+
Username = "user@test.com",
68+
Password = "wrong"
69+
};
70+
71+
var response = await _grpcService.AuthenticateUser(
72+
request, TestServerCallContextFactory.Create());
73+
74+
Assert.That(response.Success, Is.False);
75+
Assert.That(response.Message, Is.EqualTo("Invalid credentials"));
76+
Assert.That(response.Model, Is.Null);
77+
}
78+
79+
[Test]
80+
public async Task RefreshToken_Success_ReturnsNewToken()
81+
{
82+
_authService.RefreshToken()
83+
.Returns(new OperationDataResult<EformAuthorizeResult>(
84+
true, "OK", new EformAuthorizeResult
85+
{
86+
AccessToken = "refreshed-token-xyz",
87+
FirstName = "Jane",
88+
LastName = "Smith"
89+
}));
90+
91+
var response = await _grpcService.RefreshToken(
92+
new RefreshTokenRequest(), TestServerCallContextFactory.Create());
93+
94+
Assert.That(response.Success, Is.True);
95+
Assert.That(response.Model, Is.Not.Null);
96+
Assert.That(response.Model.AccessToken, Is.EqualTo("refreshed-token-xyz"));
97+
Assert.That(response.Model.FirstName, Is.EqualTo("Jane"));
98+
Assert.That(response.Model.LastName, Is.EqualTo("Smith"));
99+
}
100+
101+
[Test]
102+
public async Task RefreshToken_Failure_ReturnsError()
103+
{
104+
_authService.RefreshToken()
105+
.Returns(new OperationDataResult<EformAuthorizeResult>(
106+
false, "Token expired"));
107+
108+
var response = await _grpcService.RefreshToken(
109+
new RefreshTokenRequest(), TestServerCallContextFactory.Create());
110+
111+
Assert.That(response.Success, Is.False);
112+
Assert.That(response.Message, Is.EqualTo("Token expired"));
113+
Assert.That(response.Model, Is.Null);
114+
}
115+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Grpc.Core;
6+
7+
namespace eFormAPI.Web.Tests.Helpers;
8+
9+
public static class TestServerCallContextFactory
10+
{
11+
public static ServerCallContext Create(
12+
Metadata requestHeaders = null,
13+
CancellationToken cancellationToken = default)
14+
{
15+
return new TestCallContext(requestHeaders ?? new Metadata(), cancellationToken);
16+
}
17+
18+
private class TestCallContext : ServerCallContext
19+
{
20+
public TestCallContext(Metadata requestHeaders, CancellationToken ct)
21+
{
22+
RequestHeadersCore = requestHeaders;
23+
CancellationTokenCore = ct;
24+
DeadlineCore = DateTime.UtcNow.AddHours(1);
25+
}
26+
27+
protected override string MethodCore => "TestMethod";
28+
protected override string HostCore => "localhost";
29+
protected override string PeerCore => "ipv4:127.0.0.1:0";
30+
protected override DateTime DeadlineCore { get; }
31+
protected override Metadata RequestHeadersCore { get; }
32+
protected override CancellationToken CancellationTokenCore { get; }
33+
protected override Metadata ResponseTrailersCore => new();
34+
protected override Status StatusCore { get; set; }
35+
protected override WriteOptions WriteOptionsCore { get; set; }
36+
37+
protected override AuthContext AuthContextCore =>
38+
new(string.Empty, new Dictionary<string, List<AuthProperty>>());
39+
40+
protected override ContextPropagationToken CreatePropagationTokenCore(
41+
ContextPropagationOptions options) => throw new NotImplementedException();
42+
43+
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) =>
44+
Task.CompletedTask;
45+
}
46+
}

eFormAPI/eFormAPI.Web.Tests/eFormAPI.Web.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1414
<PrivateAssets>all</PrivateAssets>
1515
</PackageReference>
16+
<PackageReference Include="NSubstitute" Version="5.3.0" />
1617
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
1718
</ItemGroup>
1819

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
syntax = "proto3";
2+
3+
package eform;
4+
5+
option csharp_namespace = "eFormAPI.Web.Grpc";
6+
7+
message AuthenticateUserRequest {
8+
string username = 1;
9+
string password = 2;
10+
string grant_type = 3;
11+
}
12+
13+
message AuthenticateUserResponse {
14+
bool success = 1;
15+
string message = 2;
16+
AuthUserModel model = 3;
17+
}
18+
19+
message AuthUserModel {
20+
string access_token = 1;
21+
string first_name = 2;
22+
string last_name = 3;
23+
}
24+
25+
message RefreshTokenRequest {}
26+
27+
message RefreshTokenResponse {
28+
bool success = 1;
29+
string message = 2;
30+
AuthUserModel model = 3;
31+
}
32+
33+
service EformAuthService {
34+
rpc AuthenticateUser(AuthenticateUserRequest) returns (AuthenticateUserResponse);
35+
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
36+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using eFormAPI.Web.Abstractions;
4+
using eFormAPI.Web.Grpc;
5+
using Grpc.Core;
6+
using Microting.eFormApi.BasePn.Infrastructure.Models.Auth;
7+
8+
namespace eFormAPI.Web.Services.GrpcServices;
9+
10+
public class EformAuthGrpcService : Grpc.EformAuthService.EformAuthServiceBase
11+
{
12+
private readonly IAuthService _authService;
13+
14+
public EformAuthGrpcService(IAuthService authService)
15+
{
16+
_authService = authService;
17+
}
18+
19+
public override async Task<AuthenticateUserResponse> AuthenticateUser(
20+
AuthenticateUserRequest request, ServerCallContext context)
21+
{
22+
try
23+
{
24+
var loginModel = new LoginModel
25+
{
26+
Username = request.Username,
27+
Password = request.Password
28+
};
29+
30+
var result = await _authService.AuthenticateUser(loginModel);
31+
32+
var response = new AuthenticateUserResponse
33+
{
34+
Success = result.Success,
35+
Message = result.Message ?? ""
36+
};
37+
38+
if (result.Success && result.Model != null)
39+
{
40+
response.Model = new AuthUserModel
41+
{
42+
AccessToken = result.Model.AccessToken ?? "",
43+
FirstName = result.Model.FirstName ?? "",
44+
LastName = result.Model.LastName ?? ""
45+
};
46+
}
47+
48+
return response;
49+
}
50+
catch (Exception ex)
51+
{
52+
return new AuthenticateUserResponse
53+
{
54+
Success = false,
55+
Message = ex.Message
56+
};
57+
}
58+
}
59+
60+
public override async Task<RefreshTokenResponse> RefreshToken(
61+
RefreshTokenRequest request, ServerCallContext context)
62+
{
63+
try
64+
{
65+
var result = await _authService.RefreshToken();
66+
67+
var response = new RefreshTokenResponse
68+
{
69+
Success = result.Success,
70+
Message = result.Message ?? ""
71+
};
72+
73+
if (result.Success && result.Model != null)
74+
{
75+
response.Model = new AuthUserModel
76+
{
77+
AccessToken = result.Model.AccessToken ?? "",
78+
FirstName = result.Model.FirstName ?? "",
79+
LastName = result.Model.LastName ?? ""
80+
};
81+
}
82+
83+
return response;
84+
}
85+
catch (Exception ex)
86+
{
87+
return new RefreshTokenResponse
88+
{
89+
Success = false,
90+
Message = ex.Message
91+
};
92+
}
93+
}
94+
}

eFormAPI/eFormAPI.Web/Startup.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ public void ConfigureServices(IServiceCollection services)
318318
}
319319
ConnectServices(services);
320320

321+
// gRPC
322+
services.AddGrpc();
323+
321324
// plugins
322325
services.AddEFormPlugins(Program.EnabledPlugins);
323326
}
@@ -381,6 +384,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
381384

382385
// Plugins
383386
app.UseEFormPlugins(Program.EnabledPlugins);
387+
388+
// gRPC
389+
app.UseRouting();
390+
app.UseEndpoints(endpoints =>
391+
{
392+
endpoints.MapGrpcService<Services.GrpcServices.EformAuthGrpcService>();
393+
});
394+
384395
// Route all unknown requests to app root
385396
app.UseAngularMiddleware(env);
386397
FixUserToWorkerLinks();

eFormAPI/eFormAPI.Web/eFormAPI.Web.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<PackageReference Include="Sentry" Version="6.3.0" />
6363
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
6464
<PackageReference Include="McMaster.NETCore.Plugins" Version="2.0.0" />
65+
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
6566
<PackageReference Include="sendgrid" Version="9.29.3" />
6667
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
6768
<PackageReference Include="System.Threading.AccessControl" Version="10.0.5" />
@@ -71,4 +72,8 @@
7172
<Folder Include="Plugins" />
7273
</ItemGroup>
7374

75+
<ItemGroup>
76+
<Protobuf Include="Protos\auth.proto" GrpcServices="Server" />
77+
</ItemGroup>
78+
7479
</Project>

0 commit comments

Comments
 (0)