仍然定期在 ASP.NET Core 中使用 OpenIdDict(凭据流)与 OpenAuth 苦苦挣扎,我更新到了最新的 OpenIdDict 位和 VS2017 我的旧示例代码,您可以在 https://github.com/Myrmex/repro-oidang 找到,并提供完整的分步指导以创建必要的启动模板。希望这对社区有用,以帮助开始使用简单的安全场景,因此欢迎对该简单示例代码的任何贡献。

本质上,我遵循了 OpenIdDict 作者的凭据流示例,并且可以在请求时取回我的 token (使用 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!

问题是,当我尝试使用此 token 时,我不断收到 401,没有任何其他提示:没有异常(exception),没有任何记录。请求是这样的:
GET http://localhost:50728/api/values
Content-Type: application/json
Authorization: Bearer ...

这是我的相关代码:首先是 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();
}

然后是我的 Controller (您可以在上面引用的存储库中找到整个解决方案):
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
        });
    }
}

最佳答案

正如在 blog post 中提到的,OpenIddict 使用的 token 格式最近略有变化,这使得最新的 OpenIddict 位发布的 token 与您使用的旧 OAuth2 验证中间件版本不兼容。

迁移到 AspNet.Security.OAuth.Validation 1.0.0 它应该可以工作。

关于c# - OpenIdDict 和 ASP.NET Core : 401 after successfully getting the token back (full repro),我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/43271851/

10-13 03:17