将用户授权限制在我的 Google 域

Limit user authorization to my Google domain

应该可以将 Google API OAuth2 请求限制到特定的 google 域。过去可以通过在请求的 &hd=mydomain.com 末尾进行黑客攻击来实现。使用新的 MVC 身份验证似乎不再可能。有什么想法吗?

 public class AppFlowMetadata : FlowMetadata
    {
        private static readonly IAuthorizationCodeFlow flow =
            new AppGoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
            {
                ClientSecrets = new ClientSecrets
                {
                    ClientId = "***.apps.googleusercontent.com",
                    ClientSecret = "******"
                },
                Scopes = new[] { DriveService.Scope.Drive },
                DataStore = new FileDataStore(HttpContext.Current.Server.MapPath("~/App_Data"), true) ,
            });  

        public override string GetUserId(Controller controller)
        {
            // In this sample we use the session to store the user identifiers.
            // That's not the best practice, because you should have a logic to identify
            // a user. You might want to use "OpenID Connect".
            // You can read more about the protocol in the following link:
            // https://developers.google.com/accounts/docs/OAuth2Login.
            var user = controller.Session["user"];
            if (user == null)
            {
                user = Guid.NewGuid();
                controller.Session["user"] = user;
            }
            return user.ToString();

        }

        public override IAuthorizationCodeFlow Flow
        {
            get { return flow; }
        }
    }

public class AppGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
    {
        public AppGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }

        public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(String redirectUri)
        {

            var authorizeUri = new Uri(AuthorizationServerUrl).AddQuery("hd", "ourgoogledomain.com"); //is not in the request
            var authUrl = new GoogleAuthorizationCodeRequestUrl(authorizeUri)
            {
                ClientId = ClientSecrets.ClientId,
                Scope = string.Join(" ", Scopes),
                RedirectUri = redirectUri,
                //AccessType = "offline",
               // ApprovalPrompt = "force"
            };
            return authUrl;
        }
    }

下载源代码后,我发现很容易将请求对象子类化,并添加自定义参数:

    public class GoogleDomainAuthorizationCodeRequestUrl : GoogleAuthorizationCodeRequestUrl
    {
        /// <summary>
        /// Gets or sets the hosted domain. 
        /// When you want to limit authorizing users from a specific domain 
        /// </summary>
        [Google.Apis.Util.RequestParameterAttribute("hd", Google.Apis.Util.RequestParameterType.Query)]
        public string Hd { get; set; }

        public GoogleDomainAuthorizationCodeRequestUrl(Uri authorizationServerUrl) : base(authorizationServerUrl)
        {
        }
    }

    public class AppGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
    {
        public AppGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }

        public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(String redirectUri)
        {
            var authUrl = new GoogleDomainAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
            {
                Hd = "mydomain.com",
                ClientId = ClientSecrets.ClientId,
                Scope = string.Join(" ", Scopes),
                RedirectUri = redirectUri
            };

            return authUrl;
        }
    }

传递 hd 参数确实是限制用户访问您的域的正确方法。但是,重要的是您 验证 用户确实属于该托管域。我在你的 answer 中看到你想出了如何将这个参数添加回你的请求,所以我将解决这个问题的第二部分。

问题是用户实际上可以在他们的客户端中修改请求的 URL 并删除 hd 参数!因此,虽然最好为您的用户传递此参数 UI,但您还需要验证经过身份验证的用户确实属于该域。

要查看用户属于哪个托管 Google Apps for Work 域(如果有),您必须在范围列表中包含 email你授权。然后,执行以下操作之一:

选项 1. 验证 ID 令牌。

当您将代码交换为访问令牌时,令牌端点还将 return id_token 参数中的 ID 令牌(假设您在请求中包含身份范围,例如 email).如果用户是托管域的一部分,则会出现 hd 声明,您应该检查它是否存在,并与您的预期相符。

您可以在 Google 上阅读有关 ID 令牌的更多信息 OpenID Connect docs (including some links to sample code and libraries to help you decode them). This tool 可以在测试期间解码 ID 令牌。

选项 2.调用 UserInfo

