I inherited a project that is WEB API in .NET 4.6 framework. It implements custom authentication through the use of System.Web.Http.AuthorizeAttribute such as:
public class AuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
//Call authentication logic against DB.
}
}
I myself want to improve this to use the new JWT Token, so I went and wrote a .NET Core project that generates and returns a JWT token upon successful authentication. I tested this piece using Postman to POST to the controller and it works.
Now, in my current code I want to invoke that WEB-API call inside the OnAuthorization() such as follow:
public class AuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var auth = actionContext.Request.Headers.Authorization;
string[] userInfo = Encoding.Default.GetString(Convert.FromBase64String(auth.Parameter)).Split(':');
Profile user = new Profile();
user.UserName = userInfo[0];
user.Password = userInfo[1];
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("http://localhost:31786");
var response = client.PostAsJsonAsync("api/token/request", user);
//I hope to get the JWT Token back here or by this time the server should return it or set it in a cookie.
return "OK";
}
}
But I can't get this to work, the response.Status is returning "WaitingForActivation".
I know why I'm getting this because I should change this call to have await and update the function's signature to Task.
await client.PostAsJsonAsync("api/token", content);
However, I'm not able to do this because of the restriction from System.Web.Http.AuthorizeAttribute. What can I do here, is there a way to still invoke an async call from within here or do I have to move my logic to somewhere else?
That's not a problem if you use the correct overload:
public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken token)
{
var auth = actionContext.Request.Headers.Authorization;
string[] userInfo = Encoding.Default.GetString(Convert.FromBase64String(auth.Parameter)).Split(':');
Profile user = new Profile();
user.UserName = userInfo[0];
user.Password = userInfo[1];
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("http://localhost:31786");
var response = await client.PostAsJsonAsync("api/token/request", user);
//I hope to get the JWT Token back here or by this time the server should return it or set it in a cookie.
return "OK";
}
By the way, you should take out that new HttpClient() from there and reuse it instead.
Related
I'm trying to learn how to use authentication and SignInAsync via an API. For some reason, my code works only if I call the API from Postman or from Swagger, but not when I call it from code in the front-end.
To test this, I created two small projects: a web razor pages front end and a simple API. The API has the 3 possible POST requests (TestLogin, TestLogout and TestVerify) as show below:
[Route("api/[controller]")]
[ApiController]
[Authorize()]
public class LoginTestController : ControllerBase
{
[AllowAnonymous]
[HttpPost("TestLogin")]
public async Task<ActionResult> TestLoginAction([FromBody] LoginData loginData)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, loginData.UserName),
new Claim(ClaimTypes.Role, loginData.UserRole)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
if (principal is not null && principal.Identity is not null)
{
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return Ok("Successfully logged in.");
}
else
{
return BadRequest("Login failed.");
}
}
[HttpPost("TestVerify")]
public ActionResult TestVerify()
{
var user = User.Identity;
if (user is not null && user.IsAuthenticated)
return Ok($"Logged in user: " + user.Name);
else
return Ok("No user logged in. Please login.");
}
[HttpPost("TestLogout")]
public async Task<ActionResult> TestLogout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
}
In my Program.cs, I add the cookie authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
and
app.UseAuthentication();
app.UseAuthorization();
In my front-end razor page, I called the APIs this way using this code:
public string Message { get; set; }
public async Task OnPostLogin()
{
var client = new HttpClient();
var userLoginInfo = new LoginData
{
UserName = "TestUser",
UserRole = "TestRole"
};
string api = "https://localhost:7049/api/LoginTest/TestLogin";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
Message = await response.Content.ReadAsStringAsync();
}
public async Task OnPostLogout()
{
var client = new HttpClient();
var userLoginInfo = new LoginData();
string api = "https://localhost:7049/api/LoginTest/TestLogout";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
Message = await response.Content.ReadAsStringAsync();
}
public async Task OnPostValidate()
{
var client = new HttpClient();
var userLoginInfo = new LoginData();
string api = "https://localhost:7049/api/LoginTest/TestVerify";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
int statusCode = ((int)response.StatusCode);
if (response.IsSuccessStatusCode)
Message = await response.Content.ReadAsStringAsync();
else
Message = $"Failed with status code: ({response.StatusCode}/{statusCode})";
}
If I try via the front-end, I click on the Login, and it says it is successfully, but when I click on the Validate, it returns a 404 presumably because it doesn't think it is authorized even though I did a log in (or if I authorize anonymous for the validate, it says no one is logged in). If I do the same calls via Swagger, the login works the same, but the validate remembers the login, which is the desired behavior. Why is the login not being remembered when I call the API via the front-end, but it is when I do it via Swagger? And how can I fix this?
Thanks.
I am using Refit (5.1.67) as my HttpClient wrapper, in a .NET Core 3.1 app using IHttpClientFactory.
The API I am calling is secured using a client credentials token.
I am registering the client with this:
services.AddRefitClient<ISomeApiClient>().ConfigureHttpClient(c =>
c.BaseAddress = new Uri(Configuration["BaseUrlFromConfig"]));
The client has methods that look like this:
public interface ISomeApiClient
{
[Get("/api/somewhere")]
Task<IEnumerable<MyResponseObject>> GetItems([Header("X-User-Id")] string userId, [Header("Authorization")] string accessToken);
[Get("/api/somewhere-else")]
Task<MyResponseObject> GetItem([Header("X-User-Id")] string userId, [Header("Authorization")] string accessToken, int id);
}
What I want to avoid is having to explicitly pass accessToken and userId every time I call an endpoint (like above). Ideally, I want to have my client look like this:
public interface ISomeApiClient
{
[Get("/api/somewhere")]
Task<IEnumerable<MyResponseObject>> GetItems();
[Get("/api/somewhere")]
Task<IEnumerable<MyResponseObject>> GetItems(int id);
}
It feels like I need some sort of request middleware for outgoing requests, where I can add these two headers. If they were static I would just decorate the whole interface but because these are runtime values that will not work.
I cannot find any help on this one in the docs, and would appreciate any pointers.
Refit docs now explain how to do this
https://github.com/reactiveui/refit#reducing-header-boilerplate-with-delegatinghandlers-authorization-headers-worked-example
Add a header handler:
class AuthHeaderHandler : DelegatingHandler
{
private readonly IAuthTokenStore authTokenStore;
public AuthHeaderHandler(IAuthTokenStore authTokenStore)
{
this.authTokenStore = authTokenStore;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await authTokenStore.GetToken();
//potentially refresh token here if it has expired etc.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
then register in Startup.cs when registering the client:
services.AddTransient<AuthHeaderHandler>();
services.AddRefitClient<ISomeThirdPartyApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthHeaderHandler>();
You can use DI to inject your client where do you need it. We use it like this:
[ApiController]
public class ValuesController : ControllerBase
{
private readonly ISomeApiClient_client;
public ValuesController(ISomeApiClient client)
{
_client = client;
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "Your Oauth token");
}
[HttpGet("/")]
public async Task<ActionResult<Reply>> Index()
{
return await _client.GetMessageAsync();
}
}
I do not know if this is actually possible, but I think it' s worth a try to find out.
There are maybe other and better patterns (if you know one let me know, I will look them up) to do this, but I'm just curious to know if this is possible.
When you have to call an API you could do it directly from within the controller using the HttpClient like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: get some access token to use in api call
var accessToken = await HttpContext.GetTokenAsync("access_token");
//Example: do an API call direcly using a static HttpClient wrapt in a service
var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/some/endpoint");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _client.Client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var rederectUrl = "/account/login?returnUrl="+Request.Path;
return Redirect(rederectUrl);
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
return null;
}
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
When you would do this you'll have to reuse some code over and over again. You could just make a Repository but for some scenarios that would be overkill and you just want to make some quick and dirty API calls.
Now what I want to know is, when we move the logic of setting an Authorization header or handling the 401 and 403 responses outside the controller, how do you redirect or control the controller's action.
Lets say I create a Middleware for the HttpClient like this:
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var context = _contextAccessor.HttpContext;
var rederectUrl = "/account/login?returnUrl="+context.Request.Path;
context.Response.Redirect(rederectUrl); //not working
}
//When 403 user probably does not have authorization to use endpoint
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
}
return response;
}
}
We can just do the request like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: do an API call direcly using a static HttpClient initiated with Middleware wrapt in a service
var response = await _client.Client.GetAsync("https://example.com/api/some/endpoint");
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
The problem here is that context.Response.Redirect(rederectUrl); does not work. It does not break off the flow to redirect. Is it possible to implement this, and how would you solve this?
Ok since nobody answers my question I've thought about it thoroughly and I came up with the following:
Setup
We have a resource gateway (RG). The RG can return a 401 or 403 meaning that the session is expired (401) or the user does not have sufficient rights (403). We use an access token (AT) to authenticate and authorize our requests to the RG.
authentication
When we get a 401 and we have a refresh token (RT) we want to trigger something that will retrieve a new AT. When there is no RT or the RT is expired we want to reauthenticate the user.
authorization
When we get a 403 we want to show the user that he has no access or something similar like that.
Solution
To handle the above, without making it a hassle for the programmer that uses the API or API wrapper class we can use a Middleware that will specifically handle the Exception thrown by using the API or the API wrapper. The middleware can handle any of the above situations.
Create custom Exceptions
public class ApiAuthenticationException : Exception
{
public ApiAuthenticationException()
{
}
public ApiAuthenticationException(string message) : base(message)
{
}
}
public class ApiAuthorizationException : Exception
{
public ApiAuthorizationException()
{
}
public ApiAuthorizationException(string message) : base(message)
{
}
}
Throw exceptions
Create a wrapper or use the HttpClient middleware to manage the exception throwing.
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new ApiAuthenticationException();
}
//When 403 user probably does not have authorization to use endpoint -> show error page
if (response.StatusCode == HttpStatusCode.Forbidden)
{
throw new ApiAuthorizationException();
}
return response;
}
}
Now you have to setup the HttpClient inside your Startup.cs. There are multiple ways to do this. I advise to use AddTransient to innitiate a wrapper class that uses a HttpClient as a static.
You could do it like this:
public class ResourceGatewayClient : IApiClient
{
private static HttpClient _client;
public HttpClient Client => _client;
public ResourceGatewayClient(IHttpContextAccessor contextAccessor)
{
if (_client == null)
{
_client = new HttpClient(new ResourceGatewayMessageHandler(contextAccessor));
//configurate default base address
_client.BaseAddress = "https://gateway.domain.com/api";
}
}
}
And in your Startup.cs inside the ConfigureServices(IServiceCollection services) you can do:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<ResourceGatewayClient>();
Now you can use the dependency injection in any controller you would like.
Handle the Exceptions
Create something like this middleware (with thanks to this answer):
public class ApiErrorMiddleWare
{
private readonly RequestDelegate next;
public ApiErrorMiddleWare(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
if (exception is ApiAuthenticationException)
{
context.Response.Redirect("/account/login");
}
if (exception is ApiAuthorizationException)
{
//handle not authorized
}
}
Register your middleware
Go to Startup.cs and go to the Configure(IApplicationBuilder app, IHostingEnvironment env) method and add app.UseMiddleware<ApiErrorMiddleWare>();.
This should do it. Currently, I'm creating an example when it is publicly available (after peer review) I'll add a github reference.
I would like to hear some feedback on this solution or an alternative approach.
I'm writing API which will be consumed by mobile devices and I want to secure this API end points.
User authentication details is provide by another application called User Manger API (another project which contains user details).
How to make use of ASP.NET Identity framework Authorization and other features to secure my API endpoints while getting the user data from the User manager API ?
The question is a bit broad; basically you are looking for a strategy to authenticate and authorise a client for a web api (dotnet core or normal framework?) using a different existing API (is that API in your control, can you modify it if needed?)
If you can modify both, id say look through StackOverflow and Google for JWT tokens, OAuth and identity server.
1- you can implement an attribute and decorate your api controller.
2- you can implement and register a global filter inside your asp.net's app_start (and make sure you are registering filters in your global.asax).
3- you can do what #Roel-Abspoel mentions implement Identity Server in your User Manager API and have your client talk to it and get the token, then your API talk to it to validate the token.
There are other ways, but i will keep this short and sweet.
Here is an example using an attribute:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Filters;
namespace myExample
{
public class ExternalAuthenticationAttribute : IAuthenticationFilter
{
public virtual bool AllowMultiple
{
get { return false; }
}
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// get request + authorization headers
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// check for username and password (regardless if it was validated on the client, server should check)
// this will only accept Basic Authorization
if (String.IsNullOrEmpty(authorization.Parameter) || authorization.Scheme != "Basic")
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return null;
}
var userNameAndPasword = GetCredentials(authorization.Parameter);
if (userNameAndPasword == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Could not get credentials", request);
return null;
}
// now that we have the username + password call User manager API
var client = new HttpClient();
// POST USERNAME + PASSWORD INSIDE BODY, not header, not query string. ALSO USE HTTPS to make sure it is sent encrypted
var response = AuthenticateAgainstUserMapagerApi1(userNameAndPasword, client);
// THIS WILL WORK IN .NET CORE 1.1. ALSO USE HTTPS to make sure it is sent encrypted
//var response = AuthenticateAgainstUserMapagerApi2(client, userNameAndPasword);
// parse response
if (!response.IsSuccessStatusCode)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
else
{
var readTask = response.Content.ReadAsStringAsync();
var content = readTask.Result;
context.Principal = GetPrincipal(content); // if User manager API returns a user principal as JSON we would
}
return null;
}
//private static HttpResponseMessage AuthenticateAgainstUserMapagerApi2(HttpClient client, Tuple<string, string> userNameAndPasword)
//{
// client.SetBasicAuthentication(userNameAndPasword.Item1, userNameAndPasword.Item2);
// var responseTask = client.GetAsync("https://your_user_manager_api_URL/api/authenticate");
// return responseTask.Result;
//}
private static HttpResponseMessage AuthenticateAgainstUserMapagerApi1(Tuple<string, string> userNameAndPasword, HttpClient client)
{
var credentials = new
{
Username = userNameAndPasword.Item1,
Password = userNameAndPasword.Item2
};
var responseTask = client.PostAsJsonAsync("https://your_user_manager_api_URL/api/authenticate", credentials);
var response = responseTask.Result;
return response;
}
public IPrincipal GetPrincipal(string principalStr)
{
// deserialize principalStr and return a proper Principal instead of ClaimsPrincipal below
return new ClaimsPrincipal();
}
private static Tuple<string, string> GetCredentials(string authorizationParameter)
{
byte[] credentialBytes;
try
{
credentialBytes = Convert.FromBase64String(authorizationParameter);
}
catch (FormatException)
{
return null;
}
try
{
// make sure you use the proper encoding which match client
var encoding = Encoding.ASCII;
string decodedCredentials;
decodedCredentials = encoding.GetString(credentialBytes);
int colonIndex = decodedCredentials.IndexOf(':');
string userName = decodedCredentials.Substring(0, colonIndex);
string password = decodedCredentials.Substring(colonIndex + 1);
return new Tuple<string, string>(userName, password);
}
catch (Exception ex)
{
return null;
}
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
public class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
ReasonPhrase = reasonPhrase;
Request = request;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
}
use the attribute on your API class like this, which will call User Manager API each time PurchaseController is accessed:
[ExternalAuthenticationAttribute]
public class PurchaseController : ApiController
I just created an ASP .NET MVC 5 Web API project and added the Entity Framework model and other things to get it working with ASP. NET Identity.
Now I need to create a simple authenticated request to the standard method of that API out there from the WPF Client app.
ASP .NET MVC 5 Web API code
[Authorize]
[RoutePrefix("api/Account")]
public class AccountController : ApiController
// GET api/Account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("UserInfo")]
public UserInfoViewModel GetUserInfo()
{
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
UserName = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null
};
}
WPF Client code
public partial class MainWindow : Window
{
HttpClient client = new HttpClient();
public MainWindow()
{
InitializeComponent();
client.BaseAddress = new Uri("http://localhost:22678/");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json")); // It tells the server to send data in JSON format.
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Test();
}
private async void Test( )
{
try
{
var response = await client.GetAsync("api/Account/UserInfo");
response.EnsureSuccessStatusCode(); // Throw on error code.
var data = await response.Content.ReadAsAsync<UserInfoViewModel>();
}
catch (Newtonsoft.Json.JsonException jEx)
{
// This exception indicates a problem deserializing the request body.
MessageBox.Show(jEx.Message);
}
catch (HttpRequestException ex)
{
MessageBox.Show(ex.Message);
}
finally
{
}
}
}
It seems like it is connecting to the host and I am getting the correct error. That is ok.
Response status code does not indicate success: 401 (Unauthorized).
The main problem that I am not sure how to send username and password using WPF Client...
(Guys, I am not asking whether I have to encrypt it and use Auth Filter over API method implementations. I will do this for sure later...)
I heard that I have to send username and password in the header request... but I don't know how it can be done by using HttpClient client = new HttpClient();
Thanks for any clue!
P.S. Have I replace HttpClient with WebClient and use Task (Unable to authenticate to ASP.NET Web Api service with HttpClient)?
You can send over the current logged on user like so:
var handler = new HttpClientHandler();
handler.UseDefaultCredentials = true;
_httpClient = new HttpClient(handler);
then you can create your own authorization filter
public class MyAPIAuthorizationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
//perform check here, perhaps against AD group, or check a roles based db?
if(success)
{
base.OnActionExecuting(actionContext);
}
else
{
var msg = string.Format("User {0} attempted to use {1} but is not a member of the AD group.", id, actionContext.Request.Method);
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent(msg),
ReasonPhrase = msg
});
}
}
}
then use [MyAPIAuthorizationFilter] on each action in your controller that you want to secure.