2
votes

I'm developing an OAuth v2 Authorization Server using the OWIN middleware and I'm trying to implement the OAuth2 Authorization Code Grant Flow but I'm running into an issue where my client is receiving errors if the redirect_uri contains querystring parameters and the resource owner has to authenticate.

The client is able to request an Authentication Code but, when it tries to exchange the code for an Access Token, the Authorization Server responds with an HTTP Status of "400 Bad Request" and the following response body.

{"error":"invalid_grant"}

If the redirect_uri doesn't contain querystring parameters or if the client removes them prior to requesting the Authorization Code then it works perfectly. If the resource owner has already authenticated with the Authorization Server then it works as well.

My client is using DotNetOpenAuth and using Glimpse I can see that redirect_uri is consistent between the Authorization Code request and Access Token requests.

A failure in the Glimpse log looks like this:

Prepared outgoing EndUserAuthorizationRequestC (2.0) message for http://localhost:61814/authorize:
    client_id: localhost36618
    redirect_uri: http://localhost:36618/login?redirectURL=%2FProfile
    state: <state token>
    scope: authentication
    response_type: code

Processing incoming EndUserAuthorizationSuccessAuthCodeResponse (2.0) message:
    code: <authorization code>
    state: <state token>
    redirectURL: /Profile

Prepared outgoing AccessTokenAuthorizationCodeRequestC (2.0) message for http://localhost:61814/token:
    code: <authorization code>
    redirect_uri: http://localhost:36618/login?redirectURL=%2FProfile
    grant_type: authorization_code

http://localhost:61814/token returned 400 BadRequest: Bad Request

WebException from http://localhost:61814/token: {"error":"invalid_grant"}

However if I omit the querystring parameters from the redirect_uri it works:

Prepared outgoing EndUserAuthorizationRequestC (2.0) message for http://localhost:61814/authorize:
    client_id: localhost36618
    redirect_uri: http://localhost:36618/Login
    state: <state token>
    scope: authentication
    response_type: code

Processing incoming EndUserAuthorizationSuccessAuthCodeResponse (2.0) message:
    code: <authorization code>
    state: <state token>

Prepared outgoing AccessTokenAuthorizationCodeRequestC (2.0) message for http://localhost:61814/token:
    code: <authorization code>
    redirect_uri: http://localhost:36618/Login
    grant_type: authorization_code

Processing incoming AccessTokenSuccessResponse (2.0) message:
    access_token: <access token>
    token_type: bearer
    expires_in: 3599
    refresh_token: <refresh token>

Similarly if I log into the Authorization Server before using the Client, it works:

Prepared outgoing EndUserAuthorizationRequestC (2.0) message for http://localhost:61814/authorize:
    client_id: localhost36618
    redirect_uri: http://localhost:36618/login?redirectURL=%2FProfile
    state: <state token>
    scope: authentication
    response_type: code

Processing incoming EndUserAuthorizationSuccessAuthCodeResponse (2.0) message:
    code: <authorization code>
    state: <state token>
    redirectURL: /Profile

Prepared outgoing AccessTokenAuthorizationCodeRequestC (2.0) message for http://localhost:61814/token:
    code: <authorization code>
    redirect_uri: http://localhost:36618/login?redirectURL=%2FProfile
    grant_type: authorization_code

Processing incoming AccessTokenSuccessResponse (2.0) message:
    access_token: <access token>
    token_type: bearer
    expires_in: 3599
    refresh_token: <refresh token>    

The OWIN Authorization Server's OAuthAuthorizationServerProvider executes the following methods on a successful Access Token Authorization Code Request:

  • Provider.OnMatchEndpoint
  • Provider.OnValidateClientAuthentication
  • AuthorizationCodeProvider.Receive
  • Provider.OnValidateTokenRequest
  • Provider.OnGrantAuthorizationCode
  • Provider.OnTokenEndpoint
  • AccessTokenProvider.OnCreate
  • RefreshTokenProvider.OnCreate
  • Provider.OnTokenEndpointResponse

However an unsuccessful Access Token Authorization Code Request only touches the following methods:

  • Provider.OnMatchEndpoint
  • Provider.OnValidateClientAuthentication
  • AuthorizationCodeProvider.Receive

AuthorizationCodeProvider.OnReceive has the following implementation:

private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
 try
 {
  string ticket = _repo.RemoveTicket(context.Token);
  if (!string.IsNullOrEmpty(ticket))
  {
   context.DeserializeTicket(ticket);
  }
 }
 catch (Exception ex)
 {
  var wrapper = new Exception("Receive Authentication Code Error", ex);
  Logger.Error(wrapper);
 }
}

In the debugger I can see a valid Token, successful retrieval of the serialized Ticket from _repo, and the deserialized Ticket in the context object just before the method completes. No exception is thrown. The flow looks identical between successful and failed requests so I'm unclear what's failing and the Diagnostic Tools Events log doesn't show any exceptions during the request processing, just the thread exiting after ReceiveAuthenticationCode completes.

It looks like the issue is with my test client because I was able to reproduce the exact same issue using Brent Shaffer's OAuth2 Demo PHP live demo.

The test client is based on DotNetOpenAuth:

private static class Client
{
    public const string Id = "username";
    public const string Secret = "password";
}
private static class Paths
{
    public const string AuthorizationServerBaseAddress = "http://localhost:61814";
    public const string ResourceServerBaseAddress = "http://localhost:61814";
    public const string AuthorizePath = "/authorize";
    public const string TokenPath = "/token";
    public const string ResourceServerApiMethodPath = "/getaccount";
 }

public ActionResult Login(string code = "", string redirectURL = "/profile")
{
    var authorizationServerUri = new Uri(Paths.AuthorizationServerBaseAddress);
    var authorizationServer = new AuthorizationServerDescription
    {
        AuthorizationEndpoint = new Uri(authorizationServerUri, Paths.AuthorizePath),
        TokenEndpoint = new Uri(authorizationServerUri, Paths.TokenPath)
    };
    var client = new WebServerClient(authorizationServer, Client.Id, Client.Secret);


    if (!string.IsNullOrEmpty(code))
    {
        var apiResponse = null;
        var authorizationState = client.ProcessUserAuthorization();

        if (!string.IsNullOrEmpty(authorizationState?.AccessToken))
            apiResponse = GetApiResponse(authorizationState, Paths.ResourceServerApiMethodPath);

        if (apiResponse != null)
        {
            var identity = new ClaimsIdentity(new[] { new Claim("test", apiResponse),
                                                      new Claim(ClaimTypes.Role, "ResourceOwner")
                                                }, "ApplicationCookie");

            AuthMgr.SignIn(new AuthenticationProperties { IsPersistent = true }, identity);

            return Redirect(redirectURL);
        }
    }
    else
    {
        client.RequestUserAuthorization();
    }

    return View();
}

Any idea what I might be doing wrong with DotNetOpenAuth? Why can't I see the invalid request on the Server side?

1

1 Answers

0
votes

I solved. Get access_token don't use the webclient.ProcessUserAuthorization(Request) method .

 using (var client = new HttpClient())
        {
            var uri = new Uri($"http://localhost:24728/ClientAuthorization?tourl={tourl}");

            var httpContent = new FormUrlEncodedContent(new Dictionary<string, string>()
                {
                    {"code", code},
                    {"redirect_uri", uri.AbsoluteUri},
                    {"grant_type","authorization_code"},
                    {"client_id", ClientStartupProfile.Client.ClientId},
                    {"client_secret", ClientStartupProfile.Client.Secret}
                });

            var response = await client.PostAsync(ClientStartupProfile.AuthorizationServer.TokenUri, httpContent);
            var authorizationState = await response.Content.ReadAsAsync<AuthorizationState>();

            //判断access_token 是否获取成功
            if (!string.IsNullOrWhiteSpace(authorizationState.AccessToken))
                Response.Cookies.Add(new System.Web.HttpCookie("access_token", authorizationState.AccessToken));

            return Redirect(tourl);
        }