获得 OAuth 访问令牌后,使用 header 中的访问令牌向 https://www.googleapis.com/plus/v1/people/me/openIdConnect 执行 GET 请求。它将 return 一个关于用户声明的 JSON 字典。如果用户是托管域的一部分,则会出现 hd 声明,您应该检查它是否存在,并且与您的预期相符。

documentation for Google's UserInfo endpoint 阅读更多内容。

选项 1 和选项 2 之间的主要区别在于,使用 ID 令牌,您可以避免另一个 HTTP round-trip 到服务器,从而使其更快,更少 error-prone。您可以使用 OAuth2 Playground.

以交互方式尝试这两个选项

@AMH,要以最简单的方式做,您应该创建自己的 Google 提供程序,覆盖方法 ApplyRedirect 并将附加参数(如 hd)附加到将用于重定向到新 google 的地址授权页面:

public class GoogleAuthProvider : GoogleOAuth2AuthenticationProvider
{
    public override void ApplyRedirect(GoogleOAuth2ApplyRedirectContext context)
    {
        var newRedirectUri = context.RedirectUri;
        newRedirectUri += string.Format("&hd={0}", "your_domain.com");

        context.Response.Redirect(newRedirectUri);
    }
}

之后 link 您可以选择的新供应商:

app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
    ClientId = "your id",
    ClientSecret = "your secret",
    Provider = new GoogleAuthProvider(),
});

随着 .NET core package 的更新,以前的答案不再适用。幸运的是,在新的实现中,有一种方法可以挂钩身份验证事件来执行此类任务。

您将需要一个 class 来处理 2 个事件 - 一个在您前往 Google 之前触发,另一个在您返回时触发。首先,您限制可用于登录的域,然后确保具有正确域的电子邮件确实用于登录:

internal class GoogleAuthEvents : OAuthEvents
{
    private string _domainName;

    public GoogleAuthEvents(string domainName)
    {
        this._domainName = domainName?.ToLower() ?? throw new ArgumentNullException(nameof(domainName));
    }

    public override Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context)
    {
        return base.RedirectToAuthorizationEndpoint(new OAuthRedirectToAuthorizationContext(
            context.HttpContext,
            context.Options,
            context.Properties,
            $"{context.RedirectUri}&hd={_domainName}"));
    }

    public override Task TicketReceived(TicketReceivedContext context)
    {
        var emailClaim = context.Ticket.Principal.Claims.FirstOrDefault(
                c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");

        if (emailClaim == null || !emailClaim.Value.ToLower().EndsWith(_domainName))
        {
            context.Response.StatusCode = 403; // or redirect somewhere
            context.HandleResponse();
        }

        return base.TicketReceived(context);
    }
}

然后你需要通过 GoogleOptions class:

将这个 "events handler" 传递给中间件
app.UseGoogleAuthentication(new GoogleOptions
{
    Events = new GoogleAuthEvents(Configuration["Authentication:Google:LimitToDomain"])
})

我在搜索解决方案以将具有 OpenID Connect 集成的托管域指定到 Google 时发现了这个 post。我能够使用 Google.Apis.Auth.AspNetCore 包和以下代码让它工作。

在Startup.cs

services.AddGoogleOpenIdConnect(options =>
  {
    options.ClientId = "*****";
    options.ClientSecret = "*****";
    options.SaveTokens = true;
    options.EventsType = typeof(GoogleAuthenticationEvents);
  });
services.AddTransient(provider => new GoogleAuthenticationEvents("example.com"));

不要忘记 Startup.cs 的 Configure() 方法中的 app.UseAuthentication();

然后认证事件class

public class GoogleAuthenticationEvents : OpenIdConnectEvents
{
  private readonly string _hostedDomain;

  public GoogleAuthenticationEvents(string hostedDomain)
  {
    _hostedDomain = hostedDomain;
  }

  public override Task RedirectToIdentityProvider(RedirectContext context)
  {
    context.ProtocolMessage.Parameters.Add("hd", _hostedDomain);
    return base.RedirectToIdentityProvider(context);
  }

  public override Task TicketReceived(TicketReceivedContext context)
  {
    var email = context.Principal.FindFirstValue(ClaimTypes.Email);
    if (email == null || !email.ToLower().EndsWith(_hostedDomain))
    {
      context.Response.StatusCode = 403;
      context.HandleResponse();
    }
    return base.TicketReceived(context);
  }
}