2

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.

Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131
Mickael Caruso
  • 8,721
  • 11
  • 40
  • 72

1 Answers1

1

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:

Community
  • 1
  • 1
Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131
  • Thank you very much for this information. I now get an access token. Now, my resource server is in a separate project. When I decorate a controller or its functions with [Authorize], it will let me through when I don't include an Authorization: Bearer header. Can you also point me to how to consume the token issued here? I thought I've followed everything I think is right from examples from github and others' as well. – Mickael Caruso Jun 09 '16 at 14:59
  • The token validation part didn't change and is not managed by the OIDC server middleware but by either the JWT middleware (if you force the OIDC server to issue JWT tokens), the new OAuth2 validation middleware (for encrypted tokens, the new default format) or the OAuth2 introspection middleware. Maybe you should post a new question and share some code to determine why `[Authorize]` doesn't work as intended. – Kévin Chalet Jun 09 '16 at 15:16
  • I started the resource server project from scratch (Empty project). As I was building the controller and the startup file, I made sure to install only rc2 packages. I must have had incorrect packages, though that might not be the real cause, but nevertheless, everything now works. Thank you. – Mickael Caruso Jun 09 '16 at 16:48
  • Great! Feel free to mark this answer as the accepted one if you don't have another question ;) – Kévin Chalet Jun 09 '16 at 16:59