WebAPI [Authorize] returning error when logged in

2019-07-24 15:46发布

I'm trying to set up a WebAPI with MVC5 / Web Api 2, it's currently using default configurations and other defaults right now. When I log in with an account in the browser, I can go to regular MVC controller actions with the [Authorize] attribute just fine, such as the Home page shows as it should when authorized, but then if I go to /api/Me (a default built-in api controller action) or any custom api controller action I've built with the default MVC Web Api 2 scaffolding that requires authorization, I get an error like so:

{"message":"Authorization has been denied for this request."}

This is when I'm trying it in Microsoft Edge, I haven't tried it on my actual client code I'm building yet, which is a UWP app. I figured I would test in the browser first to ensure things are working properly.

I started looking at this article: https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/individual-accounts-in-web-api It seems to be more geared towards Ajax requests and SPAs though. My goal is having things working through both the web and through UWP. I will probably focus more time on the UWP side than developing a robust web app with ajax, since the app will run in an intranet and access an intranet IIS server running the web api, I'd like to build a desktop client in UWP and eventually Xamarin that access data through Web Api.

Is it safe for me to assume that if you're using a regular web browser like Edge you cannot access Web Api controller actions when they are secured through the [Authorize] attribute, since it doesn't send an access token in the header?

2条回答
孤傲高冷的网名
2楼-- · 2019-07-24 15:52

So from your UWP app you could create a couple of methods such as below, or may be wrap them up in a class that you can inject through DI (your choice).

public async Task<TResult> GetAsync<TResult>(string uriString) where TResult : class
    {
        var uri = new Uri(uriString);
        using (var client = GetHttpClient())
        {
            HttpResponseMessage response = await client.GetAsync(uri);
            if (response.StatusCode != HttpStatusCode.OK)
            {
                //Log.Error(response.ReasonPhrase);
                return default(TResult);
            }
            var json = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<TResult>(json, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
        }
    }

    public async Task<TResult> PostAsync<TResult, TInput>(string uriString, TInput payload = null) where TInput : class
    {
        var uri = new Uri(uriString);
        using (var client = GetHttpClient())
        {
            var jsonContent = JsonConvert.SerializeObject(payload, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
            HttpResponseMessage response = await client.PostAsync(uri, new StringContent(jsonContent, Encoding.UTF8, "application/json"));
            if (response.StatusCode != HttpStatusCode.OK)
            {
                //Log.Error(response.ReasonPhrase);
                return default(TResult);
            }
            var json = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<TResult>(json);
        }
    }

And if you are using basic (username and password authentication) then your GetHttpClient method will be something like:

private HttpClient GetHttpClient()
    {
        var client = new HttpClient();
        var username = // get username;
        var password = // get password;
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")));
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        return client;
    }

or if you are using bearer token then you could do:

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer ", "string token goes here...");
查看更多
不美不萌又怎样
3楼-- · 2019-07-24 16:01

I'm posting a little more supplemental information here for anyone who might be struggling with this issue where you want to access MVC5 / Web Api 2 from a client like UWP or Xamarin, but you need to lock down areas of your Web Api with the Authorize attribute.

The high level process is, instead of going through any MVC controllers you're going to do a POST directly to the /Token endpoint (or whatever endpoint specified).

First thing's first, if you want to do SSL for development but don't have a cert yet, go ahead and run the following:

C:\Program Files (x86)\IIS Express>iisexpressadmincmd setupSslUrl -url:https://localhost:55970/ -UseSelfSigned

Where 55970 is the port of whatever your local website port is, I'll be referring to this in my text.

Next thing to do is be sure and override ApplicationOAuthProvider.cs on the client validation and resource granting methods, otherwise you'll run into invalid client or invalid grant errors.

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

        UserManager<ApplicationUser> _userManager;
        ApplicationDbContext db = new ApplicationDbContext();
        _userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(db));
        try
        {

            ApplicationUser user = await _userManager.FindAsync(context.UserName, context.Password);


            if (user == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }

            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            identity.AddClaim(new Claim("sub", context.UserName));
            identity.AddClaim(new Claim("role", "user"));

            context.Validated(identity);
        }
        catch (Exception ex)
        {
            string str = ex.ToString();
        }

        db.Dispose();
    }

    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        string clientId = "";
        string clientSecret = "";
        context.TryGetFormCredentials(out clientId, out clientSecret);

        List<string> validClients = new List<string>(){ "web","Alliance_UWP","Alliance_Xamarin","Alliance_Web" };
        if (validClients.Contains(clientId))
            context.Validated();
    }

You don't need to be as verbose as I was on the allowed clients, just "web" is fine, you'll include that in your Http POST method as a url-encoded form value, along with grant_type = "password" and your username/password. Here's a quick and dirty UWP client I wrote which just takes in a username/password and accesses a dataset I have via the api that's tied down with an Authorize attribute. If you don't authenticate you'll get an authorization error, which is exactly what we want.

Just a note here that in my client I filter out self-signed certificate errors because I've set up a self-signed cert in IIS Express via the above command.

    Uri tokenUri = new Uri(@"https://localhost:55970/Token");
    // This is a test data set from my Web Api pulling data from 
    // Entity Framework and SQL Server protected by the Authorize attribute
    Uri testCasesUri = new Uri(@"https://localhost:55970/api/Cases");
    string accessToken = "";

    public MainPage()
    {
        this.InitializeComponent();
    }

    private async void btn_SubmitLogin_Click(object sender, RoutedEventArgs e)
    {
        string username = txt_User.Text;
        string password = txt_Password.Password;

        HttpBaseProtocolFilter filter = new HttpBaseProtocolFilter();
        filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Untrusted);
        filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.InvalidName);
        filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.RevocationFailure);
        HttpClient client = new HttpClient(filter);
        Dictionary<string, string> parameters = new Dictionary<string, string>();
        parameters.Add("client_id", "web");
        parameters.Add("grant_type", "password");
        parameters.Add("username", username);
        parameters.Add("password", password);
        try
        {
            HttpResponseMessage result = await client.PostAsync(tokenUri, new HttpFormUrlEncodedContent(parameters));
            string jsonResult = await result.Content.ReadAsStringAsync();
            // TokenResult is a custom model class for deserialization of the Token Endpoint
            // Be sure to include Newtonsoft.Json from NuGet
            var resultObject = JsonConvert.DeserializeObject<TokenResult>(jsonResult);
            accessToken = resultObject.AccessToken;

            // When setting the request for data from Web Api set the Authorization
            // header to Bearer and the token you retrieved
            client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
            result = await client.GetAsync(testCasesUri);
            jsonResult = await result.Content.ReadAsStringAsync();

        } catch(Exception ex)
        {
            string debugBreak = ex.ToString();
        }

Hope that helps any of you guys having trouble getting Xamarin or UWP working with Web Api.

查看更多
登录 后发表回答