Adding additional logic to Bearer authorization

35,518

Solution 1

It seems there's something missing in your code.
You're not validating your client.

You should implement ValidateClientAuthentication and check your client's credentials there.

This is what I do:

public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
        string clientId = string.Empty;
        string clientSecret = string.Empty;

        if (!context.TryGetBasicCredentials(out clientId, out clientSecret)) 
        {
            context.SetError("invalid_client", "Client credentials could not be retrieved through the Authorization header.");
            context.Rejected();
            return;
        }

        ApplicationDatabaseContext dbContext = context.OwinContext.Get<ApplicationDatabaseContext>();
        ApplicationUserManager userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();

        if (dbContext == null)
        {
            context.SetError("server_error");
            context.Rejected();
            return;
        }

        try
        {
            AppClient client = await dbContext
                .Clients
                .FirstOrDefaultAsync(clientEntity => clientEntity.Id == clientId);

            if (client != null && userManager.PasswordHasher.VerifyHashedPassword(client.ClientSecretHash, clientSecret) == PasswordVerificationResult.Success)
            {
                // Client has been verified.
                context.OwinContext.Set<AppClient>("oauth:client", client);
                context.Validated(clientId);
            }
            else
            {
                // Client could not be validated.
                context.SetError("invalid_client", "Client credentials are invalid.");
                context.Rejected();
            }
        }
        catch (Exception ex)
        {
            string errorMessage = ex.Message;
            context.SetError("server_error");
            context.Rejected();
        }
  }

A good article full of details can be found here.
A even better explanation can be found in this blog series.

UPDATE:

I did some digging and webstuff is right.

In order to pass errorDescription to the client we need to Rejected before we set the error with SetError:

context.Rejected();
context.SetError("invalid_client", "The information provided are not valid !");
return;

or we can extend it passing a serialized json object in the description:

context.Rejected();
context.SetError("invalid_client", Newtonsoft.Json.JsonConvert.SerializeObject(new { result = false, message = "The information provided are not valid !" }));
return;

enter image description here

With a javascript/jQuery client we could deserialize the text response and read the extended message:

$.ajax({
    type: 'POST',
    url: '<myAuthorizationServer>',
    data: { username: 'John', password: 'Smith', grant_type: 'password' },
    dataType: "json",
    contentType: 'application/x-www-form-urlencoded; charset=utf-8',
    xhrFields: {
        withCredentials: true
    },
    headers: {
        'Authorization': 'Basic ' + authorizationBasic
    },  
    error: function (req, status, error) {
            if (req.responseJSON && req.responseJSON.error_description)
            {
               var error = $.parseJSON(req.responseJSON.error_description);
                    alert(error.message);
            }
    }
});

Solution 2

On a side note, if you want to set a custom error message you'll have to swap the order of the context.Rejected and context.SetError.

    // Summary:
    //     Marks this context as not validated by the application. IsValidated and HasError
    //     become false as a result of calling.
    public virtual void Rejected();

If you place context.Rejected after context.SetError then the property context.HasError will be reset to false therefore the correct way to use it is:

    // Client could not be validated.
    context.Rejected();
    context.SetError("invalid_client", "Client credentials are invalid.");
Share:
35,518
Echiban
Author by

Echiban

Updated on August 17, 2020

Comments

  • Echiban
    Echiban over 3 years

    I am attempting to implement OWIN bearer token authorization, and based on this article. However, there's one additional piece of information I need in bearer token that I don't know how to implement.

    In my application, I need to deduce from the bearer token user information (say userid). This is important because I don't want an authorized user from being able to act as another user. Is this doable? Is it even the correct approach? If the userid is a guid, then this would be simple. It's an integer in this case. An authorized user can potentially impersonate another just by guessing / brute force, which is unacceptable.

    Looking at this code:

    public void ConfigureOAuth(IAppBuilder app)
    {
        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new SimpleAuthorizationServerProvider()
        };
    
        // Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
    }
    
    public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            context.Validated();
        }
    
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
    
            using (AuthRepository _repo = new AuthRepository())
            {
                IdentityUser user = await _repo.FindUser(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);
        }
    }
    

    I would think that it is possible to override the authorization / authentication to accommodate what I need?

  • Echiban
    Echiban over 9 years
    I read that article, and the only thing I don't understand is how in the world did the author came up with Authorization: Basic NDJmZjVkYWQzYzI3NGM5N2EzYTdjM2Q0NGI2N2JiNDI6Y2xpZW50MTIzNDU2‌​. I then looked at the github link the author posted and finally saw the client side. Oauth2 magic to the rescue! Thanks leftyx
  • LeftyX
    LeftyX over 9 years
    @Echiban: If you need some more info there's a new link in my answer. Taisser's blog about owin and web api is great and full of useful info. cheers.
  • user230910
    user230910 over 8 years
    if I need to pass 2 values back, can I call SetError twice?
  • LeftyX
    LeftyX over 8 years
    @user230910: I've extended my answer with some more info which might help you.
  • LeftyX
    LeftyX over 8 years
    @user230910: You're welcome. Upvotes are more than accepted :-) Cheers.
  • Cristian E.
    Cristian E. about 8 years
    Damn that actually did the trick. I am surprised how the Copy-Paste is spread across .NET community. Thank you for your unique answer.
  • InTheWorldOfCodingApplications
    InTheWorldOfCodingApplications almost 8 years
    Can we send a 401 or 403 error? The only error code i am seeing is 400
  • Diego Perez
    Diego Perez over 5 years
    No matter what I try and it's been days trying several combinations and from http.post Angular 6 request I always get a "Bad Request" string literal and cannot get the real json error_description. The weird thing is I see it under Network tab in "Response" and it also works under AngularJS but not with Angular 6. Any help?
  • Irakli
    Irakli over 4 years
    what are ResponseMiddlewareOptions?