Search code examples
asp.net-coreopenid-connectaspnet-contrib

Issuing Tokens with AspNet.Security.OpenIdConnect.Server (RC2) - Invalid Client?


I was able to pull this in RC1. The OpenIdConnectServerProvider has changed quite a bit.

I am interested in the resource owner flow, so my AuthorizationProvider looks like this:

    public sealed class AuthorizationProvider : OpenIdConnectServerProvider
    {

        public override Task MatchEndpoint(MatchEndpointContext context)
        {
            if (context.Options.AuthorizationEndpointPath.HasValue &&
                context.Request.Path.StartsWithSegments(context.Options.AuthorizationEndpointPath))
            {
                context.MatchesAuthorizationEndpoint();
            }

            return Task.FromResult<object>(null);
        }

        public override async Task ValidateAuthorizationRequest(ValidateAuthorizationRequestContext context)
        {
            context.Validate();
            await Task.FromResult<object>(null);
        }

        public override async Task ValidateTokenRequest(ValidateTokenRequestContext context)
        {                
            if (!context.Request.IsAuthorizationCodeGrantType() &&
                !context.Request.IsRefreshTokenGrantType() &&
                !context.Request.IsPasswordGrantType())
            {
                context.Reject(
                    error: "unsupported_grant_type",
                    description: "Only authorization code, refresh token, and ROPC grant types " +
                                 "are accepted by this authorization server");
            }

            /* This is where the problem is. This context.Validate()
               will automatically return a 400, server_error, with
               message "An internal server error occurred."

               If I commented this out, I will get a 400, invalid_client.

               If I put in an arbitrary client like "any_client", it
               goes to GrantResourceOwnerCredentials, as I expect.
               However, I get a 500 with no explanation when it executes.
               See the function below for more details.
            */
            context.Validate();
            await Task.FromResult<object>(null);
        }

        public override Task HandleUserinfoRequest(HandleUserinfoRequestContext context)
        {                
            context.SkipToNextMiddleware();

            return Task.FromResult<object>(null);
        }

        public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
        {
             MYDbContext db = context.HttpContext.RequestServices.GetRequiredService<MyDbContext>();
             UserManager<MyUser> UM = context.HttpContext.RequestServices.GetRequiredService<UserManager<MyUser>>();

             MyUser user = await UM.FindByNameAsync(context.Request.Username);

             if (user == null)
             {
                context.Reject(
                   error: "user_not_found",
                   description: "User not found");

                return;
             }

             bool passwordsMatch = await UM.CheckPasswordAsync(user, context.Request.Password);

             if (!passwordsMatch)
             {
                 context.Reject(
                    error: "invalid_credentials",
                    description: "Password is incorrect");

                 return;
             }

             var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);

            identity.AddClaim(ClaimTypes.Name, user.UserName, "id_token token");

            /* I set the breakpoint on this line, and the execution
               does not hit this breakpoint. I immediately get a 500.
               My output says 'System.ArgumentException' in
               AspNet.Security.OpenIdConnect.Extensions.dll
            */
            List<string> roles = (await UM.GetRolesAsync(user)).ToList();

            roles.ForEach(role =>
            {
                identity.AddClaim(ClaimTypes.Role, role, "id_token token");
            });

            var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity),
                                                  new AuthenticationProperties(),
                                                  context.Options.AuthenticationScheme);

            ticket.SetResources(new[] { "mlm_resource_server" });
            ticket.SetAudiences(new[] { "mlm_resource_server" });
            ticket.SetScopes(new[] { "defaultscope" });

            context.Validate(ticket);
        }
    }

By the way, I'm trying to run this on Fiddler:

POST /token HTTP/1.1
Host: localhost:56785
Content-Type: application/x-www-form-urlencoded

username=user&password=pw&grant_type=password

When the password is incorrect, I get the expected 400 denial, but when the password is correct, I get that 500.

What am I missing? Is the way I build that user identity now incorrect? Am I supposed to override another function?

Note - I didn't provide my startup file because I thought it's irrelevant. I'll post it later if it's absolutely needed.


Solution

  • If you had enabled logging, you'd have immediately understood what was happening: the OpenID Connect server middleware doesn't allow you to mark the token request as "fully validated" when the client_id is missing from the request:

    if (context.IsValidated && string.IsNullOrEmpty(request.ClientId)) {
        Logger.LogError("The token request was validated but the client_id was not set.");
    
        return await SendTokenResponseAsync(request, new OpenIdConnectMessage {
            Error = OpenIdConnectConstants.Errors.ServerError,
            ErrorDescription = "An internal server error occurred."
        });
    }
    

    If you want to make client authentication optional, call context.Skip() instead.


    Note that there are a couple of issues with your provider:

    ValidateAuthorizationRequest doesn't validate anything, which is terrible as any redirect_uri would be considered as valid (= a huge open redirect flaw). Luckily, since you're only interested in the ROPC grant, you're likely not implementing any interactive flow. I'd recommend removing this method (you can also remove MatchEndpoint too).


    Your initial grant check in ValidateTokenRequest is buggy as you don't stop the execution of your code after calling context.Reject(), which ultimately results in context.Validate() being invoked.


    identity.AddClaim(ClaimTypes.Name, user.UserName, "id_token token") is no longer a valid syntax. The ArgumentException is likely caused by this check:

    if (destinations.Any(destination => destination.Contains(" "))) {
        throw new ArgumentException("Destinations cannot contain spaces.", nameof(destinations));
    }
    

    Instead use this:

    identity.AddClaim(ClaimTypes.Name, user.UserName,
        OpenIdConnectConstants.Destinations.AccessToken,
        OpenIdConnectConstants.Destinations.IdentityToken);
    

    If you're still unsure how your provider should look like, don't hesitate to take a look at these concrete samples: