4

I have an Owin application I'm running in Mono, and I'm trying to get authentication to work properly on it. I used the info on this page as a start. I quickly realized that Owin Authentication uses some Windows specific libraries. This question had a workaround for that, though, which I thought would be enough. It wasn't.

The following code throws the exception described in the other question (in another place than the question describes (see comment in code)). If I try to comment out stuff to locate the error exceptions about Owin pipeline are being thrown (because of dependencies). If I comment out enough to remove that the first exception appears again.

Has anybody successfully been able to set up authentication in Owin (using AspNet Identity) in Mono?

Startup

public void Configure(IAppBuilder app)
{
    // Add the AspNet Identity user manager to the Owin context
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

    // This was the first line to fail, but adding the AesDataProtectorProvider as
    // described in the referred question fixed that
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/login"),
        Provider = new CookieAuthenticationProvider
        {
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                validateInterval: TimeSpan.FromMinutes(30),
                regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
        },
        TicketDataFormat = new SecureDataFormat<AuthenticationTicket>(
            DataSerializers.Ticket,
            new AesDataProtectorProvider("myAuthKey"),
            TextEncodings.Base64)
    });

    // This causes an exception complaining that the Windows only assembly
    // DpapiDataProtector can't be loaded, as described in the referred question
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

    // Set up the custom middleware as described in the first link
    // Something inside this causes other exceptions.
    app.Use(typeof(AuthMiddleware), app, new AuthOptions());
}

AuthMiddleware

public class AuthMiddleware : AuthenticationMiddleware<AuthOptions>
{
    public AuthMiddleware(OwinMiddleware next, IAppBuilder app, AuthOptions options)
        : base(next, options)
    {
        if (string.IsNullOrEmpty(Options.SignInAsAuthenticationType))
        {
            options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType();
        }
        if (options.StateDataFormat == null)
        {
            var dataProtector = app.CreateDataProtector(typeof(AuthMiddleware).FullName,
            options.AuthenticationType);
            options.StateDataFormat = new PropertiesDataFormat(dataProtector);
        }
    }

    protected override AuthenticationHandler<AuthOptions> CreateHandler()
    {
        return new AuthHandler();
    }
}

AuthOptions

public class AuthOptions : AuthenticationOptions
{
    public AuthOptions()
        : base("MyApp")
    {
        Description.Caption = "MyApp";
        // Where to redirect requests if not authenticated
        CallbackPath = new PathString("/login");            AuthenticationMode = AuthenticationMode.Passive;
    }

    public PathString CallbackPath { get; set; }
    public string SignInAsAuthenticationType { get; set; }
    public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
}

AuthHandler

public class AuthHandler : AuthenticationHandler<AuthOptions>
{
    protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
    {
        var identity = new ClaimsIdentity(Options.SignInAsAuthenticationType);
        var properties = Options.StateDataFormat.Unprotect(Request.Query["state"]);
        return Task.FromResult(new AuthenticationTicket(identity, properties));
    }

    protected override Task ApplyResponseChallengeAsync()
    {
        if (Response.StatusCode == 401)
        {
            var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
            if (challenge != null)
            {
                var state = challenge.Properties;
                if (string.IsNullOrEmpty(state.RedirectUri))
                {
                    state.RedirectUri = Request.Uri.ToString();
                }
                var stateString = Options.StateDataFormat.Protect(state);
                Response.Redirect(WebUtilities.AddQueryString(Options.CallbackPath.Value, "state", stateString));
            }
        }
        return Task.FromResult<object>(null);
    }

    public override async Task<bool> InvokeAsync()
    {
        Request.Environment.Add("Context", Context);

        // If user is not logged in and tries to access any page that is not in
        // the list of allowed pages, redirect to login page
        if (Context.Authentication.User == null && 
            !Request.Path.ToString().StartsWith("/login"))
        {
            Response.Redirect(Options.CallbackPath.Value);
            return true;
        }

        return false;
    }
}
Community
  • 1
  • 1
TheHvidsten
  • 4,028
  • 3
  • 29
  • 62

4 Answers4

1

GitHub repository to showcase Mono WebAPI, OAuth2 Bearer token authentication, AspNet.Identity + MySQL UserStore:

https://github.com/shturm/mono-webapi

enter image description here enter image description here

Few things to get clear:

  1. AspNet.Identity is a set of interfaces (Microsoft.AspNet.Identity.Core) and their implementations (Microsoft.AspNet.Identity.EntityFramework/Owin) that roughly describe repository pattern for persisting accounts in a database.
  2. AspNet.Identity does not do authentication/authorization. It creates, saves, updates, deletes users while taking care of passwords.
  3. AspNet.Identity main classes used are UserManager (from Microsoft.AspNet.Identity.Core) and SignInManger (from Microsoft.AspNet.Identity.Owin). SignInManager is not necessary to perform any kind of OWIN authentication.
  4. The code you posted is strictly Microsoft.Owin related and has nothing to do with AspNet.Identity except for ApplicationUserManager which in Visual Studio templates extends Microsoft.AspNet.Identity.UserManager.
shturm
  • 857
  • 7
  • 18
1

Apparently, the code submitted above includes more than is necessary to get Owin authentication to work (and which doesn't work with Mono). Having researched and tried a lot of permutations I do believe I have authentication in a working condition now. I don't know if it necessarilly is the correct condition, but as long as it works...

Using the following setup only the pages in the list of allowed pages (Controlled by the method public override async Task<bool> InvokeAsync() in AuthHandler) are accessible to users who are not logged in. On a login page I can use the ApplicationUserManager to sign in a user. After the signin all pages hosted by Owin are accessible.

Startup

app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/login"),
    Provider = new CookieAuthenticationProvider
    {
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    },
    TicketDataFormat = new SecureDataFormat<AuthenticationTicket>(
        DataSerializers.Ticket,
        new AesDataProtectorProvider("myAuthKey"),
        TextEncodings.Base64)
});

app.Use(typeof(AuthMiddleware), app, new AuthOptions());

AuthMiddleware

public class AuthMiddleware : AuthenticationMiddleware<AuthOptions>
{
    public AuthMiddleware(OwinMiddleware next, IAppBuilder app, AuthOptions options)
        : base(next, options)
    {
        if (options.StateDataFormat == null)
        {
            options.StateDataFormat = new PropertiesDataFormat(new AesDataProtectorProvider("myAuthKey"));
        }
    }

    protected override AuthenticationHandler<AuthOptions> CreateHandler()
    {
        return new AuthHandler();
    }
}

AuthHandler

public class AuthHandler : AuthenticationHandler<AuthOptions>
{
    protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
    {
        // This method never gets called in the current setup,
        // but it is required because the compilation fails otherwise.
        // Therefore only return an empty object.
        return Task.FromResult<AuthenticationTicket>(null);
    }

    protected override Task ApplyResponseChallengeAsync()
    {
        if (Response.StatusCode == 401)
        {
            var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
            if (challenge != null)
            {
                var state = challenge.Properties;
                if (string.IsNullOrEmpty(state.RedirectUri))
                {
                    state.RedirectUri = Request.Uri.ToString();
                }
                var stateString = Options.StateDataFormat.Protect(state);
                Response.Redirect(WebUtilities.AddQueryString(Options.CallbackPath.Value, "state", stateString));
            }
        }
        return Task.FromResult<object>(null);
    }

    public override async Task<bool> InvokeAsync()
    {
        // If user is not logged in and tries to access any page that is not in
        // the list of allowed pages, redirect to login page.
        // Add any additional pages not protected by authentication here
        if (Context.Authentication.User == null && 
            !Request.Path.ToString().StartsWith("/login"))
        {
            Response.Redirect(Options.CallbackPath.Value);
            return true;
        }

        return false;
    }
}
TheHvidsten
  • 4,028
  • 3
  • 29
  • 62
0

Authentication in Owin use async pipeline, but Mono does not support one.

I make pull request for mono https://github.com/mono/mono/pull/3048. It makes Synchronous execution of asynchronous web api stack.

Now you may use Owin Authentication with this fix.

Oleg Belousov
  • 509
  • 3
  • 8
0

I'm leaving an answer related to, but not addressing, the OP's problem as a resource for anyone else out there who haven't made the jump to .NET Core.

On Mono, Mod-Mono and Apache, my experience was that the regenerateIdentity function was failing on Mono, but would succeed when run locally from VS.

Before the fix (startup.auth.cs)

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
   AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
   LoginPath = new PathString("/Account/Login"),
   Provider = new CookieAuthenticationProvider
   {  
       OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
           validateInterval: TimeSpan.FromMinutes(2),
           regenerateIdentity: (manager, user) => AuthenticationService.RefreshIdentityAsync(manager, user))
   }
});

I noticed in Chrome's Dev Tools (application tab) that the cookie remained, because it was set to expire an extra 2 weeks into the future.

My solution was to adjust the 'Expires' time of the cookie such that it aligned with the validation interval. Now the client is responsible for the cookie eviction, and Mono doesn't have to handle the cookie on regeneration. I realize this is not a generic fix, but for many, I'm certain this is good enough.

After the fix

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
   AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
   LoginPath = new PathString("/Account/Login"),
   ExpireTimeSpan = TimeSpan.FromMinutes(2),
   SlidingExpiration = false,
   Provider = new CookieAuthenticationProvider
   {
       OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
           validateInterval: TimeSpan.FromMinutes(2),
           regenerateIdentity: (manager, user) => AuthenticationService.RefreshIdentityAsync(manager, user))
   }
});
daniel.caspers
  • 1,660
  • 1
  • 18
  • 22