Skip to content

Commit 638642e

Browse files
authored
Merge pull request #68 from ApplETS/feature/authentification-authentik
Authentification et Première Connexion au Serveur
2 parents 55e6b83 + 06b7b7a commit 638642e

6 files changed

Lines changed: 160 additions & 27 deletions

File tree

core/Extensions/DependencyInjectionExtension.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static IServiceCollection AddDependencyInjection(this IServiceCollection
4141
services.AddTransient<IImageService, ImageService>();
4242
services.AddTransient<ISubscriptionService, SubscriptionService>();
4343
services.AddTransient<INotificationService, NotificationService>();
44+
services.AddTransient<IIdentityProviderService, AuthentikService>();
4445

4546
// Utils
4647
services.AddTransient<IJwtUtils, JwtUtils>();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace api.core.Services.Abstractions;
2+
3+
/// <summary>
4+
/// Interface to communicate with any ID Provider
5+
/// </summary>
6+
public interface IIdentityProviderService
7+
{
8+
/// <summary>
9+
/// Fetches User's information from the ID Provider
10+
/// </summary>
11+
/// <param name="accessHeader">Header used to call the endpoint</param>
12+
/// <returns>If result is null, UserInfo endpoint cannot be accessed</returns>
13+
UserInfoDto? GetUserInfo(string accessHeader);
14+
}
15+
16+
public class UserInfoDto
17+
{
18+
public string Sub { get; set; } = null!;
19+
public string Email { get; set; } = null!;
20+
public bool EmailVerified { get; set; }
21+
public string Name { get; set; } = null!;
22+
public string GivenName { get; set; } = null!;
23+
public string PreferedUsername { get; set; } = null!;
24+
public string Nickname { get; set; } = null!;
25+
public List<string> Groups { get; set; } = null!;
26+
}

core/Services/Abstractions/IUserService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Diagnostics.Tracing;
1+
using System.Diagnostics.Tracing;
22

33
using api.core.Data.Enums;
44
using api.core.Data.requests;

core/Services/AuthentikService.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using api.core.Services.Abstractions;
2+
3+
using System.Text.Json;
4+
5+
namespace api.core.Services;
6+
7+
/// <summary>
8+
/// Service used to communicate with Authentik ID Provider
9+
/// </summary>
10+
public class AuthentikService : IIdentityProviderService
11+
{
12+
public UserInfoDto? GetUserInfo(string accessHeader)
13+
{
14+
using HttpClient client = new HttpClient()
15+
{
16+
BaseAddress = new Uri(Environment.GetEnvironmentVariable("OPENID_BASE_URL"))
17+
};
18+
19+
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "userinfo/");
20+
request.Headers.Add("Authorization", accessHeader);
21+
22+
// TODO : Changer pour un comportement asynchrone
23+
var response = client.SendAsync(request).Result;
24+
25+
if (!response.IsSuccessStatusCode)
26+
{
27+
return null;
28+
}
29+
30+
return response.Content.ReadFromJsonAsync<UserInfoDto>(new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }).Result;
31+
}
32+
}

core/Services/UserService.cs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using api.core.data.entities;
1+
using api.core.data.entities;
22
using api.core.Data.Exceptions;
33
using api.core.Data.Enums;
44
using api.core.Data.requests;
@@ -12,6 +12,7 @@
1212
using api.core.Misc;
1313
using api.core.Repositories.Abstractions;
1414
using api.core.Data.Entities;
15+
using api.core.Services.Abstractions;
1516

1617
namespace api.core.Services;
1718

@@ -21,7 +22,8 @@ public class UserService(
2122
ITagRepository tagRepository,
2223
IActivityAreaRepository activityAreaRepository,
2324
IImageService imageService,
24-
IJwtUtils jwtUtils) : IUserService
25+
IJwtUtils jwtUtils,
26+
IIdentityProviderService identityProvider) : IUserService
2527
{
2628
private const string AVATAR_FILE_NAME = "avatar.webp";
2729

@@ -57,16 +59,11 @@ public UserResponseDTO AddOrganizer(string id, UserCreateDTO organizerDto)
5759
public UserResponseDTO GetUser(string authHeader)
5860
{
5961
string userId = jwtUtils.GetUserIdFromAuthHeader(authHeader);
60-
UserResponseDTO? userRes = null;
61-
var organizer = userRepository.GetOrganizer(userId);
62-
if (organizer != null)
63-
userRes = UserResponseDTO.Map(organizer!);
62+
var user = userRepository.Get(userId);
6463

65-
var moderator = userRepository.GetModerator(userId);
66-
if (moderator != null)
67-
userRes = UserResponseDTO.Map(moderator!);
68-
69-
if (userRes == null) throw new Exception("No users associated with this ID");
64+
UserResponseDTO? userRes = user == null ?
65+
AddUser(authHeader, userId) :
66+
UserResponseDTO.Map(user);
7067

7168
var fields = tagRepository.GetInterestFieldsForOrganizer(userId);
7269
userRes.FieldsOfInterests = fields;
@@ -166,4 +163,31 @@ public string UpdateUserAvatar(string id, IFormFile avatarFile)
166163
var url = fileShareService.FileGetDownloadUri($"{userId}/{AVATAR_FILE_NAME}");
167164
return url.ToString();
168165
}
166+
167+
/// <summary>
168+
/// Adds a new user by calling the ID Provider endpoint for the users' information
169+
/// </summary>
170+
/// <param name="authHeader">The header used for calling de ID Provider's endpoint</param>
171+
/// <param name="userId">User to add</param>
172+
/// <returns>Formatted DTO of User's information</returns>
173+
private UserResponseDTO AddUser(string authHeader, string userId)
174+
{
175+
UserInfoDto? userInfo = identityProvider.GetUserInfo(authHeader);
176+
177+
if (userInfo == null)
178+
{
179+
throw new Exception("No users associated with this ID");
180+
}
181+
182+
User addedUser = userRepository.Add(new User
183+
{
184+
Email = userInfo.Email,
185+
Id = userId,
186+
ProfileDescription = userInfo.GivenName,
187+
Organization = string.Empty,
188+
IsActive = true
189+
});
190+
191+
return UserResponseDTO.Map(addedUser);
192+
}
169193
}

