1- using System ;
2- using System . Net ;
1+ using EasyExtensions . Abstractions ;
2+ using EasyExtensions . AspNetCore . Authorization . Abstractions ;
3+ using EasyExtensions . AspNetCore . Authorization . Models . Dto ;
4+ using EasyExtensions . AspNetCore . Authorization . Models . Dto . Enums ;
5+ using EasyExtensions . AspNetCore . Extensions ;
36using EasyExtensions . Helpers ;
4- using System . Threading . Tasks ;
5- using System . Security . Claims ;
7+ using Microsoft . AspNetCore . Authorization ;
68using Microsoft . AspNetCore . Mvc ;
9+ using System ;
710using System . Collections . Generic ;
8- using EasyExtensions . Abstractions ;
911using System . IdentityModel . Tokens . Jwt ;
10- using Microsoft . AspNetCore . Authorization ;
11- using EasyExtensions . AspNetCore . Extensions ;
12- using EasyExtensions . AspNetCore . Authorization . Models . Dto ;
13- using EasyExtensions . AspNetCore . Authorization . Abstractions ;
14- using EasyExtensions . AspNetCore . Authorization . Models . Dto . Enums ;
12+ using System . Net ;
13+ using System . Net . Http ;
14+ using System . Net . Http . Json ;
15+ using System . Security . Claims ;
16+ using System . Threading . Tasks ;
1517
1618namespace EasyExtensions . AspNetCore . Authorization . Controllers
1719{
@@ -90,6 +92,80 @@ public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDto
9092 } ) ;
9193 }
9294
95+ /// <summary>
96+ /// Authenticates a user using the provided credentials and issues a new access and refresh token pair.
97+ /// </summary>
98+ /// <remarks>This endpoint is typically used as part of a username and password authentication
99+ /// flow. The returned tokens can be used to access protected resources and to refresh authentication without
100+ /// re-entering credentials. Repeated failed login attempts may be subject to rate limiting or account lockout
101+ /// policies, depending on system configuration.</remarks>
102+ /// <param name="request">The login request containing the user's username and password. Cannot be null.</param>
103+ /// <returns>An <see cref="IActionResult"/> containing a <see cref="TokenPairDto"/> with access and refresh tokens if
104+ /// authentication is successful; otherwise, an unauthorized response if the credentials are invalid.</returns>
105+ [ HttpPost ( "login" ) ]
106+ public async Task < IActionResult > Login ( [ FromBody ] LoginRequestDto request )
107+ {
108+ Guid ? userId = await FindUserByUsernameAsync ( request . Username ) ;
109+ if ( ! userId . HasValue || userId == Guid . Empty )
110+ {
111+ return this . ApiUnauthorized ( "Invalid username or password" ) ;
112+ }
113+ string ? phc = await FindUserPhcAsync ( userId . Value ) ;
114+ if ( string . IsNullOrWhiteSpace ( phc ) )
115+ {
116+ return this . ApiUnauthorized ( "Invalid username or password" ) ;
117+ }
118+ bool isValidPassword = _passwordHasher . Verify ( request . Password , phc ) ;
119+ if ( ! isValidPassword )
120+ {
121+ return this . ApiUnauthorized ( "Invalid username or password" ) ;
122+ }
123+ var roles = await GetUserRolesAsync ( userId . Value ) ;
124+ string accessToken = CreateAccessToken ( userId . Value , roles ) ;
125+ string refreshToken = StringHelpers . CreateRandomString ( 64 ) ;
126+ await SaveAndRevokeRefreshTokenAsync ( userId . Value , string . Empty , refreshToken ) ;
127+ await OnUserLoggedInAsync ( userId . Value , AuthType . Credentials ) ;
128+ return Ok ( new TokenPairDto
129+ {
130+ AccessToken = accessToken ,
131+ RefreshToken = refreshToken
132+ } ) ;
133+ }
134+
135+ [ HttpPost ( "login/google" ) ]
136+ public async Task < IActionResult > Login ( [ FromQuery ] string token )
137+ {
138+ const string url = "https://openidconnect.googleapis.com/v1/userinfo" ;
139+ using HttpClient http = new ( ) ;
140+ http . DefaultRequestHeaders . Authorization = new ( "Bearer" , token ) ;
141+ GoogleOpenIdResponseDto response = await http . GetFromJsonAsync < GoogleOpenIdResponseDto > ( url )
142+ ?? throw new InvalidOperationException ( "Failed to get user info from Google." ) ;
143+ bool mustVerifyEmail = IsEmailVerificationRequired ( ) ;
144+ if ( mustVerifyEmail && ! response . IsEmailVerified )
145+ {
146+ return this . ApiUnauthorized ( "Email is not verified" ) ;
147+ }
148+ Guid ? userId = await FindUserByUsernameAsync ( response . Email ) ;
149+ if ( ! userId . HasValue || userId == Guid . Empty )
150+ {
151+ userId = await CreateUserFromGoogleAsync ( response ) ;
152+ if ( ! userId . HasValue || userId == Guid . Empty )
153+ {
154+ return this . ApiUnauthorized ( "User not found" ) ;
155+ }
156+ }
157+ var roles = await GetUserRolesAsync ( userId . Value ) ;
158+ string accessToken = CreateAccessToken ( userId . Value , roles ) ;
159+ string refreshToken = StringHelpers . CreateRandomString ( 64 ) ;
160+ await SaveAndRevokeRefreshTokenAsync ( userId . Value , string . Empty , refreshToken ) ;
161+ await OnUserLoggedInAsync ( userId . Value , AuthType . Google ) ;
162+ return Ok ( new TokenPairDto
163+ {
164+ AccessToken = accessToken ,
165+ RefreshToken = refreshToken
166+ } ) ;
167+ }
168+
93169 private string CreateAccessToken ( Guid userId , IEnumerable < string > roles )
94170 {
95171 return _tokenProvider . CreateToken ( cb =>
@@ -103,6 +179,14 @@ private string CreateAccessToken(Guid userId, IEnumerable<string> roles)
103179 } ) ;
104180 }
105181
182+ /// <summary>
183+ /// Asynchronously searches for a user by username and returns the user's unique identifier if found.
184+ /// </summary>
185+ /// <param name="username">The username of the user to locate. Cannot be null or empty.</param>
186+ /// <returns>A task that represents the asynchronous operation. The task result contains the unique identifier of the
187+ /// user if found; otherwise, null.</returns>
188+ public abstract Task < Guid ? > FindUserByUsernameAsync ( string username ) ;
189+
106190 /// <summary>
107191 /// Saves a new refresh token for the specified user and revokes the previous refresh token asynchronously.
108192 /// </summary>
@@ -169,5 +253,36 @@ public virtual Task OnTokenRefreshedAsync(Guid userId, string newRefreshToken)
169253 {
170254 return Task . CompletedTask ;
171255 }
256+
257+ /// <summary>
258+ /// Handles logic to be performed when a user has successfully logged in.
259+ /// </summary>
260+ /// <param name="userId">The unique identifier of the user who has logged in.</param>
261+ /// <param name="authType">The type of authentication used for login.</param>
262+ /// <returns>A task that represents the asynchronous operation.</returns>
263+ public virtual Task OnUserLoggedInAsync ( Guid userId , AuthType authType )
264+ {
265+ return Task . CompletedTask ;
266+ }
267+
268+ /// <summary>
269+ /// Determines whether email verification is required for user accounts.
270+ /// </summary>
271+ /// <returns><see langword="true"/> if email verification is required; otherwise, <see langword="false"/>.</returns>
272+ public virtual bool IsEmailVerificationRequired ( )
273+ {
274+ return true ;
275+ }
276+
277+ /// <summary>
278+ /// Creates a new user account based on the information provided by a Google OpenID response.
279+ /// </summary>
280+ /// <param name="dto">The Google OpenID response data containing user information to create the account. Cannot be null.</param>
281+ /// <returns>A task that represents the asynchronous operation. The task result contains the unique identifier of the
282+ /// newly created user if successful; otherwise, null.</returns>
283+ public virtual Task < Guid ? > CreateUserFromGoogleAsync ( GoogleOpenIdResponseDto dto )
284+ {
285+ return Task . FromResult < Guid ? > ( null ) ;
286+ }
172287 }
173288}
0 commit comments