I need help on using ADFS as the identity provider to authenticate an user from MS Office. What I want to achieve is to be able to directly open the credentials page from the ADFS server in MS Office and authenticate the user. Please bear with me through the long explanation.
We have a WebDAV implementation as part of our web application. Authentication is needed to access different resources through WebDAV.
We added support for MS-OFBA (Microsoft Office Form Based Authentication). When opening a DAV URL, in MS Word for instance, an embedded browser is used by Word to open our login web page.
This is done by a custom middleware that checks the User-Agent header, and if it contains "Microsoft Office", further checks if (Request.User == null || !Request.User.Identity.IsAuthenticated)
and if so, responds with HTTP 403 Forbidden and sets two special headers as indicated here:
Response.StatusCode = 403;
Response.Headers.Add("X-FORMS_BASED_AUTH_REQUIRED", new[] { string.Format("{0}?ReturnUrl={1}", loginUri, successUri) });
Response.Headers.Add("X-FORMS_BASED_AUTH_RETURN_URL", new[] { successUri });
This works fine, but form-based authentication is restricted in Office apps. We need to set a list of trusted locations for sign-in prompts and due to various deployment scenarios this seems to be complicated.
We also use an ADFS server as an identity provider to authenticate users, and this server doesn't seem to need to be listed in the list of hosts allowed to show sign-in prompts, so we're trying to use it directly from Office.
For the ADFS server, we register an OpenID Connect middleware in passive mode, which is challenged when the user clicks on a button in the login page.
The OIDC middleware sees that:
- the status was set to 401 Unauthorized
- there is an AuthenticationResponseChallenge with its type
so it reacts by:
- changing the status to 302
- adding a Location header with a generated URL that looks like this: https://adfs-provider.hostname/adfs/oauth2/authorize/?client_id=...&redirect_uri=...&response_type=id_token&scope=openid%20profile%20email&state=...&response_mode=form_post&nonce=...
- adding a Set-Cookie: OpenIdConnect.nonce.etc=...
This answer is not suited for MS Office, since it requires the data as described above (403, custom headers).
It's not clear how can I challenge the ADFS OIDC middleware so that MS Office will properly load its credentials page.
What I did so far is to add two new middlewares:
- one is registered after registering the OIDC middleware and it challenges this OIDC middleware when it detects an WebDAV URL request from an unauthenticated user
- one is registered before the OIDC middleware and, acts after the OIDC acted, by checking if the OIDC middleware modified the response, then re-modifies it to suit MS Office
This way, MS Office requests the credentials page from the ADFS provider, but the reply is 401 instead of 200 and the page itself, which results in the browser showing the Windows credentials popup that doesn't lead me anywhere. I suspect that the 401 from ADFS is due to the Set-Cookie: OpenIdConnect.nonce... missing. I watched the requests with Fiddler and this header is not sent by MS Office.
Here's some code:
// This is the "MS Office friend" middleware:
app.Use(async (context, next) =>
{
await next.Invoke();
// This runs after the OIDC middleware for ADFS modified the response.
// We check to see if it was challenged, hence it set the Location.
var provider = "ADFS";
var challenge = context.Authentication.AuthenticationResponseChallenge;
bool challengeHasAuthenticationTypes = challenge != null && challenge.AuthenticationTypes != null && challenge.AuthenticationTypes.Length != 0;
bool providerHasChallenge = challengeHasAuthenticationTypes && challenge.AuthenticationTypes.Any(at => string.Equals(at, provider, StringComparison.Ordinal));
if (providerHasChallenge)
{
// At this point, there is a also the Set-Cookie header set: OpenIdConnect.nonce...
// We're modififying the response so it suits MS Office.
var successUri = string.Format("{0}://{1}{2}", context.Request.Scheme, context.Request.Host.ToUriComponent(), "/");
var loginUri = context.Response.Headers["Location"];
context.Response.StatusCode = 403;
context.Response.Headers.Add("X-FORMS_BASED_AUTH_REQUIRED", new[] { string.Format("{0}?{1}={2}", loginUri, "ReturnUrl", successUri) });
context.Response.Headers.Add("X-FORMS_BASED_AUTH_RETURN_URL", new[] { successUri });
context.Response.Headers.Add("X-FORMS_BASED_AUTH_DIALOG_SIZE", new[] { string.Format("{0}x{1}", 800, 600) });
}
});
// This is the OIDC middleware.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions("ADFS")
{
// Options not included here for clarity
});
// This is the "challenger" middleware:
app.Use(async (context, next) =>
{
await next.Invoke();
if (context.Request.User != null && context.Request.User.Identity.IsAuthenticated
&& IsWebDav(context.Request))
{
// This is going to set an AuthenticationResponseChallenge for the OIDC middleware, which will make it run its logic.
context.Response.StatusCode = 401;
context.Authentication.Challenge("ADFS");
}
});
Is it possible to make MS Office send the Set-Cookie header as well? Or maybe there is another way to achieve what I need, that is to authenticate MS Office with an ADFS provider?
Thanks.