Recently setup JWT for an api within a Piranha project. I can hit the login endpoint (anonymous) without Piranha hijacking the request.

When I hit an API end point (after successful auth & receiving the JWT) with the [Authorize] attribute it gets picked up always by Piranha. it attempts to redirect me to the CMS login.

Being that this is an API the redirection to a web page is not acceptable behavior. Anyway to rectify this behavior?

        var appSettingsSection = config.GetSection("AppSettings");
        services.Configure<AppSettings> (appSettingsSection);
        // configure jwt authentication
        var appSettings = appSettingsSection.Get<AppSettings> ();
        var key = Encoding.UTF8.GetBytes (appSettings.Secret); // todo - UTF8 vs ASCII?!
        services.AddAuthentication (x => {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer (x => {
                x.RequireHttpsMetadata = false;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey (key),
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
            });
        services.AddPiranhaApplication ();
        services.AddPiranhaFileStorage ();
        services.AddPiranhaImageSharp ();

            services.AddPiranhaEF (options =>
                options.UseSqlite ("Filename=./piranha.db"));
            services.AddPiranhaIdentityWithSeed<IdentitySQLiteDb> (options =>
                options.UseSqlite ("Filename=./piranha.db"));
        }
        services.AddPiranhaManager ();
        services.AddPiranhaMemCache ();

        services.AddMvc (config => {
                config.ModelBinderProviders.Insert (0,
                    new Piranha.Manager.Binders.AbstractModelBinderProvider ());
            }).SetCompatibilityVersion (CompatibilityVersion.Version_2_1);

——— Update ———
With help from @hakan the following attribtute works:

[ApiController]
[Route ("api/v1/")]
[Produces("application/json")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ApiController : ControllerBase {

The issue here is really how ASP.NET Identity is interacting with JWT. In your startup your calling:

services.AddPiranhaIdentityWithSeed<IdentitySQLiteDb> (options =>
    options.UseSqlite ("Filename=./piranha.db"));

which means that the setup uses the default options Piranha sets, some of these options are actually more geared towards development (like password strength). You can provide your own options and cookie options into the method, like so:

services.AddPiranhaIdentityWithSeed<IdentitySQLiteDb> (options =>
    options.UseSqlite ("Filename=./piranha.db"), identityOptions, cookieOptions);

The default Identity Options used are:

// Password settings
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 1;

// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;

// User settings
options.User.RequireUniqueEmail = true;

And these are the default cookie options:

options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LoginPath = "/manager/login";
options.AccessDeniedPath = "/manager/login";
options.SlidingExpiration = true;

Best regards

Håkan

1

If you still want to use Cookie Auth with your API endpoints, what’s below could work, with some caveats. To set it up, do the following:

  1. Supply the default options per Håkan’s post, updating as needed
  2. Add a custom handler for cookieOptions.Events.OnRedirectToLogin.

Caveats:

  1. .NET Core will still re-direct if the response has no body.
  2. If you are using CORs, the headers are no longer sent automatically so you need to figure out how to do this.
  3. I am using the Axios client in my front end project. Axios by default does not send cookies with PUT/DELETE requests even with the withCredentials parameter set to true.
identityOptions => 
                {
                    // Password settings
                    identityOptions.Password.RequireDigit = false;
                    identityOptions.Password.RequiredLength = 6;
                    identityOptions.Password.RequireNonAlphanumeric = false;
                    identityOptions.Password.RequireUppercase = false;
                    identityOptions.Password.RequireLowercase = false;
                    identityOptions.Password.RequiredUniqueChars = 1;

                        // Lockout settings
                        identityOptions.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
                        identityOptions.Lockout.MaxFailedAccessAttempts = 10;
                        identityOptions.Lockout.AllowedForNewUsers = true;

                        // User settings
                        identityOptions.User.RequireUniqueEmail = true;

                    },
                    cookieOptions => 
                    {
                        cookieOptions.Cookie.HttpOnly = true;
                        cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(30);
                        cookieOptions.LoginPath = "/manager/login";
                        cookieOptions.AccessDeniedPath = "/manager/login";
                        cookieOptions.SlidingExpiration = true;

                        var defaultAction = cookieOptions.Events.OnRedirectToLogin;
                        cookieOptions.Events.OnRedirectToLogin = (context) =>
                        {

                            if (context.Request.Path.Value.StartsWith("/api"))
                            {
                                context.Response.StatusCode = 401;
                                context.Response.BodyWriter.WriteAsync(new ReadOnlyMemory<byte>(Encoding.ASCII.GetBytes("unauthorized.")));
                                return Task.CompletedTask;
                            }
                            else
                            {
                                return defaultAction(context);
                            }
                        };
                    });