2

I have the following application at GitHub and have deployed it to https://stratml.services on an Azure App Service with Authentication defined as Microsoft Account with anymous requests requiring a Microsoft Account sign in. In "prod" this challenge occurs, however https://stratml.services/Home/IdentityName returns no content.

I have been following this and this however I do not want to use EntityFramework and from the latter's description it seems to imply if I configure my Authentication scheme correctly I do not have to.

This following code is in my Start class:

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
        }).AddMicrosoftAccount(microsoftOptions =>
        {
            microsoftOptions.ClientId = Configuration["Authentication:AppId"];
            microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
            microsoftOptions.CallbackPath = new PathString("/.auth/login/microsoftaccount/callback");

        });

Update: Thanks to the first answer I was able to get, it now authorizes to Microsoft and attempts to feedback to my application however I receive the following error:

InvalidOperationException: No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Cookies

Please visit https://stratml.services/Home/IdentityName and the GitHub has been updated.

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
        }).AddCookie(option =>
        {
            option.Cookie.Name = ".myAuth"; //optional setting
        }).AddMicrosoftAccount(microsoftOptions =>
        {
            microsoftOptions.ClientId = Configuration["Authentication:AppId"];
            microsoftOptions.ClientSecret = Configuration["Authentication:Key"];

        });
Jason Lind
  • 263
  • 1
  • 2
  • 15

2 Answers2

2

I have checked this issue on my side, based on my test, you could confgure your settings as follows:

Under the ConfigureServices method, add the cookie and MSA authentication services.

services.AddAuthentication(options =>
{
    options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
    option.Cookie.Name = ".myAuth"; //optional setting
})
.AddMicrosoftAccount(microsoftOptions =>
{
    microsoftOptions.ClientId = Configuration["Authentication:AppId"];
    microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
});

Under the Configure method, add app.UseAuthentication().

TEST:

[Authorize]
public IActionResult Index()
{
    return Content(this.User.Identity.Name);
}

enter image description here

When I checking your online website, I found that you are using the Authentication and authorization in Azure App Service and Authenticate with Microsoft account.

enter image description here

AFAIK, when using the app service authentication, the claims could not be attached to current user, you could retrieve the identity name via Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"] or you could follow this similar issue to manually attach all claims for current user.

In general, you could either manually enable authentication middle-ware in your application or just leverage the app service authentication provided by Azure without changing your code for enabling authentication. Moreover, you could Remote debugging web apps to troubleshoot with your application.

UPDATE:

For enable the MSA authentication in my code and test it when deployed to azure, I disabled the App Service Authentication, then deployed my application to azure web app. I opened a new incognito window and found that my web app could work as expected.

enter image description here

If you want to simulate the MSA login locally and use Easy Auth when deployed to azure, I assumed that you could set a setting value in appsettings.json and manually add the authentication middle-ware for dev and override the setting on azure, details you could follow here. And you could use the same application Id and configure the following redirect urls:

https://stratml.services/.auth/login/microsoftaccount/callback //for easy auth
https://localhost:44337/signin-microsoft //manually MSA authentication for dev locally

Moreover, you could follow this issue to manually attach all claims for current user. Then you could retrieve the user claims in the same way for the manually MSA authentication and Easy Auth.

Bruce Chen
  • 18,207
  • 2
  • 21
  • 35
  • I really don't understand how the callback URL works, Azure provided with a valid one however I'm receiving an error that the one I have provided is not valid. Do I need to create some sort of landing there or something or do additional wiring? However this is very close to what I want. As much as I like Azure with this project I do not want a code dependency on it. – Jason Lind Dec 06 '17 at 14:25
  • I just updated my answer about some test, you could refer to it. – Bruce Chen Dec 07 '17 at 04:23
0

If you are using App Service Authentication (EasyAuth), according to Microsoft documentation page:

App Service passes some user information to your application by using special headers. External requests prohibit these headers and will only be present if set by App Service Authentication / Authorization. Some example headers include:

X-MS-CLIENT-PRINCIPAL-NAME

X-MS-CLIENT-PRINCIPAL-ID

X-MS-TOKEN-FACEBOOK-ACCESS-TOKEN

X-MS-TOKEN-FACEBOOK-EXPIRES-ON

Code that is written in any language or framework can get the information that it needs from these headers. For ASP.NET 4.6 apps, the ClaimsPrincipal is automatically set with the appropriate values.

So basically, if you are using ASP.NET Core 2.0, you need to set the ClaimPrincipal manually. What you need to use in order to fetch this headers and set the ClaimsPrincipal is AuthenticationHandler

public class AppServiceAuthenticationOptions : AuthenticationSchemeOptions
{
    public AppServiceAuthenticationOptions()
    {
    }
}

internal class AppServiceAuthenticationHandler : AuthenticationHandler<AppServiceAuthenticationOptions>
{
    public AppServiceAuthenticationHandler(
        IOptionsMonitor<AppServiceAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        return Task.FromResult(FetchAuthDetailsFromHeaders());
    }

    private AuthenticateResult FetchAuthDetailsFromHeaders()
    {
        Logger.LogInformation("starting authentication handler for app service authentication");

        if (Context.User == null || Context.User.Identity == null || Context.User.Identity.IsAuthenticated == false)
        {
            Logger.LogDebug("identity not found, attempting to fetch from the request headers");

            if (Context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
            {
                var headerId = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
                var headerName = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];

                var claims = new Claim[] {
                    new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", headerId),
                    new Claim("name", headerName)
                };
                Logger.LogDebug($"Populating claims with id: {headerId} | name: {headerName}");

                var identity = new GenericIdentity(headerName);
                identity.AddClaims(claims);

                var principal = new GenericPrincipal(identity, null);
                var ticket = new AuthenticationTicket(principal,
                    new AuthenticationProperties(),
                    Scheme.Name);

                Context.User = principal;
                return AuthenticateResult.Success(ticket);
            }
            else
            {
                return AuthenticateResult.Fail("Could not found the X-MS-CLIENT-PRINCIPAL-ID key in the headers");
            }
        }

        Logger.LogInformation("identity already set, skipping middleware");
        return AuthenticateResult.NoResult();
    }
}

You can then write an extension method for the middleware

public static class AppServiceAuthExtensions
{
    public static AuthenticationBuilder AddAppServiceAuthentication(this AuthenticationBuilder builder, Action<AppServiceAuthenticationOptions> configureOptions)
    {
        return builder.AddScheme<AppServiceAuthenticationOptions, AppServiceAuthenticationHandler>("AppServiceAuth", "Azure App Service EasyAuth", configureOptions);
    }
}

And add app.UseAuthentication(); in the Configure() method and put following in the ConfigureServices() method of your startup class.

services
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = "AppServiceAuth";
        options.DefaultChallengeScheme = "AppServiceAuth";
    })
    .AddAppServiceAuthentication(o => { });

If you need full claims details, you can retrieve it on the AuthenticationHandler by making request to /.auth/me and use the same cookies that you've received on the request.

Community
  • 1
  • 1
hendryanw
  • 1,819
  • 5
  • 24
  • 39
  • So this would replace MicrosoftAccount? Is there ant way to simulate this locally? – Jason Lind Dec 06 '17 at 13:14
  • Yes you would need to replace your Microsoft account authentication scheme with this because you are using App Service EasyAuth. AFAIK, there is no way currently to simulate this locally. On the other hand, you can configure the `AuthenticationHandler` to skip through the check and return `AuthenticationResult.NoResult()` immediately if e.g. the environment is Development. – hendryanw Dec 06 '17 at 13:49