tests/Tests/Services/UserServiceTests.cs

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using api.core.Repositories.Abstractions;
1616
using Microsoft.IdentityModel.JsonWebTokens;
1717
using api.core.Misc;
18+
using api.core.Services.Abstractions;
1819

1920
namespace api.tests.Tests.Services;
2021
public class UserServiceTests
@@ -25,6 +26,7 @@ public class UserServiceTests
2526
private readonly Mock<IFileShareService> _fileShareServiceMock;
2627
private readonly Mock<IImageService> _imageServiceMock;
2728
private readonly Mock<IJwtUtils> _jwtUtilsMock;
29+
private readonly Mock<IIdentityProviderService> _providerServiceMock;
2830
private readonly UserService _userService;
2931

3032
public UserServiceTests()
@@ -35,6 +37,7 @@ public UserServiceTests()
3537
_fileShareServiceMock = new Mock<IFileShareService>();
3638
_imageServiceMock = new Mock<IImageService>();
3739
_jwtUtilsMock = new Mock<IJwtUtils>();
40+
_providerServiceMock = new Mock<IIdentityProviderService>();
3841

3942
_fileShareServiceMock.Setup(service => service.FileGetDownloadUri(It.IsAny<string>())).Returns(new Uri("http://example.com/avatar.webp"));
4043
_userService = new UserService(
@@ -43,7 +46,8 @@ public UserServiceTests()
4346
_tagRepositoryMock.Object,
4447
_activityAreaRepositoryMock.Object,
4548
_imageServiceMock.Object,
46-
_jwtUtilsMock.Object);
49+
_jwtUtilsMock.Object,
50+
_providerServiceMock.Object);
4751
}
4852

4953
[Fact]
@@ -112,7 +116,7 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenOrganizerIsFoundById()
112116
Role = UserRole.Organizer
113117
};
114118

115-
_userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(organizer);
119+
_userRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(organizer);
116120
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId);
117121

118122
// Act
@@ -123,7 +127,7 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenOrganizerIsFoundById()
123127
result.Id.Should().Be(organizerId);
124128
result.Email.Should().Be(organizer.Email);
125129

126-
_userRepositoryMock.Verify(repo => repo.GetOrganizer(organizerId), Times.Once);
130+
_userRepositoryMock.Verify(repo => repo.Get(organizerId), Times.Once);
127131
}
128132

129133
[Fact]
@@ -141,8 +145,7 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenModeratorIsFoundById()
141145
};
142146

143147
_jwtUtilsMock.Setup(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(moderatorId)).Returns(moderatorId);
144-
_userRepositoryMock.Setup(repo => repo.GetOrganizer(moderatorId)).Returns((User?)null); // Simulate no organizer found
145-
_userRepositoryMock.Setup(repo => repo.GetModerator(moderatorId)).Returns(moderator); // Simulate moderator found
148+
_userRepositoryMock.Setup(repo => repo.Get(moderatorId)).Returns(moderator); // Simulate moderator found
146149

147150
// Act
148151
var result = _userService.GetUser(moderatorId);
@@ -152,29 +155,76 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenModeratorIsFoundById()
152155
result.Id.Should().Be(moderatorId);
153156
result.Email.Should().Be(moderator.Email);
154157

155-
_userRepositoryMock.Verify(repo => repo.GetOrganizer(moderatorId), Times.Once);
156-
_userRepositoryMock.Verify(repo => repo.GetModerator(moderatorId), Times.Once);
158+
_userRepositoryMock.Verify(repo => repo.Get(moderatorId), Times.Once);
157159
_jwtUtilsMock.Verify(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(moderatorId), Times.Once);
158160
}
159161

