dgn
dgn

Reputation: 1223

Authorization_code grant flow on Owin.Security.OAuth: returns invalid_grant

I am trying to setup my authentication using the authorization_code grant flow. I had it previously working with grant_type=password, so I kind of know how the stuff is supposed to work. But when using grant_type=authorization_code, I couldn't make it return anything other than invalid_grant

Here is my setup:

app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
    AllowInsecureHttp = true,
    TokenEndpointPath = new PathString("/auth/token"),
    AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),
    Provider = new SampleAuthProvider()
});

app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
    AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
    AuthenticationType = "Bearer"
});

SampleAuthProvider is the following class: https://gist.github.com/anonymous/8a0079b705423b406c00

Basically, it's just logging every step and validating it. I tried the request:

POST http://localhost:12345/auth/token
grant_type=authorization_code&code=xxxxxx&client_id=xxxxx&redirect_uri=https://xxxx.com/
Content-Type: application/x-www-form-urlencoded

It's going through:

And that's all. I expected it to call OnValidateTokenRequest and OnGrantAuthorizationCodenext, but it just didn't. I have no idea why.

The xxxx's in the request aren't placeholders, I tried it like that. Maybe the middleware makes some checks on its own and rejects the request because of that? I tried variants of the redirect_uri with http, without any protocol, without trailing slash...

It also works properly with a custom grant_type. It so if I too desperate, I guess I can use that to simulate authorization_code, but I'd rather not have to do that.

TL;DR

My OAuthAuthorizationServerProvider returns {"error":"invalid_grant"}after OnValidateClientAuthentication when using grant_type=authorization_code.

Thanks for your help!


Edit

As pointed out by RajeshKannan, I made a mistake in my configuration. I didn't provide an AuthorizationCodeProvider instance. However, that didn't completely solve the problem, since in my case, the code is not issued by the AuthorizationCodeProvider, and I can't just deserialize it. I anwered with the workaround I got working.

Upvotes: 9

Views: 15292

Answers (6)

Ash
Ash

Reputation: 2294

The answer by @dgn more or less worked for me. This is just an extension to that. As it turns out, you can supply whatever string you want to the ClaimsIdentity constructor. The following works just as well, and doubles up as a detailed code comment:

var identity = new ClaimsIdentity(
    @"Katana - What a shitty framework/implementation.
    Unintuitive models and pipeline, pretty much have to do everything, and the docs explain nothing. 
    Like what can go in here? WTF knows but turns out as long as _something_ is in here, 
    there is a client_id key in your AuthenticationProperties with the same value as 
    what's set inside your implementation for OAuthAuthorizationServerProvider.ValidateClientAuthentication, and
    your AuthenticationProperties.ExpiresUtc is set to some time in the future, it works.
    Oh and you don't actually need to supply an implementation for OAuthAuthorizationServerProvider.GrantAuthorizationCode...
    but if you are using the resource owner grant type, you _do_ need to supply an implementation of 
    OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials. Hmm. Whatever.
    Katana and IdenetityServer - two frameworks that are absolute garbage. In the amount of time it took me to
    figure out all the observations in this paragraph, I could've written my own /token endpoint."
);

Upvotes: 1

Ken Kin
Ken Kin

Reputation: 4693

I solved this with the the following simplest example and would like to share it. Hope someone find it helpful.

--

It seems the middleware will check if the key redirect_uri exists in the dictionary of AuthenticationProperties, remove it and everything works fine(with validated context).

A simplified example of AuthorizationCodeProvider woubld be like so:

public class AuthorizationCodeProvider:AuthenticationTokenProvider {
    public override void Create(AuthenticationTokenCreateContext context) {
        context.SetToken(context.SerializeTicket());
    }

    public override void Receive(AuthenticationTokenReceiveContext context) {
        context.DeserializeTicket(context.Token);

        context.Ticket.Properties.Dictionary.Remove("redirect_uri"); // <-
    }
}

And don't forget to make the context validated in the overridden method OAuthAuthorizationServerProvider.ValidateClientAuthentication. Again, here's a simplified example which inherit from the ApplicationOAuthProvider class of the template project:

