Reputation: 531
We are looking to implement MS-OFBA in the ASP.NET Core WebDAV Server Sample (https://www.webdavsystem.com/server/server_examples/cross_platform_asp_net_core_sql/). The sample already has code for basic and digest authentication but we need to support MS-OFBA.
I've implemented an MSOFBAuthMiddleware class similar to the existing basic and digest middleware classes where we set the required "X-FORMS_BASED_AUTH_" headers if it's a request from an Office application.
This works up to a point:
Initially we've been trying this with a local login page but ultimately we'd prefer to use our existing Identity Server login page. Again we can get the login page to show but the redirect isn't working.
In Identity Server after logging in we should redirect to "/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" but in actuality we are redirected to the root of the application "/".
Update: I've resolved this redirection problem and Identity Server now redirects to the correct URL, but the httpContext.User.Identity.IsAuthenticated value is still always false in the middleware.
Startup.cs (partial)
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 (partial)
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 (partial)
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 (partial) - from Identity Server project
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
}
};
Thanks, Stuart.
Upvotes: 6
Views: 1678
Reputation: 5904
Here is how to add WebDAV with MS-OFBA to your .NET Core project. Note that you will need the the IT Hit WebDAV wizards for Visual Studio v11.0.10207 or later.
Complete the wizard and run the project. You will be navigated to the website login page or Azure AD login page, depending on the options selected on the second step. In case you have selected 'Individual User Accounts' create an account and login. Select 'Edit' on MS Office document, it will show the website login or Azure Login inside the MS Office MS-OFBA dialog.
Please see detailed instructions with screenshots in the Creating WebDAV Server with Azure AD Authentication article.
Upvotes: 0