@@ -24,6 +24,8 @@ public abstract class BaseAuthController(
2424 IPasswordHashService _passwordHasher ,
2525 ITokenProvider _tokenProvider ) : ControllerBase
2626 {
27+ private const string CookieRefreshTokenName = "ee_refresh_token" ;
28+
2729 /// <summary>
2830 /// Gets the IP address from which the current request originated.
2931 /// </summary>
@@ -71,8 +73,24 @@ public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest
7173 /// <returns>An <see cref="IActionResult"/> containing the new access token if the refresh token is valid; otherwise, a
7274 /// 404 Not Found result if the token is invalid or revoked.</returns>
7375 [ HttpPost ( "refresh" ) ]
74- public async Task < IActionResult > RefreshToken ( [ FromBody ] RefreshTokenRequestDto request )
76+ public async Task < IActionResult > RefreshToken ( [ FromBody ] RefreshTokenRequestDto ? request )
7577 {
78+ bool useCookie = string . IsNullOrWhiteSpace ( request ? . RefreshToken ) ;
79+ if ( useCookie )
80+ {
81+ if ( Request . Cookies . TryGetValue ( CookieRefreshTokenName , out string ? cookieRefreshToken ) )
82+ {
83+ request = request is not null
84+ ? request with { RefreshToken = cookieRefreshToken }
85+ : new RefreshTokenRequestDto { RefreshToken = cookieRefreshToken } ;
86+ }
87+ }
88+
89+ if ( string . IsNullOrWhiteSpace ( request ? . RefreshToken ) )
90+ {
91+ return this . ApiNotFound ( "Refresh token was not found" ) ;
92+ }
93+
7694 Guid ? userId = await FindUserByRefreshTokenAsync ( request . RefreshToken ) ;
7795 if ( ! userId . HasValue || userId == Guid . Empty )
7896 {
@@ -83,10 +101,17 @@ public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDto
83101 var roles = await GetUserRolesAsync ( userId . Value ) ;
84102 string accessToken = CreateAccessToken ( userId . Value , roles ) ;
85103 await SaveAndRevokeRefreshTokenAsync ( userId . Value , request . RefreshToken , newRefreshToken , AuthType . Unknown ) ;
86- return Ok ( new TokenPairDto
104+ Response . Cookies . Append ( CookieRefreshTokenName , newRefreshToken , new ( )
105+ {
106+ Secure = true ,
107+ HttpOnly = true ,
108+ SameSite = Microsoft . AspNetCore . Http . SameSiteMode . Strict ,
109+ Expires = DateTimeOffset . UtcNow . Add ( GetCookieExpirationTime ( ) ) ,
110+ } ) ;
111+ return Ok ( new TokenPairResponseDto
87112 {
88113 AccessToken = accessToken ,
89- RefreshToken = newRefreshToken
114+ RefreshToken = useCookie ? StringHelpers . CreateRandomString ( 64 ) : newRefreshToken
90115 } ) ;
91116 }
92117
@@ -98,7 +123,7 @@ public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDto
98123 /// re-entering credentials. Repeated failed login attempts may be subject to rate limiting or account lockout
99124 /// policies, depending on system configuration.</remarks>
100125 /// <param name="request">The login request containing the user's username and password. Cannot be null.</param>
101- /// <returns>An <see cref="IActionResult"/> containing a <see cref="TokenPairDto "/> with access and refresh tokens if
126+ /// <returns>An <see cref="IActionResult"/> containing a <see cref="TokenPairResponseDto "/> with access and refresh tokens if
102127 /// authentication is successful; otherwise, an unauthorized response if the credentials are invalid.</returns>
103128 [ HttpPost ( "login" ) ]
104129 public async Task < IActionResult > Login ( [ FromBody ] LoginRequestDto request )
@@ -132,7 +157,14 @@ public async Task<IActionResult> Login([FromBody] LoginRequestDto request)
132157 string refreshToken = StringHelpers . CreateRandomString ( 64 ) ;
133158 await SaveAndRevokeRefreshTokenAsync ( userId . Value , string . Empty , refreshToken , AuthType . Credentials ) ;
134159 await OnUserLoggingInAsync ( userId . Value , AuthType . Credentials , AuthRejectionType . None ) ;
135- return Ok ( new TokenPairDto
160+ Response . Cookies . Append ( CookieRefreshTokenName , refreshToken , new ( )
161+ {
162+ Secure = true ,
163+ HttpOnly = true ,
164+ SameSite = Microsoft . AspNetCore . Http . SameSiteMode . Strict ,
165+ Expires = DateTimeOffset . UtcNow . Add ( GetCookieExpirationTime ( ) ) ,
166+ } ) ;
167+ return Ok ( new TokenPairResponseDto
136168 {
137169 AccessToken = accessToken ,
138170 RefreshToken = refreshToken
@@ -148,7 +180,7 @@ public async Task<IActionResult> Login([FromBody] LoginRequestDto request)
148180 /// The method issues new tokens and revokes any previous refresh tokens for the user.</remarks>
149181 /// <param name="token">The Google OAuth access token to use for retrieving user information. Must be a valid token issued by
150182 /// Google.</param>
151- /// <returns>An <see cref="IActionResult"/> containing a <see cref="TokenPairDto "/> with access and refresh tokens if
183+ /// <returns>An <see cref="IActionResult"/> containing a <see cref="TokenPairResponseDto "/> with access and refresh tokens if
152184 /// authentication is successful; otherwise, an unauthorized response if authentication fails or the user's
153185 /// email is not verified.</returns>
154186 /// <exception cref="InvalidOperationException">Thrown if user information cannot be retrieved from Google using the provided token.</exception>
@@ -181,7 +213,14 @@ public async Task<IActionResult> LoginWithGoogle([FromQuery] string token)
181213 string refreshToken = StringHelpers . CreateRandomString ( 64 ) ;
182214 await SaveAndRevokeRefreshTokenAsync ( userId . Value , string . Empty , refreshToken , AuthType . Google ) ;
183215 await OnUserLoggingInAsync ( userId . Value , AuthType . Google , AuthRejectionType . None ) ;
184- return Ok ( new TokenPairDto
216+ Response . Cookies . Append ( CookieRefreshTokenName , refreshToken , new ( )
217+ {
218+ Secure = true ,
219+ HttpOnly = true ,
220+ SameSite = Microsoft . AspNetCore . Http . SameSiteMode . Strict ,
221+ Expires = DateTimeOffset . UtcNow . Add ( GetCookieExpirationTime ( ) ) ,
222+ } ) ;
223+ return Ok ( new TokenPairResponseDto
185224 {
186225 AccessToken = accessToken ,
187226 RefreshToken = refreshToken
@@ -294,6 +333,15 @@ public virtual bool IsEmailVerificationRequired()
294333 return Task . FromResult < Guid ? > ( null ) ;
295334 }
296335
336+ /// <summary>
337+ /// Returns the default expiration time span for cookies issued by the application.
338+ /// </summary>
339+ /// <returns>A <see cref="TimeSpan"/> representing the duration after which a cookie expires. The default is 30 days.</returns>
340+ public virtual TimeSpan GetCookieExpirationTime ( )
341+ {
342+ return TimeSpan . FromDays ( 30 ) ;
343+ }
344+
297345 private string CreateAccessToken ( Guid userId , IEnumerable < string > roles )
298346 {
299347 return _tokenProvider . CreateToken ( cb =>
0 commit comments