I made the login method like this:
public async Task<IActionResult> Login([FromBody] LoginUserDTO userDTO)
{
var res = await _authManager.ValidateUser(userDTO);
if (!res) return Unauthorized();
await _authManager.SetLoginInfo(userDTO, Request);
return Accepted(new { Token = await _authManager.CreateToken() });
}
public async Task<string> CreateToken()
{
var signingCredentials = GetSigningCredentials();
var claims = await GetClaims();
var token = GenerateTokenOptions(signingCredentials, claims);
return new JwtSecurityTokenHandler().WriteToken(token);
}
How can I create an endpoint for Logout?
In ASP, there is no such thing as logging out from a JWT on the server.
A JWT is a token that has an expiry date and is issued by the server (or a trusted third-party). It is then cached by the client and sent to the server by the client in the header of subsequent requests and is then validated by the server to ensure that it is both valid and not expired.
If the expiry is reached, then the server will return a 401 - Unauthorised response.
If you want to log a client out then you just remove the client side cached token so that it cannot be sent in the header of any future requests.
Related
I have a usual (tutorial-like) piece of code in Azure Service App. The HomeController is initialized as:
public HomeController(ILogger<HomeController> logger, GraphServiceClient graphServiceClient, ITokenAcquisition tokenAcquisition)
{
var task = Task.Run(async () => await m_tokenAcquisition.GetAuthenticationResultForUserAsync(new[] { "some.allowed.scope" }));
var context = task.Result;
var accessToken = context.AccessToken;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Add("FeatureFlag", "00000004");
var newGraphClient = new GraphServiceClient(client);
}
The access token is good to go for 'GraphServiceClient'.
There is no use for the access token after it expires which happens in an hour or so. But the service needs to do periodic work on Azure account without bothering the user.
The main question is how should I proceed to prevent the user from frequent logins?
To access the refreshed token without asking users to login.
You need to customize the code for refreshing the token in application with the timer by checking the token expiry before it happens or by adding a listener for the token expiry event.
_timer = new Timer(TokenRefresh, null, _expiresIn * 1000 - 60000, Timeout.Infinite);
using (var client = new HttpClient())
{
var response = await client.PostAsync("https://authserver.com/token", new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grantType", "tokenRefresh"),
new KeyValuePair<string, string>("tokenRefresh", _tokenRefresh)
}));
var responseContent = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(responseContent);
_accessToken = (string)json["accessToken"];
_tokenRefresh = (string)json["tokenRefresh"];
_expiresIn = (int)json["expiresIn"];
//Expires in 1 hrs (3600)
}
_timer.Change(_expiresIn * 1000 - 60000, Timeout.Infinite);
You need to log into the application.
And the application has to send the login credentials to an authentication serverand has to verify them. And then returns an access token, refresh token.
Now you have the access token and refresh token securely on the client side.
When the access token expires, the application uses the refresh token to request a new access token from the authentication server.
The authentication server checks the refresh token and returns a new access token.
You need to customize the code for refreshing the token in application with the timer by checking the token expiry before it happens or by adding a listener for the token expiry event or delegate
As the refresh token has a shortlife, you need to use the new refresh token obtained from the token refresh process to get new access token again.
For Safety the refresh token and access token have to be stored securely and encrypted.
References taken from
Token Requests
Github Code
So I decided to create a simple web app which sends a login request to my login endpoint on my Web API like this.
await fetch("https://localhost:1234/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ Username: "Admin", Password: "Password" }),
})
.then((response) => response.json())
.then((jsondata) => {
console.log(jsondata);
});
}
And what happens on my Web API is this
public IActionResult Login([FromBody] UserLogin userLogin)
{
var user = Authenticate(userLogin);
if (user != null)
{
var token = Generate(user);
Response.Cookies.Append("Authorization", token, new CookieOptions { HttpOnly = true, Secure = true });
return Ok();
}
return NotFound("User not found.");
}
So when the user logs in with a valid username and password, it generates a JWT and adds it to the response cookies. When I make a request with Postman/Insomnia, I can see the cookie in the response which means that on the client, when I make the same request I should get the same response. However, the issue is that I have no idea how to retrieve that cookie and store it so that I can send it in later requests when trying to access pages that require a JWT to authorize.
When I check the browsers and I got to Application > Cookies there is nothing there.
The goal is to be able to receive it, store is as a httpOnly cookie (because that's that's slightly safer than storing it on session/localStorage I've read) and then be able to use it to access other pages which requires me to send it to authorize.
That seems to be an issue because you can read or write httpOnly cookies using JavaScript https://stackoverflow.com/a/14691716/5693405
What's the proper way of dealing with this?
You can create a WEB API method Login to return token:
[Route("api/auth")]
[ApiController]
public class AuthController : ControllerBase
{
// GET api/values
[HttpPost, Route("login")]
public IActionResult Login([FromBody]LoginModel user)
{
// ... other code is omitted for the brevity
return Ok(new { Token = tokenString });
}
}
and then you can just store token at local storage of your browser:
login(form: NgForm) {
// ... other code is omitted for the brevity
localStorage.setItem("jwt", token);
// ... other code is omitted for the brevity
}
I have implemented the authentication with OWIN and bearer token and it works fine when the user login.
When \Token URL is called and username/password is passed to it, that gives token in response. But I would like to store this token in Database so instead of making another call to the server can I get the token in code? I am not able to get the value of the generated token in the ticket or any other object.
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
return Task.Factory.StartNew(() =>
{
var username = context.UserName;
var password = context.Password;
var userService = new UserService();
User user = userService.GetUserByCredentials(username, password);
if (user != null)
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, user.userName),
new Claim("UserID", user.userName)
};
ClaimsIdentity oAutIdentity = new ClaimsIdentity(claims, Startup.OAuthOptions.AuthenticationType);
var ticket = new AuthenticationTicket(oAutIdentity, new AuthenticationProperties() { });
context.Validated(ticket);
}
else
{
context.SetError("invalid_grant", "Error");
}
});
}
I am debugging the code but surprisingly the access_token seems to be visible nowhere only getting it in postman results.
The token is not valid forever. A new token is given for every authentication and is valid for a set amount of time. There is no use in saving this token to the database.
Sure you can.
You just need to override the method TokenEndpointResponseinside your authServerProvider : OAuthAuthorizationServerProvider.
Inside OAuthTokenEndpointResponseContext, there is a field called accessToken that you can retrieve the token value.
public override Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
// Summary:
// Called before the TokenEndpoint redirects its response to the caller.
return Task.FromResult<object>(null);
}
I have Asp.Net core backend running in Azure.
HTML/JS frontend running on localhost, using CORS to communicate with backend
When both, frontend and backend are in localhost, or they are both in Azure, the authentication works -> Azure AD app is setup correctly.
Here is how I log in:
[Route("/api/[controller]")]
public class AccountController : Controller
{
[HttpGet]
public IActionResult Index()
{
return Json(new AccountInfoViewModel
{
IsAuthenticated = User.Identity.IsAuthenticated,
UserName = User.Identity.Name,
Roles = new string[0],
LoginUrl = Url.Action(nameof(Login), null, null, null, Request.Host.Value),
LogoutUrl = Url.Action(nameof(Login), null, null, null, Request.Host.Value),
});
}
[HttpGet("login")]
public async Task Login(string returnUrl)
{
await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = returnUrl });
}
[HttpGet("logoff")]
public async Task LogOff()
{
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
[HttpGet("endsession")]
public async Task EndSession()
{
// If AAD sends a single sign-out message to the app, end the user's session, but don't redirect to AAD for sign out.
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
so from localhost, I then redirect to:
https://myapp.azurewebsites.net/Account/Login?returnUrl=localhost:12345
which triggers Login action and it redirects me to my Azure AD SSO page and after login, it redirects me back to localhost. However, the request to the backend is still not authenticated.
Important:
When I remove redirectUrl from login action, I'm redirected to the backend root instead of original origin (localhost). Any request from that origin (backend) is authenticated.
I had to explicitelly tell javascript to include authentication headers:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
for more details: https://learn.microsoft.com/en-us/aspnet/core/security/cors#credentials-in-cross-origin-requests
EDIT: in case of chrome and opera, which implements SameSite attribute of cookies, you also have to setup authentication cookie like this:
services.AddAuthentication(...)
.AddCookie(option => option.Cookie.SameSite = SameSiteMode.None)
.AddOpenIdConnect(...)
I've logged into an external provider (Google) which returned me an access token.
My external call back calls this code to get the user claims:
public async Task<IActionResult> ExternalLoginCallback(string returnUrl)
{
// read external identity from the temporary cookie
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme);
var tempUser = info?.Principal;
if (tempUser == null)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var claims = tempUser.Claims.ToList();
//Do more work
}
Now When I logout of Identity Server 4 and log back in, It does not make another request for consent. Yet all of my claims are still there, where does identity server store these or are they being retrieved from the cookie some how?