我们希望在 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
}
};
谢谢,斯图尔特。