public partial class DefaultOAuthProvider:ApplicationOAuthProvider {
    public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context) {
        if(null!=context.RedirectUri) {
            context.Validated(context.RedirectUri);
            return Task.CompletedTask;
        }

        return base.ValidateClientRedirectUri(context);
    }

    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) {
        if(context.TryGetFormCredentials(out String clientId, out String clientSecret)) {
            // Specify the actual expected client id and secret in your case
            if(("expected-clientId"==clientId)&&("expected-clientSecret"==clientSecret)) {

                context.Validated(); // <-

                return Task.CompletedTask;
            }
        }

        return base.ValidateClientAuthentication(context);
    }

    public DefaultOAuthProvider(String publicClientId) : base(publicClientId) {
    }
}

Note that if you invoke context.Validated with a particular client id, then you will have to put the same client_id in the properties of the ticket, you can do that with the method AuthenticationTokenProvider.Receive

Upvotes: 0

Gebb
Gebb

Reputation: 6546

I had the same error. Things I was missing:

  • Specify OAuthAuthorizationServerOptions.AuthorizationCodeProvider according to the documentation.
  • Specify the same client_id as a GET-parameter when making a request to the token endpoint as you did when you received the authorization_code.
  • Override OAuthAuthorizationServerProvider.ValidateClientAuthentication and in this method call context.TryGetFormCredentials. This sets the property context.ClientId to the value from the client_id GET-parameter. This property must be set, otherwise you'll get the invalid_grant error. Also, call context.Validated().

After doing all of the above, I could finally exchange the authorization_code to an access_token at the token endpoint.

Upvotes: 5

mkaj
mkaj

Reputation: 3489

Thanks scenario, My code was missing the following two required values. Posted here in case others find it useful:

            // Required. The request is rejected if it's not provided
            authProps.Dictionary.Add("client_id", clientIds[0]); 

            // Required, must be in the future
            authProps.ExpiresUtc = DateTimeOffset.Now.AddMinutes(1); 

Upvotes: 2

dgn
dgn

Reputation: 1223

Here is what I got working. I'm not completely comfortable with that solution, but it works and should help others to fix their issues.


So, the issue is that I didn't set the AuthorizationCodeProvider property. When a request with grant_type=authorization_code is received, the code must be validated by that code provider. The framework assumes that the code was issued by that code provider, but that's not my case. I get it from another server and have to send the code back to it for validation.

In the standard case, where you are also the one issuing the code, the link provided by RajeshKannan describes everything you have to do.

Here is where you have to set the property:

app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString(Paths.TokenPath),
    Provider = new SampleAuthProvider(),
    AuthorizationCodeProvider = new MyAuthorizationCodeProvider ()
}

And the declaration of the MyAuthorizationCodeProvider class:

internal class MyAuthorizationCodeProvider : AuthenticationTokenProvider
{
    public override async Task ReceiveAsync(
        AuthenticationTokenReceiveContext context)
    {
        object form;
        // Definitely doesn't feel right
        context.OwinContext.Environment.TryGetValue(
                "Microsoft.Owin.Form#collection", out form); 
        var redirectUris = (form as FormCollection).GetValues("redirect_uri");
        var clientIds = (form as FormCollection).GetValues("client_id");
        if (redirectUris != null && clientIds != null)
        {
            // Queries the external server to validate the token
            string username = await MySsoService.GetUserName(context.Token,
                                                             redirectUris[0]);
            if (!string.IsNullOrEmpty(username))
            {
                var identity = new ClaimsIdentity(new List<Claim>()
                {
                    // I need the username in  GrantAuthorizationCode
                    new Claim(ClaimTypes.NameIdentifier, username) 
                }, DefaultAuthenticationTypes.ExternalBearer);

                var authProps = new AuthenticationProperties();

                // Required. The request is rejected if it's not provided
                authProps.Dictionary.Add("client_id", clientIds[0]); 

                // Required, must be in the future
                authProps.ExpiresUtc = DateTimeOffset.Now.AddMinutes(1); 

                var ticket = new AuthenticationTicket(identity, authProps);
                context.SetTicket(ticket);
            }
        }
    }
}

Upvotes: 13

RajeshKannan
RajeshKannan

Reputation: 904

Make sure that you have configured your authorization server options. I think you should provide your authorize end point details:

 AuthorizeEndpointPath = new PathString(Paths.AuthorizePath)

In the below link, the authorization code grant will be explained in detail and it lists the method which were involved in authorization code grant life cycle.

Owin Oauth authorization server

Upvotes: 1

Related Questions