5

我们希望在 ASP.NET Core WebDAV 服务器示例 ( https://www.webdavsystem.com/server/server_examples/cross_platform_asp_net_core_sql/ ) 中实现 MS-OFBA。该示例已经包含基本和摘要身份验证的代码,但我们需要支持 MS-OFBA。

我已经实现了一个类似于现有基本和摘要中间件类的 MSOFBAuthMiddleware 类,如果它是来自 Office 应用程序的请求,我们在其中设置所需的“X-FORMS_BASED_AUTH_”标头。

这在一定程度上起作用:

  • 标头被发回,Word(Excel 等)打开对话框并显示登录页面。
  • 我们可以成功登录,设置认证cookie,页面重定向用户。
  • 尽管在此重定向上,当我们检查用户是否已通过身份验证时,httpContext.User.Identity.IsAuthenticated 值始终为 false。

最初,我们一直在尝试使用本地登录页面,但最终我们更愿意使用现有的 Identity Server 登录页面。我们可以再次显示登录页面,但重定向不起作用。

在登录后的 Identity Server 中,我们应该重定向到 "/connect/authorize/login?client_id=mvc.manual&response_type=id_token&scope=openid%20profile%20&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Faccount%2Fcallback&state=random_state&nonce=random_nonce&response_mode= form_post”,但实际上我们被重定向到应用程序“/”的根目录。

更新:我已经解决了这个重定向问题,Identity Server 现在重定向到正确的 URL,但中间件中的 httpContext.User.Identity.IsAuthenticated 值仍然始终为 false。

Startup.cs(部分)

public void ConfigureServices(IServiceCollection services)
{
    services.AddWebDav(Configuration, HostingEnvironment);
    services.AddSingleton<WebSocketsService>();
    services.AddMvc();
    services.Configure<DavUsersOptions>(options => Configuration.GetSection("DavUsers").Bind(options));
    services.AddAuthentication(o =>
        {
            o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        }
    ).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();

    //app.UseBasicAuth();
    //app.UseDigestAuth();
    app.UseMSOFBAuth();
    app.UseAuthentication();

    app.UseWebSockets();
    app.UseWebSocketsMiddleware();
    app.UseMvc();
    app.UseWebDav(HostingEnvironment);
}

MSOFBAuthMiddleware.cs(部分)

public async Task Invoke(HttpContext context)
{
    // If Authorize header is present - perform request authenticating.
    if (IsAuthorizationPresent(context.Request))
    {
        ClaimsPrincipal userPrincipal = AuthenticateRequest(context.Request);
        if (userPrincipal != null)
        {
            // Authenticated succesfully.
            context.User = userPrincipal;
            await next(context);
        }
        else
        {
            // Invalid credentials.
            Unauthorized(context);
            return;
        }
    }
    else
    {
        if (IsOFBAAccepted(context.Request))
        {
            // The Unauthorized method subsequently call the SetAuthenticationHeader() method below.
            Unauthorized(context);
            return;
        }
        else
        {
            await next(context);
        }
    }
}

/// <summary>
/// Analyzes request headers to determine MS-OFBA support.
/// </summary>
/// <remarks>
/// MS-OFBA is supported by Microsoft Office 2007 SP1 and later versions 
/// and any application that provides X-FORMS_BASED_AUTH_ACCEPTED: t header 
/// in OPTIONS request.
/// </remarks>
private bool IsOFBAAccepted(HttpRequest request)
{
    // In case application provided X-FORMS_BASED_AUTH_ACCEPTED header
    string ofbaAccepted = request.Headers["X-FORMS_BASED_AUTH_ACCEPTED"];
    if ((ofbaAccepted != null) && ofbaAccepted.Equals("T", StringComparison.CurrentCultureIgnoreCase))
    {
        return true;
    }

    // Microsoft Office does not submit X-FORMS_BASED_AUTH_ACCEPTED header, but it still supports MS-OFBA,
    // Microsoft Office includes "Microsoft Office" string into User-Agent header
    string userAgent = request.Headers["User-Agent"];
    if ((userAgent != null) && userAgent.Contains("Microsoft Office"))
    {
        return true;
    }

    return false;
}

/// <summary>
/// Sets authentication header to request basic authentication and show login dialog.
/// </summary>
/// <param name="context">Instance of current context.</param>
/// <returns>Successfull task result.</returns>
protected override async Task SetAuthenticationHeader(object context)
{
    HttpContext httpContext = (HttpContext)context;

    if (httpContext.User == null || !httpContext.User.Identity.IsAuthenticated)
    {
        string redirectLocation = httpContext.Response.Headers["Location"];

        string successUri = "http://localhost:5000/account/success";

        var client = new DiscoveryClient("http://accounts:43000");
        client.Policy.RequireHttps = false;
        var disco = await client.GetAsync();
        var loginUri = new AuthorizeRequest(disco.AuthorizeEndpoint).CreateAuthorizeUrl(
            clientId: "mvc.manual",
            responseType: "id_token",
            scope: "openid profile ",
            redirectUri: "http://localhost:5000/account/callback",
            state: "random_state",
            nonce: "random_nonce",
            responseMode: "form_post");

        httpContext.Response.StatusCode = 403;
        httpContext.Response.Headers.Add("X-FORMS_BASED_AUTH_REQUIRED", new[] { loginUri });
        httpContext.Response.Headers.Add("X-FORMS_BASED_AUTH_RETURN_URL", new[] { successUri });
        httpContext.Response.Headers.Add("X-FORMS_BASED_AUTH_DIALOG_SIZE", new[] { string.Format("{0}x{1}", 800, 640) });
    }
}

AccountController.cs(部分)

public async Task<IActionResult> Callback()
{
    var state = Request.Form["state"].FirstOrDefault();
    var idToken = Request.Form["id_token"].FirstOrDefault();
    var error = Request.Form["error"].FirstOrDefault();

    if (!string.IsNullOrEmpty(error)) throw new Exception(error);
    if (!string.Equals(state, "random_state")) throw new Exception("invalid state");

    var user = await ValidateIdentityToken(idToken);

    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user);

    return Redirect("http://localhost:5000/account/success");
}