160162
[Fact]
161-
public void GetUser_ShouldThrowException_WhenNoUserIsAssociatedWithProvidedId()
163+
public void GetUser_ShouldReturnUserResponseDTO_WhenNewUserIsFoundById()
164+
{
165+
// Arrange
166+
var userId = "userId";
167+
var user = new User
168+
{
169+
Id = userId,
170+
Email = "jane.doe@example.com",
171+
CreatedAt = DateTime.UtcNow,
172+
UpdatedAt = DateTime.UtcNow
173+
};
174+
175+
var userInfo = new UserInfoDto
176+
{
177+
Email = "jane.doe@example.com",
178+
EmailVerified = true,
179+
GivenName = "Jane Doe",
180+
Name = "Jane Doe",
181+
Nickname = "jane.doe",
182+
PreferedUsername = "jane.doe",
183+
Sub = userId
184+
};
185+
186+
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(userId)).Returns(userId);
187+
_userRepositoryMock.Setup(repo => repo.Get(userId)).Returns(null as User);
188+
_providerServiceMock.Setup(provider => provider.GetUserInfo(userId)).Returns(userInfo);
189+
_userRepositoryMock.Setup(repo =>
190+
repo.Add(It.Is<User>(u => u.Id == userInfo.Sub && u.Email == userInfo.Email && u.ProfileDescription == userInfo.GivenName)))
191+
.Returns(user);
192+
193+
// Act
194+
var result = _userService.GetUser(userId);
195+
196+
// Assert
197+
result.Should().NotBeNull();
198+
result.Type.Should().BeEmpty();
199+
result.Id.Should().Be(user.Id);
200+
result.Email.Should().Be(user.Email);
201+
202+
_jwtUtilsMock.Verify(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(userId), Times.Once);
203+
_userRepositoryMock.Verify(repo => repo.Get(userId), Times.Once);
204+
_userRepositoryMock.Verify(repo => repo.Add(It.IsAny<User>()), Times.Once);
205+
_providerServiceMock.Verify(provider => provider.GetUserInfo(userId), Times.Once);
206+
}
207+
208+
[Fact]
209+
public void GetUser_ShouldThrowException_WhenNoUserIsAssociatedWithProvidedIdNorValid()
162210
{
163211
// Arrange
164212
var userId = "nobody";
165213

166214
// Setup both organizer and moderator repositories to return null, simulating that no user is found with the provided ID
167-
_userRepositoryMock.Setup(repo => repo.GetOrganizer(userId)).Returns(null as User);
168-
_userRepositoryMock.Setup(repo => repo.GetModerator(userId)).Returns(null as User);
215+
_userRepositoryMock.Setup(repo => repo.Get(userId)).Returns(null as User);
169216
_jwtUtilsMock.Setup(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(userId)).Returns(userId);
170217

218+
// Setup the provider call as if the call was invalid (no connection or invalid token)
219+
_providerServiceMock.Setup(provider => provider.GetUserInfo(userId)).Returns(null as UserInfoDto);
220+
171221
// Act
172222
Action act = () => _userService.GetUser(userId);
173223

174224
// Assert
175225
act.Should().Throw<Exception>().WithMessage("No users associated with this ID");
176-
_userRepositoryMock.Verify(repo => repo.GetOrganizer(userId), Times.Once);
177-
_userRepositoryMock.Verify(repo => repo.GetOrganizer(userId), Times.Once);
226+
_userRepositoryMock.Verify(repo => repo.Get(userId), Times.Once);
227+
_providerServiceMock.Verify(provider => provider.GetUserInfo(userId), Times.Once);
178228
}
179229

180230

@@ -209,7 +259,7 @@ public void UpdateUser_ShouldReturnTrue_WhenOrganizerIsUpdatedSuccessfully()
209259
};
210260

211261
_activityAreaRepositoryMock.Setup(repo => repo.Get(actAreaIdModified)).Returns(activity); // Simulate activity area found
212-
_userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(existingOrganizer);
262+
_userRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(existingOrganizer);
213263
_userRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny<User>())).Returns(true);
214264
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId);
215265

@@ -250,7 +300,7 @@ public void UpdateUser_ShouldThrow_WhenActivityAreaIsNotFoundInTheList()
250300
UpdatedAt = DateTime.UtcNow
251301
};
252302
_activityAreaRepositoryMock.Setup(repo => repo.Get(badActAreaIdModified)).Returns(null as ActivityArea); // Simulate activity area not found
253-
_userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(existingOrganizer);
303+
_userRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(existingOrganizer);
254304
_userRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny<User>())).Returns(true);
255305
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId);
256306

@@ -279,7 +329,7 @@ public void UpdateUser_ShouldReturnTrue_WhenModeratorIsUpdatedSuccessfully()
279329
Role = UserRole.Moderator
280330
};
281331

282-
_userRepositoryMock.Setup(repo => repo.GetModerator(moderatorId)).Returns(existingModerator); // Simulate moderator found
332+
_userRepositoryMock.Setup(repo => repo.Get(moderatorId)).Returns(existingModerator); // Simulate moderator found
283333
_userRepositoryMock.Setup(repo => repo.Update(moderatorId, It.IsAny<User>())).Returns(true);
284334
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(moderatorId)).Returns(moderatorId);
285335

0 commit comments

Comments
 (0)