I have an ASP.NET MVC website using angular for the front end, that then needs to communicate to a REST Web API service to retrieve data. I have authentication logic against a custom provider in my MVC project and this all works fine using ADLDS. However the calls to the REST Web API have no authentication data passed and I can't work out how to pass the user who is authenticated with MVC, over to the REST Web API.
Here is an example call to the Web API.
public void ApproveModel(int modelVersionId, string comments)
{
var request = new RestRequest("api/modelversion", Method.POST) { RequestFormat = DataFormat.Json };
request.AddBody(new[] { new ModelAction { ModelActionType = ModelActionConstants.ModelActionApproveModel, ActionParameters = new Dictionary<string, string> { { ModelActionConstants.ModelActionParameterModelVersionId, modelVersionId.ToString(CultureInfo.InvariantCulture) }, {ModelActionConstants.ModelActionParameterComments, comments} } } });
request.UseDefaultCredentials = true;
var response = _client.Execute(request);
if (response.StatusCode != HttpStatusCode.OK)
throw new ServerException(response.Content);
}
My Web API Controller method (abbreviated)
[ValidateAccess(Constants.Dummy, Constants.SECURE_ENVIRONMENT)]
public HttpResponseMessage Post(ModelAction[] actions)
{
...
}
And my custom validate access attribute which uses Thinktecture
public class ValidateAccess : ClaimsAuthorizeAttribute
{
private static readonly ILog Log = LogManager.GetLogger(typeof(ValidateAccess));
private readonly string _resource;
private readonly string _claimsAction;
public ValidateAccess(string claimsAction, string resource)
{
_claimsAction = claimsAction;
_resource = resource;
XmlConfigurator.Configure();
}
protected override bool IsAuthorized(HttpActionContext actionContext)
{
if (actionContext == null) throw new ArgumentNullException("actionContext");
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
Log.InfoFormat("User {0} is not authenticated - Not authorizing further. Redirecting to error page.",
HttpContext.Current.User.Identity.Name);
return false;
}
// specified users or roles when we use our attribute
return CheckAccess(actionContext);
}
protected override bool CheckAccess(HttpActionContext actionContext)
{
if (_claimsAction == String.Empty && _resource == string.Empty)
{
//user is in landing page
return true;
}
return ClaimsAuthorization.CheckAccess(_claimsAction, _resource);
}
}
My problems are
I am not familiar with Web Api. Is IsAuthorized(HttpActionContext actionContext) the right method to override to enforce access policy on API calls?
Why am I getting null for the User identity?
Related
I have created a .NET6 Web-API that will be consumed by an SPA. I have these projects all set up with Azure AD auth and it's working great.
In my Web-API I now want to have a controller that can be accessed with BASIC Auth. What is the best aproach here?
I have tried creating a custom auth attribute like this:
[AttributeUsage(AttributeTargets.Class)]
public class BasicAuth : Attribute, Microsoft.AspNetCore.Mvc.Filters.IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext filterContext)
{
if (filterContext != null)
{
var auth = filterContext.HttpContext.Request.Headers.Authorization.ToString();
if (!String.IsNullOrWhiteSpace(auth))
{
if (IsValidToken(auth))
{
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
return;
}
else
{
filterContext.Result = new UnauthorizedResult();
}
}
else
{
filterContext.Result = new UnauthorizedResult();
}
}
}
public bool IsValidToken(string auth)
{
var encoding = Encoding.GetEncoding("UTF-8");
string base64 = auth.Split(" ")[1];
var cred = encoding.GetString(Convert.FromBase64String(base64));
var credArray = cred.Split(':');
if (credArray[0] == "username" && credArray[1] == "password")
{
return true;
}
else
{
return false;
}
}
}
And then i put the attribute like this:
[BasicAuth]
[ApiController]
[Route("[controller]")]
public class PublicDataController : ControllerBase{...}
This is working but I have a feeling that it can be done better?
Any advice?
I have a regular application using cookie based authentication. This is how it's configured:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Login")
.AddCookie("Login", c => {
c.ClaimsIssuer = "Myself";
c.LoginPath = new PathString("/Home/Login");
c.AccessDeniedPath = new PathString("/Home/Denied");
});
}
This works for my regular actions:
[Authorize]
public IActionResult Users()
{
return View();
}
But doesn't work well for my ajax requests:
[Authorize, HttpPost("Api/UpdateUserInfo"), ValidateAntiForgeryToken, Produces("application/json")]
public IActionResult UpdateUserInfo([FromBody] Request<User> request)
{
Response<User> response = request.DoWhatYouNeed();
return Json(response);
}
The problem is that when the session expires, the MVC engine will redirect the action to the login page, and my ajax call will receive that.
I'd like it to return the status code of 401 so I can redirect the user back to the login page when it's an ajax request.
I tried writing a policy, but I can't figure how to unset or make it ignore the default redirect to login page from the authentication service.
public class AuthorizeAjax : AuthorizationHandler<AuthorizeAjax>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizeAjax requirement)
{
if (context.User.Identity.IsAuthenticated)
{
context.Succeed(requirement);
}
else
{
context.Fail();
if (context.Resource is AuthorizationFilterContext redirectContext)
{
// - This is null already, and the redirect to login will still happen after this.
redirectContext.Result = null;
}
}
return Task.CompletedTask;
}
}
How can I do this?
Edit: After a lot of googling, I found this new way of handling it in version 2.0:
services.AddAuthentication("Login")
.AddCookie("Login", c => {
c.ClaimsIssuer = "Myself";
c.LoginPath = new PathString("/Home/Login");
c.Events.OnRedirectToLogin = (context) =>
{
// - Or a better way to detect it's an ajax request
if (context.Request.Headers["Content-Type"] == "application/json")
{
context.HttpContext.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
});
And it works for now!
What you need can be achieved by extending AuthorizeAttribute class.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.Result = new JsonResult
{
Data = new { Success = false, Data = "Unauthorized" },
ContentEncoding = System.Text.Encoding.UTF8,
ContentType = "application/json",
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
You can then specify this attribute on Ajax methods.
Hope this helps.
Reference: http://benedict-chan.github.io/blog/2014/02/11/asp-dot-net-mvc-how-to-handle-unauthorized-response-in-json-for-your-api/
I have created a POST API under UmbracoApiController.
[HttpPost]
[ActionName("SaveData")]
public HttpResponseMessage SaveData([FromBody]JObject data)
{
if (!authorized)
{
return Request.CreateResponse(HttpStatusCode.Unauthorized,
"Unauthorized access. Please check your credentials");
}
}
Instead of returning 401, it is going to the login page with 302 status.
I have created a custom attribute as well -
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class BasicAuthorization : AuthorizationFilterAttribute
{
private const string _authorizedToken = "Authorization";
public override void OnAuthorization(HttpActionContext filterContext)
{
var authorizedToken = string.Empty;
try
{
var headerToken = filterContext.Request.Headers.FirstOrDefault(x => x.Key == _authorizedToken);
if (headerToken.Key != null)
{
authorizedToken = Convert.ToString(headerToken.Value.SingleOrDefault());
if (!IsAuthorize(authorizedToken))
{
var httpContext = HttpContext.Current;
var httpResponse = httpContext.Response;
filterContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent("Unauthorized access. Please check your credentials")
};
httpResponse.StatusCode = (int) HttpStatusCode.Unauthorized;
httpResponse.SuppressFormsAuthenticationRedirect = true;
return;
}
}
else
{
filterContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
return;
}
}
catch (Exception)
{
filterContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
return;
}
base.OnAuthorization(filterContext);
}
private static bool IsAuthorize(string authorizedToken)
{
return authorizedToken == ConfigurationManager.AppSettings["VideoIngestionKey"];
}
}
But this also does not work. I am using Umbraco 7.6.13
Any help greatly appreciated.
Thanks
Have something similar but used with Surface Controller not Web API controller.
Override HandleUnauthorizedRequest to implement custom response / override Umbraco & .NET defaults.
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// example redirects to a 'Forbidden' doctype/view with Reponse.StatusCode set in view;
filterContext.Result =
new RedirectToUmbracoPageResult(
UmbracoContext.Current.ContentCache.GetSingleByXPath("//forbidden"));
}
It's odd that Forms authentication seems to be kicking in and redirecting you to login page for an API request. The AuthorizationFilterAttribute should return a Http 401 by default (so could deal with via web.config customErrors or httpErrors sections instead of code).
May want to review your web.config settings?
Hello I have a web api controller inside a mvc web site.
I'm trying to allow access to the controller using 2 rules:
User is admin or the request came from local computer;
I'm new to AuthorizationFilterAttribute but I tried to write one that limit access
to local request only:
public class WebApiLocalRequestAuthorizationFilter : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException("httpContext");
}
if (actionContext.Request.IsLocal())
{
return;
}
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
actionContext.Response.Content = new StringContent("Username and password are missings or invalid");
}
}
Then I decorated my controller with 2 attributes as
[Authorize(Roles = "Admin")]
[WebApiLocalRequestAuthorizationFilter]
public class ContactController : ApiController
{
public ContactModel Get(int id)
{
ContactsService contactsService = new ContactsService();
return contactsService.GetContactById(id).Map<ContactModel>();
}
}
But as I suspected , now, in order to access the controller I need to be admin and the request should be made from localhost. How can I do it?
Kind regards,
Tal Humy
One solution is to create a class that inherits from AuthorizeAttribute
e.g. something like this
public class MyAuthorizeAttribute: AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool accessAllowed = false;
bool isInGroup = false;
List<string> roleValues = Roles.Split(',').Select(rValue => rValue.Trim().ToUpper()).ToList();
foreach (string role in roleValues)
{
isInGroup = IdentityExtensions.UserHasRole(httpContext.User.Identity, role);
if (isInGroup)
{
accessAllowed = true;
break;
}
}
//add any other validation here
//if (actionContext.Request.IsLocal()) accessAllowed = true;
if (!accessAllowed)
{
//do some logging
}
return accessAllowed;
}
...
}
Then you can use it like so:
[MyAuthorizeAttribute(Roles = "Support,Admin")]
In the above code, IdentityExtensions checks for, and caches, ActiveDirectory roles which also allows us to fake the current user having roles by changing the cache.
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.