private async Task<ClaimsPrincipal> ValidateIdentityToken(string idToken)
{
    var user = await ValidateJwt(idToken);

    var nonce = user.FindFirst("nonce")?.Value ?? "";
    if (!string.Equals(nonce, "random_nonce")) throw new Exception("invalid nonce");

    return user;
}

private static async Task<ClaimsPrincipal> ValidateJwt(string jwt)
{
    // read discovery document to find issuer and key material
    var client = new DiscoveryClient("http://accounts:43000");
    client.Policy.RequireHttps = false;

    var disco = await client.GetAsync();

    var keys = new List<SecurityKey>();
    foreach (var webKey in disco.KeySet.Keys)
    {
        var e = Base64Url.Decode(webKey.E);
        var n = Base64Url.Decode(webKey.N);

        var key = new RsaSecurityKey(new RSAParameters { Exponent = e, Modulus = n })
        {
            KeyId = webKey.Kid
        };

        keys.Add(key);
    }

    var parameters = new TokenValidationParameters
    {
        ValidIssuer = disco.Issuer,
        ValidAudience = "mvc.manual",
        IssuerSigningKeys = keys,

        NameClaimType = JwtClaimTypes.Name,
        RoleClaimType = JwtClaimTypes.Role
    };

    var handler = new JwtSecurityTokenHandler();
    handler.InboundClaimTypeMap.Clear();

    var user = handler.ValidateToken(jwt, parameters, out var _);
    return user;
}

Clients.cs(部分)——来自 Identity Server 项目

public static Client WebDavServiceManual { get; } = new Client
{
    ClientId = "mvc.manual",
    ClientName = "MVC Manual",
    ClientUri = "http://localhost:5000",
    AllowedGrantTypes = GrantTypes.Implicit,
    RedirectUris = { "http://localhost:5000/account/callback", "http://localhost:5000/account/success" },
    PostLogoutRedirectUris = { "http://localhost:5000/" },
    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        IdentityServerConstants.StandardScopes.Email,
        IdentityServerConstants.StandardScopes.OfflineAccess
    }
};

谢谢,斯图尔特。

4

1 回答 1

0

以下是如何将带有 MS-OFBA 的 WebDAV 添加到您的 .NET Core 项目中。请注意,您将需要适用于 Visual Studio v11.0.10207 或更高版本的 IT Hit WebDAV 向导。

  1. 从Visual Studio Marketplace或此处的产品下载区下载并安装 Visual Studio 的 WebDAV 向导。
  2. 在 Visual Studio 中创建 ASP.NET Core Web 应用程序。创建时选择以下身份验证选项之一:“个人用户帐户”或“工作或学校帐户”。
  3. 从项目上下文菜单中运行“添加 WebDAV 服务器实现”向导。在身份验证步骤中,选择“MS-OFBA”选项。

完成向导并运行项目。您将导航到网站登录页面或 Azure AD 登录页面,具体取决于在第二步中选择的选项。如果您选择了“个人用户帐户”,请创建一个帐户并登录。在 MS Office 文档上选择“编辑”,它将在 MS Office MS-OFBA 对话框中显示网站登录或 Azure 登录。

请参阅使用 Azure AD 身份验证创建 WebDAV 服务器一文中的屏幕截图的详细说明。

于 2021-04-10T04:41:25.087 回答