簡體   English   中英

通過使用 OpenIddict 與外部提供者登錄來獲取令牌

[英]Get Token by loging in with External Providers using OpenIddict

我有一個帶有 ASP.NET Core 的 API,它將被本機移動應用程序(當前是 UWP、Android)使用,我正在嘗試實現一種客戶端可以使用用戶名/密碼和外部提供程序注冊和登錄的方式,例如谷歌和臉書。 現在我正在使用openIddict並且我的ExternalProviderCallback必須返回我認為當前返回 cookie 的本地令牌! (我已經從某處復制了大部分代碼)而且它似乎不是 AuthorizationCodeFlow,我認為這是正確的方法!

現在這是我的創業班

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

        if (env.IsDevelopment())
        {
            builder.AddUserSecrets();
        }
        builder.AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IConfiguration>(c => Configuration);
        services.AddEntityFramework();
        services.AddIdentity<ApplicationUser, IdentityRole>(config =>
        {
            //Setting some configurations
            config.User.RequireUniqueEmail = true;
            config.Password.RequireNonAlphanumeric = false;
            config.Cookies.ApplicationCookie.AutomaticChallenge = false;
            config.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents()
            {
                OnRedirectToLogin = context =>
                {
                    if (context.Request.Path.StartsWithSegments("/api") && 
                    context.Response.StatusCode == 200)
                        context.Response.StatusCode = 401;
                    return Task.CompletedTask;
                },
                OnRedirectToAccessDenied = context =>
                {
                    if (context.Request.Path.StartsWithSegments("/api") && 
                    context.Response.StatusCode == 200)
                        context.Response.StatusCode = 403;
                    return Task.CompletedTask;
                }
            };
        })
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
        services.AddDbContext<ApplicationDbContext>(options =>
        {
            options.UseSqlite(Configuration["Data:DefaultConnection:ConnectionString"]);
            options.UseOpenIddict();
        });

        services.AddOpenIddict()
            .AddEntityFrameworkCoreStores<ApplicationDbContext>()
            .UseJsonWebTokens()
            .AddMvcBinders()
            .EnableAuthorizationEndpoint(Configuration["Authentication:OpenIddict:AuthorizationEndPoint"])
            .EnableTokenEndpoint(Configuration["Authentication:OpenIddict:TokenEndPoint"])
            .AllowPasswordFlow()
            .AllowAuthorizationCodeFlow()
            .AllowImplicitFlow()
            .AllowRefreshTokenFlow()
            .DisableHttpsRequirement()
            .AddEphemeralSigningKey()
            .SetAccessTokenLifetime(TimeSpan.FromMinutes(2))
            .SetRefreshTokenLifetime(TimeSpan.FromMinutes(10));
        services.AddSingleton<DbSeeder>();
        services.AddMvc(options =>
        {
            options.SslPort = 44380;
            options.Filters.Add(new RequireHttpsAttribute());
        });
    }

    // 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, DbSeeder dbSeeder)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseIdentity();
        app.UseOAuthValidation();

        app.UseGoogleAuthentication(new GoogleOptions()
        {
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
            ClientId = Configuration["Authentication:Google:ClientId"],
            ClientSecret = Configuration["Authentication:Google:ClientSecret"],
            CallbackPath = "/signin-google",
            Scope = { "email" }
        });
        app.UseFacebookAuthentication(new FacebookOptions()
        {
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
            AppId = Configuration["Authentication:Facebook:AppId"],
            AppSecret = Configuration["Authentication:Facebook:AppSecret"],
            CallbackPath = "/signin-facebook",
            Scope = { "email" }
        });

        app.UseOpenIddict();

        app.UseMvcWithDefaultRoute();
        try
        {
            dbSeeder.SeedAsync().Wait();
        }
        catch (AggregateException ex)
        {
            throw new Exception(ex.ToString());
        }
    }
}

這是AccountController,它正在做外部提供者的工作:

[Route("api/[controller]")]
public class AccountsController : BaseController
{
    private readonly IConfiguration _configuration;

    #region Constructor

    public AccountsController(ApplicationDbContext context,
        SignInManager<ApplicationUser> signInManager,
        UserManager<ApplicationUser> userManager,
        IConfiguration configuration)
        : base(context, signInManager, userManager)
    {
        _configuration = configuration;
    }

    #endregion Constructor


    #region External Authentication Providers 

    // GET: /api/Accounts/ExternalLogin 
    [HttpGet("ExternalLogin/{provider}")]
    public IActionResult ExternalLogin(string provider, string returnUrl = null)
    {
        switch (provider.ToLower())
        {
            case "facebook":
            case "google":
            case "twitter":
                // Request a redirect to the external login provider.
                var redirectUrl = Url.Action("ExternalLoginCallback",
                    "Accounts", new { ReturnUrl = returnUrl });
                var properties =
                    SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
                return Challenge(properties, provider);
            default:
                return BadRequest(new
                {
                    Error = $"Provider '{provider}' is not supported."
                });
        }
    }

    [HttpGet("ExternalLoginCallBack")]
    public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null,
        string remoteError = null)
    {
        try
        {
            if (remoteError != null)
            {
                throw new Exception(remoteError);
            }
            var info = await SignInManager.GetExternalLoginInfoAsync();
            if (info == null)
            {
                throw new Exception("ERROR: No login info available.");
            }
            var user = await UserManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
            if (user == null)
            {
                var emailKey =
                    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
                var email = info.Principal.FindFirst(emailKey).Value;
                user = await UserManager.FindByEmailAsync(email);
                if (user == null)
                {
                    var now = DateTime.Now;
                    var idKey =
                        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
                    var username = string.Format("{0}{1}", info.LoginProvider,
                        info.Principal.FindFirst(idKey).Value);
                    user = new ApplicationUser
                    {
                        UserName = username,
                        Email = email,
                        CreatedDate = now,
                        LastModifiedDate = now
                    };
                    await UserManager.CreateAsync(user, "SomePass4ExProvider123+-");
                    await UserManager.AddToRoleAsync(user, "Registered");
                    user.EmailConfirmed = true;
                    user.LockoutEnabled = false;
                }
                await UserManager.AddLoginAsync(user, info);
                await DbContext.SaveChangesAsync();
            }
            // create the auth JSON object 
            var auth = new
            {
                type = "External",
                providerName = info.LoginProvider
            };

            // output a <SCRIPT> tag to call a JS function registered into the parent window global scope
            return Content("<script type=\"text / javascript\">" +
                           "window.opener.externalProviderLogin(" +
                           JsonConvert.SerializeObject(auth) + ");" +
                           "window.close();" + "</script>", "text/html");

        }
        catch (Exception ex)
        {
            return BadRequest(new {Error = ex.Message});
        }
    }

    [HttpPost("Logout")]
    public IActionResult Logout()
    {
        if (HttpContext.User.Identity.IsAuthenticated)
        {
            SignInManager.SignOutAsync().Wait();
        }
        return Ok();
    }

    #endregion External Authentication Providers 
}

最后是將生成令牌的 ConnectController :

[Route("api/[controller]")]
public class ConnectController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IConfiguration _configuration;

    public ConnectController(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IConfiguration configuration)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _configuration = configuration;
    }

    [HttpPost("token"), Produces("application/json")]
    public async Task<IActionResult> Token(OpenIdConnectRequest request)
    {
        if (request.IsPasswordGrantType())
        {
            var user = await _userManager.FindByNameAsync(request.Username);

            #region Authenticate User

            if (user == null)
            {
                // Return bad request if the user doesn't exist
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "Invalid username or password"
                });
            }
            if (!await _signInManager.CanSignInAsync(user) ||
                (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)))
            {

                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The specified user cannot sign in."
                });
            }

            if (!await _userManager.CheckPasswordAsync(user, request.Password))
            {
                // Return bad request if the password is invalid
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "Invalid username or password"
                });
            }

            // The user is now validated, so reset lockout counts, if necessary
            if (_userManager.SupportsUserLockout)
            {
                await _userManager.ResetAccessFailedCountAsync(user);
            }

            #endregion

            var identity = new ClaimsIdentity(
                OpenIdConnectServerDefaults.AuthenticationScheme,
                OpenIdConnectConstants.Claims.Name, null);

            identity.AddClaim(OpenIdConnectConstants.Claims.Subject,
                user.Id,
                OpenIdConnectConstants.Destinations.AccessToken);

            identity.AddClaim(OpenIdConnectConstants.Claims.Name, 
                user.DisplayName??user.UserName,
                OpenIdConnectConstants.Destinations.AccessToken);


            var principal = new ClaimsPrincipal(identity);

            var ticket = await CreateTicketAsync(principal, request, new AuthenticationProperties());

            return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
        }
        if (request.IsRefreshTokenGrantType())
        {
            var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
                OpenIdConnectServerDefaults.AuthenticationScheme);



            var id = info.Principal.FindFirst(OpenIdConnectConstants.Claims.Subject)?.Value;
            var user = await _userManager.FindByIdAsync(id);

            if (user == null)
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The refresh token is no longer valid."
                });
            }

            if (!await _signInManager.CanSignInAsync(user))
            {
                return BadRequest(new OpenIdConnectResponse
                {
                    Error = OpenIdConnectConstants.Errors.InvalidGrant,
                    ErrorDescription = "The user is no longer allowed to sign in."
                });
            }
            var identity = new ClaimsIdentity(
                OpenIdConnectServerDefaults.AuthenticationScheme,
                OpenIdConnectConstants.Claims.Name, null);

            identity.AddClaim(OpenIdConnectConstants.Claims.Subject,
                user.Id,
                OpenIdConnectConstants.Destinations.AccessToken);

            identity.AddClaim(OpenIdConnectConstants.Claims.Name,
                user.DisplayName ?? user.UserName,
                OpenIdConnectConstants.Destinations.AccessToken);

            // ... add other claims, if necessary.

            var principal = new ClaimsPrincipal(identity);
            var ticket = await CreateTicketAsync(principal,request, info.Properties);

            // Ask OpenIddict to generate a new token and return an OAuth2 token response.
            return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
        }

        // Return bad request if the request is not for password grant type
        return BadRequest(new OpenIdConnectResponse
        {
            Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
            ErrorDescription = "The specified grant type is not supported."
        });
    }
    private async Task<AuthenticationTicket> CreateTicketAsync(ClaimsPrincipal principal,
        OpenIdConnectRequest request,
       AuthenticationProperties properties = null)
    {

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

        if (!request.IsRefreshTokenGrantType())
        {
            //TODO : // Include resources and scopes, **as APPROPRIATE**
            // 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[]
            {
                /* openid: */ OpenIdConnectConstants.Scopes.OpenId,
                /* email: */ OpenIdConnectConstants.Scopes.Email,
                /* profile: */ OpenIdConnectConstants.Scopes.Profile,
                /* offline_access: */ OpenIdConnectConstants.Scopes.OfflineAccess,
                /* roles: */ OpenIddictConstants.Scopes.Roles
            }.Intersect(request.GetScopes()));
        }
        return ticket;
    }

    #region Authorization code, implicit and implicit flows

    // Note: to support interactive flows like the code flow,
    // you must provide your own authorization endpoint action:

    [Authorize, HttpGet("authorize")]
    public IActionResult Authorize(OpenIdConnectRequest request)
    {
        return Ok();
    }

    #endregion
}

這就是我發送請求的方式:

https://localhost:44380/api/Accounts/ExternalLogin/Google?returnUrl=https://localhost:44380

它成功返回到我在 AccountsController 中的 ExternalLoginCallback 操作,但沒有 JWT 令牌作為正常的 PasswordGrantFlow 發送回用戶。

如果有可能,請在此處向我發送代碼,不要將我重定向到其他地方,因為我對服務器端完全陌生,而且我之前已經進行過搜索。

嘗試Velusia 示例授權代碼流

如果您想立即將您的用戶重定向到指定的社交提供商而不是將他們返回到登錄頁面,您可以調整您的授權端點:

[HttpGet("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
    Debug.Assert(request.IsAuthorizationRequest(),
        "The OpenIddict binder for ASP.NET Core MVC is not registered. " +
        "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");

    if (!User.Identity.IsAuthenticated)
    {
        // Resolve the optional provider name from the authorization request.
        // If no provider is specified, call Challenge() to redirect the user
        // to the login page defined in the ASP.NET Core Identity options.
        var provider = (string) request.GetParameter("identity_provider");
        if (string.IsNullOrEmpty(provider))
        {
            return Challenge();
        }

        // Ensure the specified provider is supported.
        if (!HttpContext.Authentication.GetAuthenticationSchemes()
            .Where(description => !string.IsNullOrEmpty(description.DisplayName))
            .Any(description => description.AuthenticationScheme == provider))
        {
            return Challenge();
        }

        // When using ASP.NET Core Identity and its default AccountController,
        // the user must be redirected to the ExternalLoginCallback action
        // before being redirected back to the authorization endpoint.
        var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider,
            Url.Action("ExternalLoginCallback", "Account", new
            {
                ReturnUrl = Request.PathBase + Request.Path + Request.QueryString
            }));

        return Challenge(properties, provider);
    }

    // Retrieve the application details from the database.
    var application = await _applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted);
    if (application == null)
    {
        return View("Error", new ErrorViewModel
        {
            Error = OpenIdConnectConstants.Errors.InvalidClient,
            ErrorDescription = "Details concerning the calling client application cannot be found in the database"
        });
    }

    // Flow the request_id to allow OpenIddict to restore
    // the original authorization request from the cache.
    return View(new AuthorizeViewModel
    {
        ApplicationName = application.DisplayName,
        RequestId = request.RequestId,
        Scope = request.Scope
    });
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM