首页 > 解决方案 > 使用(本地)Gitlab 服务器作为 OAuth 提供者

问题描述

我想让 .NET Core 2.1 MVC 应用程序使用我的内部 Gitlab 服务器作为 OAuth 身份验证服务提供者。在 Gitlab 管理区域内,我添加了一个应用程序:

Application Id: xxx
Secret: xxx
Callback url: http://localhost:5000/Account/ExternalLoginCallback
Trusted: Y
Scopes: - api (Access the authenticated user's API)
        - openid (Authenticate using OpenID Connect)

Startup.ConfigureServices类似于:

services.AddAuthentication().AddOAuth("GitLab", options => 
        {
            options.ClientId = "xxx";
            options.ClientSecret = "xxx";
            options.CallbackPath = new PathString("/Account/ExternalLoginCallback");

            options.AuthorizationEndpoint = "https://myGitlabServer/oauth/authorize";
            options.TokenEndpoint = "https://myGitlabServer/login/oauth/token";
            options.UserInformationEndpoint = "https://myGitlabServer/api/v4/user";

            options.SaveTokens = true;

            options.Events = new OAuthEvents
            {
                OnCreatingTicket = async context =>
                {
                    var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
                    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);

                    var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
                    response.EnsureSuccessStatusCode();

                    var user = JObject.Parse(await response.Content.ReadAsStringAsync());

                    context.RunClaimActions(user);
                }
            };
        });

在导航到我的应用程序的登录页面时,我可以选择 GitLab 作为登录提供程序并成功重定向到登录页面。在使用正确的凭据唱歌后,我希望控制器被调用,但重定向失败。

Exception: OAuth token endpoint failure: Status: NotFound;Headers: Server: nginx

对应的请求是:

GET http://localhost:5000/Account/ExternalLoginCallback?code=xxx&state=xxx HTTP/1.1

AccountController签名看起来像:

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
    {
       ...
    }

任何想法我错过了什么或做错了什么?

标签: asp.net-core

解决方案


有几件事要纠正。

首先 localhost 不适用于部署在网络中某处的应用程序,因此我必须更改回调 url

http://localhost:5000/Account/ExternalLoginCallback

Callback url: http://{IP}:5000/signin-gitlab 

注意重命名/Account/ExternalLoginCallback为 just /signin-gitlab

Startup.ConfigureServices(相关部分)现在:

        ...

        services.Configure<CookiePolicyOptions>(options =>
        {

            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddOAuth("Gitlab", options =>
        {
            options.SignInScheme = IdentityConstants.ExternalScheme;

            // App creds
            options.ClientId = "xxx";
            options.ClientSecret = "xxx";                

            options.CallbackPath = new PathString("/signin-gitlab");

            options.AuthorizationEndpoint = "https://myGitlabServer/oauth/authorize";
            options.TokenEndpoint = "https://myGitlabServer/oauth/token";
            options.UserInformationEndpoint = "https://myGitlabServer/api/v4/user";

            options.SaveTokens = true;

            options.Events = new OAuthEvents
            {
                OnCreatingTicket = async context =>
                {
                    var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
                    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                    var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
                    response.EnsureSuccessStatusCode();

                    var user = JObject.Parse(await response.Content.ReadAsStringAsync());

                    // Add claims

                    var userId = user.Value<string>("username");
                    if (!string.IsNullOrEmpty(userId))
                    {
                        context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                        options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, userId);
                    }

                    var name = user.Value<string>("name");
                    if (!string.IsNullOrEmpty(name))
                    {
                        context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, name, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                        options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, name);
                    }

                    var email = user.Value<string>("email");
                    if (!string.IsNullOrEmpty(email))
                    {
                        context.Identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.Email, context.Options.ClaimsIssuer));
                        options.ClaimActions.MapJsonKey(ClaimTypes.Email, email);
                    }

                    var avatar = user.Value<string>("avatar_url");
                    if (!string.IsNullOrEmpty(avatar))
                    {
                        context.Identity.AddClaim(new Claim(ClaimTypes.Uri, avatar, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                        options.ClaimActions.MapJsonKey(ClaimTypes.Uri, avatar);
                    }
                }
            };
        })
        .AddCookie();

        ...

在我的内部,我AccountController有 2 种方法来处理外部登录:

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public IActionResult ExternalLogin(string provider, string returnUrl = null)
    {
        // Request a redirect to the external login provider.
        var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
        var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);

        return Challenge(properties, provider);
    }

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
    {
        if (remoteError != null)
        {
            ErrorMessage = $"Error from external provider: {remoteError}";
            return RedirectToAction(nameof(Login));
        }

        var info = await _signInManager.GetExternalLoginInfoAsync();

        if (info == null)
        {
            return RedirectToAction(nameof(Login));
        }

        // Sign in 
        var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);

        if (result.Succeeded)
        {
            var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
            var claimsPrincipal = await this._signInManager.CreateUserPrincipalAsync(user);
            var identity = claimsPrincipal.Identity as ClaimsIdentity;

            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

            var updateResult = await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
            return RedirectToLocal(returnUrl);
        }

        if (result.IsLockedOut)
        {
            return RedirectToAction(nameof(Lockout));
        }
        else
        {
            // If the user doesn't have an account, then ask to create one
            ViewData["ReturnUrl"] = returnUrl;
            ViewData["LoginProvider"] = info.LoginProvider;
            var email = info.Principal.FindFirstValue(ClaimTypes.Email);
            return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
        }
    }

最后是我的登录视图中的视图部分:

        <section>
            <h4>Use another service to log in.</h4>
            <hr />
            @{
                var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
                if (loginProviders.Count == 0)
                {
                    <div>
                        <p>
                            There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                            for details on setting up this ASP.NET application to support logging in via external services.
                        </p>
                    </div>
                }
                else
                {
                    <form asp-action="ExternalLogin" asp-route-returnurl="@Context.Request.Query["returnUrl"]" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in loginProviders)
                                {
                                    <button type="submit" class="btn btn-gitlab" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account"><i class="fab fa-@provider.Name.ToLower()"></i>&nbsp; @provider.Name</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>

推荐阅读