简体   繁体   中英

OpenIdDict and ASP.NET Core: 401 after successfully getting the token back (full repro)

still periodically struggling with OpenAuth using OpenIdDict (credentials flow) in ASP.NET Core, I updated to the latest OpenIdDict bits and VS2017 my old sample code you can find at https://github.com/Myrmex/repro-oidang , with a full step-by-step guidance to create an essential startup template. Hope this can be useful to the community to help getting started with simple security scenarios, so any contribution to that simple example code is welcome.

Essentially I followed the credentials flow sample from the OpenIdDict author, and I can get my token back when requesting it like (using Fiddler):

POST http://localhost:50728/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=password&scope=offline_access profile email roles&resource=http://localhost:4200&username=zeus&password=P4ssw0rd!

Problem is that when I try to use this token, I keep getting a 401, without any other hint: no exception, nothing logged. The request is like:

GET http://localhost:50728/api/values
Content-Type: application/json
Authorization: Bearer ...

Here is my relevant code: first Startup.cs :

public void ConfigureServices(IServiceCollection services)
{
    // setup options with DI
    // https://docs.asp.net/en/latest/fundamentals/configuration.html
    services.AddOptions();

    // CORS (note: if using Azure, remember to enable CORS in the portal, too!)
    services.AddCors();

    // add entity framework and its context(s) using in-memory 
    // (or use the commented line to use a connection string to a real DB)
    services.AddEntityFrameworkSqlServer()
        .AddDbContext<ApplicationDbContext>(options =>
        {
            // options.UseSqlServer(Configuration.GetConnectionString("Authentication")));
            options.UseInMemoryDatabase();
            // register the entity sets needed by OpenIddict.
            // Note: use the generic overload if you need
            // to replace the default OpenIddict entities.
            options.UseOpenIddict();
        });

    // register the Identity services
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    // configure Identity to use the same JWT claims as OpenIddict instead
    // of the legacy WS-Federation claims it uses by default (ClaimTypes),
    // which saves you from doing the mapping in your authorization controller.
    services.Configure<IdentityOptions>(options =>
    {
        options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
        options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
        options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
    });

    // register the OpenIddict services
    services.AddOpenIddict(options =>
    {
        // register the Entity Framework stores
        options.AddEntityFrameworkCoreStores<ApplicationDbContext>();

        // register the ASP.NET Core MVC binder used by OpenIddict.
        // Note: if you don't call this method, you won't be able to
        // bind OpenIdConnectRequest or OpenIdConnectResponse parameters
        // to action methods. Alternatively, you can still use the lower-level
        // HttpContext.GetOpenIdConnectRequest() API.
        options.AddMvcBinders();

        // enable the endpoints
        options.EnableTokenEndpoint("/connect/token");
        options.EnableLogoutEndpoint("/connect/logout");
        // http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
        options.EnableUserinfoEndpoint("/connect/userinfo");

        // enable the password flow
        options.AllowPasswordFlow();
        options.AllowRefreshTokenFlow();

        // during development, you can disable the HTTPS requirement
        options.DisableHttpsRequirement();

        // Note: to use JWT access tokens instead of the default
        // encrypted format, the following lines are required:
        // options.UseJsonWebTokens();
        // options.AddEphemeralSigningKey();
    });

    // add framework services
    services.AddMvc()
        .AddJsonOptions(options =>
        {
            options.SerializerSettings.ContractResolver =
                new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
        });

    // seed the database with the demo user details
    services.AddTransient<IDatabaseInitializer, DatabaseInitializer>();

    // swagger
    services.AddSwaggerGen();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
    IDatabaseInitializer databaseInitializer)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();
    loggerFactory.AddNLog();

    // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling
    if (env.IsDevelopment()) app.UseDeveloperExceptionPage();

    // to serve up index.html
    app.UseDefaultFiles();
    app.UseStaticFiles();

    // CORS
    // https://docs.asp.net/en/latest/security/cors.html
    app.UseCors(builder =>
            builder.WithOrigins("http://localhost:4200")
                .AllowAnyHeader()
                .AllowAnyMethod());

    // add a middleware used to validate access tokens and protect the API endpoints
    app.UseOAuthValidation();

    app.UseOpenIddict();

    app.UseMvc();

    // app.UseMvcWithDefaultRoute();
    // app.UseWelcomePage();

    // seed the database
    databaseInitializer.Seed().GetAwaiter().GetResult();

    // swagger
    // enable middleware to serve generated Swagger as a JSON endpoint
    app.UseSwagger();
    // enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.)
    app.UseSwaggerUi();
}

And then my controller (you can find the whole solution in the repository quoted above):

public sealed class AuthorizationController : Controller
{
    private readonly IOptions<IdentityOptions> _identityOptions;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly UserManager<ApplicationUser> _userManager;

    public AuthorizationController(
        IOptions<IdentityOptions> identityOptions,
        SignInManager<ApplicationUser> signInManager,
        UserManager<ApplicationUser> userManager)
    {
        _identityOptions = identityOptions;
        _signInManager = signInManager;
        _userManager = userManager;
    }

    private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user)
    {
        // Create a new ClaimsPrincipal containing the claims that
        // will be used to create an id_token, a token or a code.
        ClaimsPrincipal principal = await _signInManager.CreateUserPrincipalAsync(user);

        // Create a new authentication ticket holding the user identity.
        AuthenticationTicket ticket = new AuthenticationTicket(
            principal, new AuthenticationProperties(),
            OpenIdConnectServerDefaults.AuthenticationScheme);

        // Set the list of scopes granted to the client application.
        // Note: the offline_access scope must be granted
        // to allow OpenIddict to return a refresh token.
        ticket.SetScopes(new[] {
            OpenIdConnectConstants.Scopes.OpenId,
            OpenIdConnectConstants.Scopes.Email,
            OpenIdConnectConstants.Scopes.Profile,
            OpenIdConnectConstants.Scopes.OfflineAccess,
            OpenIddictConstants.Scopes.Roles
        }.Intersect(request.GetScopes()));

        ticket.SetResources("resource-server");

        // Note: by default, claims are NOT automatically included in the access and identity tokens.
        // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
        // whether they should be included in access tokens, in identity tokens or in both.
        foreach (var claim in ticket.Principal.Claims)
        {
            // Never include the security stamp in the access and identity tokens, as it's a secret value.
            if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
                continue;

            List<string> destinations = new List<string>
            {
                OpenIdConnectConstants.Destinations.AccessToken
            };

            // Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
            // The other claims will only be added to the access_token, which is encrypted when using the default format.
            if (claim.Type == OpenIdConnectConstants.Claims.Name &&
                ticket.HasScope(OpenIdConnectConstants.Scopes.Profile) ||
                claim.Type == OpenIdConnectConstants.Claims.Email &&
                ticket.HasScope(OpenIdConnectConstants.Scopes.Email) ||
                claim.Type == OpenIdConnectConstants.Claims.Role &&
                ticket.HasScope(OpenIddictConstants.Claims.Roles))
            {
                destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
            }

            claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken);
        }

        return ticket;
    }

    [HttpPost("~/connect/token"), Produces("application/json")]
    public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
    {
        // if you prefer not to bind the request as a parameter, you can still use:
        // OpenIdConnectRequest request = HttpContext.GetOpenIdConnectRequest();

        Debug.Assert(request.IsTokenRequest(),
            "The OpenIddict binder for ASP.NET Core MVC is not registered. " +
            "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");

        if (!request.IsPasswordGrantType())
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
                ErrorDescription = "The specified grant type is not supported."
            });
        }

        ApplicationUser user = await _userManager.FindByNameAsync(request.Username);
        if (user == null)
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.InvalidGrant,
                ErrorDescription = "The username/password couple is invalid."
            });
        }

        // Ensure the user is allowed to sign in.
        if (!await _signInManager.CanSignInAsync(user))
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.InvalidGrant,
                ErrorDescription = "The specified user is not allowed to sign in."
            });
        }

        // Reject the token request if two-factor authentication has been enabled by the user.
        if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.InvalidGrant,
                ErrorDescription = "The specified user is not allowed to sign in."
            });
        }

        // Ensure the user is not already locked out.
        if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.InvalidGrant,
                ErrorDescription = "The username/password couple is invalid."
            });
        }

        // Ensure the password is valid.
        if (!await _userManager.CheckPasswordAsync(user, request.Password))
        {
            if (_userManager.SupportsUserLockout)
                await _userManager.AccessFailedAsync(user);

            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.InvalidGrant,
                ErrorDescription = "The username/password couple is invalid."
            });
        }

        if (_userManager.SupportsUserLockout)
            await _userManager.ResetAccessFailedCountAsync(user);

        // Create a new authentication ticket.
        AuthenticationTicket ticket = await CreateTicketAsync(request, user);

        var result = SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
        return result;
        // return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
    }

    [HttpGet("~/connect/logout")]
    public async Task<IActionResult> Logout()
    {
        // Extract the authorization request from the ASP.NET environment.
        OpenIdConnectRequest request = HttpContext.GetOpenIdConnectRequest();

        // Ask ASP.NET Core Identity to delete the local and external cookies created
        // when the user agent is redirected from the external identity provider
        // after a successful authentication flow (e.g Google or Facebook).
        await _signInManager.SignOutAsync();

        // Returning a SignOutResult will ask OpenIddict to redirect the user agent
        // to the post_logout_redirect_uri specified by the client application.
        return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme);
    }

    // http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
    [Authorize]
    [HttpGet("~/connect/userinfo")]
    public async Task<IActionResult> GetUserInfo()
    {
        ApplicationUser user = await _userManager.GetUserAsync(User);

        // to simplify, in this demo we just have 1 role for users: either admin or editor
        string sRole = await _userManager.IsInRoleAsync(user, "admin")
            ? "admin"
            : "editor";

        // http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
        return Ok(new
        {
            sub = user.Id,
            given_name = user.FirstName,
            family_name = user.LastName,
            name = user.UserName,
            user.Email,
            email_verified = user.EmailConfirmed,
            roles = sRole
        });
    }
}

As mentioned in this blog post , the token format used by OpenIddict slightly changed recently, which makes tokens issued by the latest OpenIddict bits incompatible with the old OAuth2 validation middleware version you're using.

Migrate to AspNet.Security.OAuth.Validation 1.0.0 and it should work.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM